diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb713230..18297b0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## [1.42.1.0] - 2026-05-19 + +## **Embedder PTY teardown stops clobbering — gbrowser's phoenix overlay survives every shutdown.** +## **`buildFetchHandler` gains an explicit ownership flag for terminal-agent files; CLI behavior preserved bit-for-bit.** + +`browse/src/server.ts` factory shutdown unconditionally killed the terminal-agent and unlinked its discovery files on every teardown. Correct for gstack's CLI path, wrong for embedders that pass their own pre-launched `BrowserManager` and run their own PTY server. Their `terminal-port` file got clobbered every cycle, `/health.terminalPort` reported null until the overlay rewrote it. gbrowser's phoenix overlay shipped a client-side mitigation; with this PR landed, that mitigation becomes redundant. The new `ServerConfig.ownsTerminalAgent?: boolean` (default `true`) gates the three teardown side effects together: `pkill -f terminal-agent\.ts`, `safeUnlinkQuiet(/terminal-port)`, `safeUnlinkQuiet(/terminal-internal-token)`. Embedders pass `false` to keep their PTY lifecycle intact. + +### The numbers that matter + +Source: `bun test browse/test/server-embedder-terminal-port.test.ts browse/test/server-factory.test.ts` — 32 tests, all green. Static-grep test pins the CLI `start()` call site so a refactor that drops the explicit `: true` fails CI. + +| Surface | Before | After | +|---|---|---| +| gbrowser phoenix overlay teardown | `terminal-port` unlinked every cycle; `/health.terminalPort: null` until overlay rewrites; client-side mitigation required | Pass `ownsTerminalAgent: false` — files untouched, embedder owns full lifecycle | +| gstack CLI shutdown | `pkill` + 2 unlinks fire | Identical (default `true`, explicit `: true` at `start()` call site documents intent + static-grep test) | +| Test runner safety | n/a | `spawnSync` stubbed in all 4 cases so real `pkill -f terminal-agent\.ts` cannot run on developer machine | +| Multi-case shutdown tests | Module-scoped `isShuttingDown` silently no-ops 2nd shutdown | New `__resetShuttingDown` test-only export mirrors `__resetRegistry` precedent | +| Real-daemon collision risk | Test mutates `~/.gstack/.../terminal-port` — would clobber a running developer daemon | `beforeAll` saves real contents, `afterAll` restores; tests safe to run while gstack is alive | + +### What this means for builders + +If you embed gstack's `buildFetchHandler` and run your own PTY server, pass `ownsTerminalAgent: false` in your cfg and your `terminal-port`/`terminal-internal-token` files survive every gstack teardown — no more client-side rewrite mitigation. If you use the gstack CLI, nothing changes. The flag is the third caller-owned teardown gate in `ServerConfig` (joining `xvfb?` and `proxyBridge?`); if a fourth appears we collapse to an ownership object. + +### Itemized changes + +**Added** +- `ServerConfig.ownsTerminalAgent?: boolean` in `browse/src/server.ts` (default `true`). JSDoc enumerates all three gated side effects, the pkill regex breadth caveat, and the polarity inversion vs `xvfb?`/`proxyBridge?` (which gate by *presence* of caller-owned handles) +- `__resetShuttingDown()` test-only export in `browse/src/server.ts`, mirroring `__resetRegistry` precedent in `token-registry.ts`. JSDoc warns about production-import footgun +- `browse/test/server-embedder-terminal-port.test.ts` (4 tests): `ownsTerminalAgent: false` preserves files + skips pkill, explicit `true` deletes + invokes pkill, unset defaults to `true`, static-grep test asserts CLI call site documents intent. Tests save+restore real-daemon `terminal-port`/`terminal-internal-token` contents in `beforeAll`/`afterAll` so a running developer session is never clobbered + +**Changed** +- `buildFetchHandler` JSDoc references the new field alongside `beforeRoute` and `browserManager` in the embedder-composition paragraph +- CLI `start()` call site explicitly passes `ownsTerminalAgent: true` with a comment pointing at `cli.ts:1037-1063`. Documents intent + caught by the new static-grep test if a refactor drops it +- Strict opt-out semantics: `cfg.ownsTerminalAgent === false ? false : true` — only explicit `false` flips the gate. Defends against JS callers bypassing TS and passing truthy non-bool values + +**Removed** +- Dead `try { safeUnlinkQuiet(...) } catch {}` wrappers inside the new gate. `safeUnlinkQuiet` already swallows all errors internally; the outer try/catch was slop-scan flagged dead code + +**For contributors** +- Followup TODOs filed in `TODOS.md`: identity-based terminal-agent kill (replace `pkill -f` with PID-tracked `process.kill`), the pre-existing `shutdown()` reads module-level `config` (composition gap with parallel `chromiumProfile` gap), and the 4th-gate-collapse-to-ownership-object trigger +- Plan + reviews under `~/.gstack/projects/garrytan-gstack/`: autoplan CEO + Eng dual voices (Codex + Claude subagent), interactive `/plan-eng-review` (D3: drop dead try/catch), `/ship` adversarial pass (strict-bool + JSDoc hardening + test save/restore) + ## [1.42.0.0] - 2026-05-19 ## **Daegu wave: 23 community-filed bugs land as one bisect-clean PR with the documented sidebar security stack finally enforced.** diff --git a/CLAUDE.md b/CLAUDE.md index 6cbff85f9..3ff25fffe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -236,6 +236,20 @@ Activity / Refs / Inspector as debug overlays behind the footer's flow, dual-token model, and threat-model boundary — silent failures here usually trace to not understanding the cross-component flow. +**Embedder terminal-agent ownership** (v1.42.1.0+). `buildFetchHandler` +in `browse/src/server.ts` accepts `ServerConfig.ownsTerminalAgent?: +boolean` (default `true`). When `true`, factory shutdown runs the full +teardown: `pkill -f terminal-agent\.ts` plus `safeUnlinkQuiet` on +`/terminal-port` and `/terminal-internal-token`. +Embedders (e.g. the gbrowser phoenix overlay) that pre-launch their +own PTY server must pass `false` so their discovery files survive +gstack teardown cycles. The flag is the third caller-owned teardown +gate in `ServerConfig` (alongside `xvfb?` and `proxyBridge?`); polarity +is inverted (explicit bool vs presence) and documented in the field's +JSDoc. CLI `start()` always passes `true` explicitly — the static-grep +test in `browse/test/server-embedder-terminal-port.test.ts` fails CI +if a refactor drops it. + **WebSocket auth uses Sec-WebSocket-Protocol, not cookies.** Browsers can't set `Authorization` on a WebSocket upgrade, but they CAN set `Sec-WebSocket-Protocol` via `new WebSocket(url, [token])`. The agent diff --git a/TODOS.md b/TODOS.md index 0516f972e..01fdc1c85 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,5 +1,99 @@ # TODOS +## browse server: terminal-agent teardown follow-ups (filed v1.41 via /plan-eng-review) + +### P3: Identity-based terminal-agent kill (replace pkill regex with PID) + +**What:** Record the spawned terminal-agent PID at `browse/src/cli.ts:1057` and +replace `pkill -f terminal-agent\.ts` at both `cli.ts:1047` and +`server.ts:1281` (now inside the `if (ownsTerminalAgent)` gate) with +`process.kill(pid, signal)` against the recorded PID. + +**Why:** `pkill -f terminal-agent\.ts` matches by command-line regex, so today +it can kill ANY process whose argv contains `terminal-agent.ts` — sibling +gstack sessions, editor processes that have the file open, a second gstack +run on the same host. Latent footgun for the CLI path, not just embedders. + +**Pros:** Removes a real cross-session foot-cannon. PID-based kill is the +correct identity primitive. Lets us tighten `pkill -f`'s broad-match warning +in the new `ownsTerminalAgent` JSDoc to "historical" rather than "current". + +**Cons:** Requires threading the PID through the CLI-to-server state path +(currently the parent server reads `terminal-port` to discover the agent; it +would also need `terminal-agent-pid`). Touches `cli.ts`, `server.ts`, and +`terminal-agent.ts` together — bigger surface than the v1.41 fix. + +**Context:** Surfaced by both Codex and Claude subagent during /autoplan +review of the `ownsTerminalAgent` gate. Currently documented as out-of-scope +in `browse/src/server.ts` JSDoc for `ServerConfig.ownsTerminalAgent`. The +embedder fix (ownsTerminalAgent: false) means embedders don't hit this; CLI +users still do. + +**Depends on:** None. + +--- + +### P3: shutdown() reads module-level `config`, not `cfg.config` (composition gap) + +**What:** `browse/src/server.ts:shutdown()` reads `path.dirname(config.stateFile)` +where `config` is the module-level value resolved at import time, not the +`cfg.config` passed into `buildFetchHandler`. Same gap applies to +`cleanSingletonLocks(resolveChromiumProfile())` at server.ts:1298 — should +read `cfg.chromiumProfile`. + +**Why:** Embedders today happen to share state-dir resolution with the CLI +(both go through `resolveConfig()` against the same env), so this doesn't +bite. But if an embedder ever passes a divergent `cfg.config` (e.g., a test +harness pointing at a temp dir), shutdown will operate on the wrong paths. +The `ownsTerminalAgent` flag exposes the problem without fixing it. + +**Pros:** Closes the embedder-composition story properly. Pairs with +`cfg.chromiumProfile` to give a single coherent "this factory teardown +respects cfg" contract. + +**Cons:** Pre-existing — not a regression. Two call sites today (1285 for +terminal files, 1298 for chromium locks). Threading `cfg.config` and +`cfg.chromiumProfile` into the right closures is straightforward but +broader than the v1.41 fix. + +**Context:** Flagged by both Codex and Claude subagent in the /plan-eng-review +dual voices. Documented as out-of-scope in the v1.41 plan; same shape as the +`chromiumProfile` PR-body note to the gbrowser team. + +**Depends on:** None. + +--- + +### P3: Ownership-object refactor if a 4th caller-owned teardown gate appears + +**What:** Today `ServerConfig` has three caller-owned teardown gates: +`xvfb?` (presence ⇒ don't close), `proxyBridge?` (same), and now +`ownsTerminalAgent` (explicit boolean). If a 4th gate appears, collapse to +`cfg.callerOwns?: Set<'terminalAgent' | 'xvfb' | 'proxyBridge' | ...>` or +similar. + +**Why:** Three independent flags is below the refactor threshold — each +field has clear, distinct semantics and the JSDoc voice is consistent. A +fourth tips the cost balance: the per-field surface gets noisy, and +"what does this factory own?" becomes a question you have to ask of three +or four scattered fields instead of one explicit set. + +**Pros:** Single source of truth for "what gstack tears down". Trivial +extension surface for future caller-owned resources. Easier to assert in +tests ("the set should contain X, not Y"). + +**Cons:** Premature today. The polarity-inversion note in the +`ownsTerminalAgent` JSDoc only hurts a little — it's one anomaly, not a +pattern. Refactoring now to an ownership object would touch every embedder. + +**Context:** Recommended by Claude subagent during /plan-ceo-review dual +voice (autoplan). Trigger: a 4th caller-owned teardown gate in this same +`ServerConfig` shape. + +**Depends on:** A 4th gate to motivate the refactor. + +--- + ## /sync-gbrain memory stage perf follow-up ### P2: Investigate `gbrain import` perf on large staging dirs diff --git a/VERSION b/VERSION index dd19f3311..65881123f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.42.0.0 +1.42.1.0 diff --git a/browse/src/server.ts b/browse/src/server.ts index 25bbca8a1..9f6866a9d 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -205,6 +205,35 @@ export interface ServerConfig { * dispatch; returning null falls through. */ beforeRoute?: (req: Request, surface: Surface, auth: TokenInfo | null) => Promise; + /** + * Whether gstack owns the lifecycle of the terminal-agent process and its + * discovery files (`/terminal-port`, `/terminal-internal-token`). + * + * When true (default), shutdown() runs three side effects: + * 1. `pkill -f terminal-agent\.ts` — regex-broad, matches ANY process whose + * command line contains `terminal-agent.ts` on this host (including + * sibling gstack sessions). Pre-existing CLI behavior, not introduced by + * this flag. Identity-based PID kill is a separate followup (see TODOS). + * 2. `safeUnlinkQuiet(/terminal-port)` + * 3. `safeUnlinkQuiet(/terminal-internal-token)` + * + * This is correct for gstack's CLI path, which spawns `terminal-agent.ts` as + * the producer of those files (see cli.ts:1037-1063). + * + * Embedders (gbrowser phoenix overlay, future hosts) that run their own PTY + * server and write those files themselves should pass `false`. When `false`, + * the embedder owns BOTH the agent process AND both discovery files — + * terminal-agent.ts's own SIGTERM cleanup only removes `terminal-port` + * (see terminal-agent.ts:558), so the internal-token file is the embedder's + * full responsibility. + * + * Polarity note: this differs from `xvfb?` and `proxyBridge?`, which gate by + * the *presence* of a caller-owned handle (presence ⇒ don't close). This + * field gates by an explicit boolean because there is no handle object — + * the terminal-agent is started elsewhere (cli.ts), and shutdown's only + * reference is the regex-based pkill + the file paths. + */ + ownsTerminalAgent?: boolean; } /** @@ -1229,8 +1258,11 @@ if (import.meta.main) { /** * Build a request handler set for the browse daemon. Embedders (gbrowser * phoenix overlay) call this directly with their own cfg to compose overlay - * routes via cfg.beforeRoute. The CLI path calls it through start() with - * env-derived defaults — externally-observable behavior is identical. + * routes via cfg.beforeRoute, pass a pre-launched cfg.browserManager, and + * opt out of terminal-agent teardown via cfg.ownsTerminalAgent (default + * true, set to false when the embedder runs its own PTY server). The CLI + * path calls this through start() with env-derived defaults and explicit + * cfg.ownsTerminalAgent: true — externally-observable behavior is identical. * * Auth state lives ENTIRELY inside the factory closure: cfg.authToken is the * single source of truth for the bearer secret, factory-scoped validateAuth @@ -1260,6 +1292,11 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle { initRegistry(cfg.authToken); const { authToken, browserManager: cfgBrowserManager, startTime, beforeRoute, browsePort } = cfg; + // Strict opt-out: only explicit `false` flips the gate. Any other value + // (undefined, truthy non-bool from a JS caller bypassing TS, etc.) defaults + // to gstack-owns. Matches the "default-true preserves CLI bit-for-bit" + // premise even under malformed cfg. + const ownsTerminalAgent = cfg.ownsTerminalAgent === false ? false : true; // Factory-scoped validateAuth. Closes over cfg.authToken so every internal // auth check sees the same token the routes receive. Module-level @@ -1277,14 +1314,16 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle { isShuttingDown = true; console.log('[browse] Shutting down...'); - try { - const { spawnSync } = require('child_process'); - spawnSync('pkill', ['-f', 'terminal-agent\\.ts'], { stdio: 'ignore', timeout: 3000 }); - } catch (err: any) { - console.warn('[browse] Failed to kill terminal-agent:', err.message); + if (ownsTerminalAgent) { + try { + const { spawnSync } = require('child_process'); + spawnSync('pkill', ['-f', 'terminal-agent\\.ts'], { stdio: 'ignore', timeout: 3000 }); + } catch (err: any) { + console.warn('[browse] Failed to kill terminal-agent:', err.message); + } + safeUnlinkQuiet(path.join(path.dirname(config.stateFile), 'terminal-port')); + safeUnlinkQuiet(path.join(path.dirname(config.stateFile), 'terminal-internal-token')); } - try { safeUnlinkQuiet(path.join(path.dirname(config.stateFile), 'terminal-port')); } catch {} - try { safeUnlinkQuiet(path.join(path.dirname(config.stateFile), 'terminal-internal-token')); } catch {} try { detachSession(); } catch (err: any) { console.warn('[browse] Failed to detach CDP session:', err.message); } @@ -2541,6 +2580,7 @@ export async function start() { xvfb, proxyBridge, startTime, + ownsTerminalAgent: true, // CLI spawns terminal-agent.ts itself (see cli.ts:1037-1063) }); const server = Bun.serve({ @@ -2686,6 +2726,23 @@ export async function start() { } } +/** + * Test-only. Resets the module-level shutdown latch so a second test case + * can exercise shutdown() in the same process. Mirrors __resetRegistry in + * token-registry.ts. shutdown() short-circuits when isShuttingDown is true + * (see line near the start of shutdown), so without this, tests that call + * shutdown() more than once silently no-op after the first call. + * + * DO NOT call from production code. Defeats the shutdown re-entry guard, + * which can race process.exit with cfgBrowserManager.close() and the pkill / + * safeUnlinkQuiet side effects. The `__` prefix is the convention; nothing + * enforces it. If you find yourself reaching for this outside a test file, + * the right fix is to make isShuttingDown factory-scoped instead. + */ +export function __resetShuttingDown(): void { + isShuttingDown = false; +} + // Auto-kickoff only when this module is the entry point. Embedders // (gbrowser phoenix overlay) import { start, buildFetchHandler, ... } // without triggering the listener-binding side effects. diff --git a/browse/test/server-embedder-terminal-port.test.ts b/browse/test/server-embedder-terminal-port.test.ts new file mode 100644 index 000000000..722a331d8 --- /dev/null +++ b/browse/test/server-embedder-terminal-port.test.ts @@ -0,0 +1,189 @@ +import { describe, test, expect, beforeEach, beforeAll, afterAll } from 'bun:test'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import { + buildFetchHandler, + __resetShuttingDown, + type ServerConfig, +} from '../src/server'; +import { __resetRegistry } from '../src/token-registry'; +import { BrowserManager } from '../src/browser-manager'; +import { resolveConfig } from '../src/config'; + +// Tests for the v1.41+ ownsTerminalAgent flag. +// +// Embedders (gbrowser phoenix overlay) that run their own PTY server and write +// terminal-port / terminal-internal-token themselves were getting those files +// clobbered by gstack's shutdown(). The flag (default true) gates three side +// effects: pkill -f terminal-agent\.ts, unlink terminal-port, unlink +// terminal-internal-token. False = embedder owns them, gstack stays hands-off. +// +// CRITICAL: each test stubs BOTH process.exit (so shutdown's exit doesn't kill +// the test runner) AND child_process.spawnSync (so pkill doesn't run real +// `pkill -f terminal-agent\.ts` on the developer's machine — would kill any +// sibling gstack sessions). + +const stateDir = resolveConfig().stateDir; +const PORT_FILE = path.join(stateDir, 'terminal-port'); +const TOKEN_FILE = path.join(stateDir, 'terminal-internal-token'); +const SENTINEL_PORT = 'sentinel-port-65432'; +const SENTINEL_TOKEN = 'sentinel-token-abcdef1234567890'; + +function makeMinimalConfig(overrides: Partial = {}): ServerConfig { + const token = 'embedder-test-' + crypto.randomBytes(16).toString('hex'); + return { + authToken: token, + browsePort: 34568, + idleTimeoutMs: 1_800_000, + config: resolveConfig(), + browserManager: new BrowserManager(), + startTime: Date.now(), + ...overrides, + }; +} + +function writeSentinels(): void { + fs.mkdirSync(stateDir, { recursive: true }); + fs.writeFileSync(PORT_FILE, SENTINEL_PORT); + fs.writeFileSync(TOKEN_FILE, SENTINEL_TOKEN); +} + +function readIfExists(p: string): string | null { + try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } +} + +/** + * Stubs process.exit + child_process.spawnSync, runs the callback, and + * restores both regardless of throw. Returns the captured spawnSync argv + * list so callers can assert pkill was or wasn't invoked. The callback + * is expected to swallow the __exit:N throw from shutdown(). + */ +async function withStubs( + cb: (spawnSyncCalls: any[][]) => Promise +): Promise { + const origExit = process.exit; + const childProcess = require('child_process'); + const origSpawnSync = childProcess.spawnSync; + const spawnSyncCalls: any[][] = []; + (process as any).exit = ((code: number) => { + throw new Error(`__exit:${code}`); + }) as any; + childProcess.spawnSync = ((...args: any[]) => { + spawnSyncCalls.push(args); + return { status: 0, stdout: '', stderr: '', signal: null, pid: 0, output: [] }; + }) as any; + try { + await cb(spawnSyncCalls); + } finally { + (process as any).exit = origExit; + childProcess.spawnSync = origSpawnSync; + } + return spawnSyncCalls; +} + +async function runShutdown(handle: { shutdown: (code?: number) => Promise }): Promise { + try { + await handle.shutdown(0); + } catch (err: any) { + if (typeof err?.message !== 'string' || !err.message.startsWith('__exit:')) throw err; + } +} + +function pkillCalls(calls: any[][]): any[][] { + return calls.filter((call) => call[0] === 'pkill'); +} + +describe('buildFetchHandler ownsTerminalAgent gate', () => { + // shutdown() reads `path.dirname(config.stateFile)` from module-level config + // (composition gap — see TODOS T9). So unlinks target the real state dir, + // not a per-test temp dir. If a real gstack daemon is running on this host, + // its terminal-port + terminal-internal-token live where this test writes. + // Save + restore real-daemon file contents around the whole suite so the + // test never clobbers a developer's running session. + let realPortBackup: string | null = null; + let realTokenBackup: string | null = null; + + beforeAll(() => { + realPortBackup = readIfExists(PORT_FILE); + realTokenBackup = readIfExists(TOKEN_FILE); + }); + + afterAll(() => { + if (realPortBackup !== null) { + fs.mkdirSync(stateDir, { recursive: true }); + fs.writeFileSync(PORT_FILE, realPortBackup); + } else { + try { fs.unlinkSync(PORT_FILE); } catch {} + } + if (realTokenBackup !== null) { + fs.mkdirSync(stateDir, { recursive: true }); + fs.writeFileSync(TOKEN_FILE, realTokenBackup); + } else { + try { fs.unlinkSync(TOKEN_FILE); } catch {} + } + }); + + beforeEach(() => { + __resetRegistry(); + __resetShuttingDown(); + // Clean any leftover sentinels from a prior failed run so the "preserved" + // assertion can't pass spuriously off a stale file. + try { fs.unlinkSync(PORT_FILE); } catch {} + try { fs.unlinkSync(TOKEN_FILE); } catch {} + }); + + test('1. ownsTerminalAgent:false preserves both files and skips pkill', async () => { + writeSentinels(); + const handle = buildFetchHandler(makeMinimalConfig({ ownsTerminalAgent: false })); + const calls = await withStubs(async () => { + await runShutdown(handle); + }); + expect(readIfExists(PORT_FILE)).toBe(SENTINEL_PORT); + expect(readIfExists(TOKEN_FILE)).toBe(SENTINEL_TOKEN); + expect(pkillCalls(calls).length).toBe(0); + }); + + test('2. ownsTerminalAgent:true (explicit) deletes both files and invokes pkill exactly once', async () => { + writeSentinels(); + const handle = buildFetchHandler(makeMinimalConfig({ ownsTerminalAgent: true })); + const calls = await withStubs(async () => { + await runShutdown(handle); + }); + expect(readIfExists(PORT_FILE)).toBeNull(); + expect(readIfExists(TOKEN_FILE)).toBeNull(); + const pkills = pkillCalls(calls); + expect(pkills.length).toBe(1); + // argv[1] is the args array passed to spawnSync. + expect(pkills[0][1]).toEqual(['-f', 'terminal-agent\\.ts']); + }); + + test('3. ownsTerminalAgent unset defaults to true (deletes + pkill)', async () => { + writeSentinels(); + // Note: no ownsTerminalAgent in the overrides — uses the `?? true` default. + const handle = buildFetchHandler(makeMinimalConfig()); + const calls = await withStubs(async () => { + await runShutdown(handle); + }); + expect(readIfExists(PORT_FILE)).toBeNull(); + expect(readIfExists(TOKEN_FILE)).toBeNull(); + expect(pkillCalls(calls).length).toBe(1); + }); + + test('4. CLI start() call site passes ownsTerminalAgent: true literally (static grep)', () => { + // Resolves browse/src/server.ts relative to this test file so the test + // works regardless of cwd. import.meta.url is the test file's URL. + const serverTsPath = path.resolve( + new URL(import.meta.url).pathname, + '..', + '..', + 'src', + 'server.ts', + ); + const source = fs.readFileSync(serverTsPath, 'utf-8'); + // Match the call site inside start()'s buildFetchHandler({...}) literal. + // The pattern looks for the trailing comma and trailing context so the + // match cannot be satisfied by the JSDoc reference earlier in the file. + expect(source).toMatch(/ownsTerminalAgent:\s*true,\s*\/\/\s*CLI spawns terminal-agent\.ts/); + }); +}); diff --git a/package.json b/package.json index 9c46f7324..c75857f1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.42.0.0", + "version": "1.42.1.0", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module",