github-router 0.3.176 → 0.3.178

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "github-router browser bridge",
4
4
  "short_name": "gh-router-browser",
5
- "version": "0.3.176",
5
+ "version": "0.3.178",
6
6
  "description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
7
7
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
8
8
  "background": {
@@ -1,6 +1,6 @@
1
- import { B as withNoOutputRetry, F as IMPLEMENT_DEFAULT_MODEL, I as PLAN_DEFAULT_MODEL, L as REVIEW_DEFAULT_MODEL, M as BROWSE_DEFAULT_MODEL, N as DEFAULT_MODEL, P as EXPLORE_DEFAULT_MODEL, R as appendPlanReminder, z as runWorkerAgent } from "./peer-mcp-personas-BKkdfOyK.js";
2
- import "./paths-CTlT1nTo.js";
3
- import "./lifecycle-ChPBRt6K.js";
4
- import "./lifecycle-BUXxiltc.js";
1
+ import { B as withNoOutputRetry, F as IMPLEMENT_DEFAULT_MODEL, I as PLAN_DEFAULT_MODEL, L as REVIEW_DEFAULT_MODEL, M as BROWSE_DEFAULT_MODEL, N as DEFAULT_MODEL, P as EXPLORE_DEFAULT_MODEL, R as appendPlanReminder, z as runWorkerAgent } from "./peer-mcp-personas-B1-1mVPB.js";
2
+ import "./paths-BZPgxKUl.js";
3
+ import "./lifecycle-DrDEjx_c.js";
4
+ import "./lifecycle-Dvr3pGGG.js";
5
5
 
6
6
  export { BROWSE_DEFAULT_MODEL, DEFAULT_MODEL, EXPLORE_DEFAULT_MODEL, IMPLEMENT_DEFAULT_MODEL, PLAN_DEFAULT_MODEL, REVIEW_DEFAULT_MODEL, appendPlanReminder, runWorkerAgent, withNoOutputRetry };
@@ -435,7 +435,7 @@ let grammars = /* @__PURE__ */ new Map();
435
435
  const parsers = /* @__PURE__ */ new Map();
436
436
  let cancelled = false;
437
437
  function handleJob(job) {
438
- if (process.env.GH_ROUTER_TS_WORKER_CRASH === "1") throw new Error("injected worker crash (test)");
438
+ if (process.env.GH_ROUTER_TS_WORKER_CRASH === "1" || job.testCrash) throw new Error("injected worker crash (test)");
439
439
  if (cancelled) return {
440
440
  id: job.id,
441
441
  ok: true,
@@ -1,4 +1,4 @@
1
- import "./paths-CTlT1nTo.js";
2
- import { a as sweepStaleColbertMetaAtBoot, i as sweepLiveChildren, n as isPidAlive, o as trackChild, r as registerColbertExitHandlers, t as getColbertInstanceUuid } from "./lifecycle-ChPBRt6K.js";
1
+ import "./paths-BZPgxKUl.js";
2
+ import { a as sweepStaleColbertMetaAtBoot, i as sweepLiveChildren, n as isPidAlive, o as trackChild, r as registerColbertExitHandlers, t as getColbertInstanceUuid } from "./lifecycle-DrDEjx_c.js";
3
3
 
4
4
  export { getColbertInstanceUuid, isPidAlive, registerColbertExitHandlers, sweepLiveChildren, sweepStaleColbertMetaAtBoot, trackChild };
@@ -1,4 +1,4 @@
1
- import "./paths-CTlT1nTo.js";
2
- import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, o as sweepStaleWorktreesAtBoot, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-BUXxiltc.js";
1
+ import "./paths-BZPgxKUl.js";
2
+ import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, o as sweepStaleWorktreesAtBoot, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-Dvr3pGGG.js";
3
3
 
4
4
  export { WorktreeRegistry, getInstanceUuid, recordWorkerRepo, registerExitHandlers, sweepRegistry, sweepStaleWorktreesAtBoot };
@@ -1,4 +1,4 @@
1
- import { t as PATHS } from "./paths-CTlT1nTo.js";
1
+ import { t as PATHS } from "./paths-BZPgxKUl.js";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import nodePath from "node:path";
@@ -553,4 +553,4 @@ async function sweepStaleColbertMetaAtBoot() {
553
553
 
554
554
  //#endregion
555
555
  export { sweepStaleColbertMetaAtBoot as a, killManagedTree as c, runCommandCapture as d, runCommandVoid as f, sweepLiveChildren as i, parseBoolEnv as l, isPidAlive as n, trackChild as o, runManagedExeCapture as p, registerColbertExitHandlers as r, killChildProcessTree as s, getColbertInstanceUuid as t, resolveExecutable as u };
556
- //# sourceMappingURL=lifecycle-ChPBRt6K.js.map
556
+ //# sourceMappingURL=lifecycle-DrDEjx_c.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle-ChPBRt6K.js","names":["path","quoted: string","child: ReturnType<typeof spawn>","chunks: Array<Buffer>","stderrChunks: Array<Buffer>","inactivityTimer: ReturnType<typeof setTimeout> | undefined","_instanceUuid: string | null","_exitHandler: (() => void) | null","_sigintHandler: (() => void) | null","_sigtermHandler: (() => void) | null","names: Array<string>","path","meta: Record<string, unknown>"],"sources":["../src/lib/exec.ts","../src/lib/colbert/lifecycle.ts"],"sourcesContent":["/**\n * Windows-safe, injection-safe command execution helpers.\n *\n * Why this module exists: on Windows, npm-installed CLIs (`claude`,\n * `npm`, `codex`) are `.cmd`/`.bat` shims. Node's `execFile`/`spawn`\n * with `shell:false` cannot launch them (CreateProcess only resolves\n * `.exe`), so callers must go through `cmd.exe` (`shell:true`). That in\n * turn opens two hazards this module closes:\n *\n * 1. **Metacharacter injection** (`& | < > ^ ( ) ! %`). A naive\n * \"quote only tokens with spaces\" scheme lets `pkg@latest&calc`\n * run `calc` as a second command. `buildExecInvocation` applies\n * real cmd.exe quoting (argv-quote + caret-escape) and fails\n * closed on `%` (which cannot be reliably escaped on the cmd\n * command line).\n *\n * 2. **CWD shadowing.** `cmd.exe` resolves a bare `npm` from the\n * current directory before PATH, so an untrusted repo can plant\n * `npm.cmd`. `resolveExecutable` resolves to an absolute path\n * against PATH only (honoring PATHEXT), never the cwd, so callers\n * spawn the real binary by absolute path.\n *\n * All runners are best-effort and timeout-bounded; callers wrap in\n * try/catch and never let an update/probe failure block launch.\n */\n\nimport { spawn, spawnSync, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\n/**\n * Parse a boolean-ish env value. Returns `undefined` when unset or\n * unrecognized so callers can apply their own default. Accepts\n * `1|true|yes|on` (true) and `0|false|no|off|<empty>` (false),\n * case-insensitive. The single shared parser for all new `GH_ROUTER_*`\n * flags so on/off semantics don't drift per call site.\n */\nexport function parseBoolEnv(value: string | undefined): boolean | undefined {\n if (value === undefined) return undefined\n const v = value.trim().toLowerCase()\n if (v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\") return true\n if (v === \"0\" || v === \"false\" || v === \"no\" || v === \"off\" || v === \"\") {\n return false\n }\n return undefined\n}\n\n/** Read the PATH value from an env object, case-insensitively. */\nfunction pathValueOf(env: NodeJS.ProcessEnv): string {\n for (const key of Object.keys(env)) {\n if (key.toLowerCase() === \"path\") return env[key] ?? \"\"\n }\n return \"\"\n}\n\nexport interface ResolveExecutableOpts {\n env?: NodeJS.ProcessEnv\n platform?: NodeJS.Platform\n /** Directory to treat as \"current dir\" and exclude from resolution. */\n cwd?: string\n}\n\n/**\n * Resolve an executable name to an absolute path against PATH, honoring\n * `PATHEXT` on Windows and **excluding the current working directory**.\n *\n * Returns `null` when unresolved — callers treat that as \"tool absent\"\n * and skip (best-effort). Spawning the returned absolute path means\n * `cmd.exe`'s implicit cwd-first lookup never applies, closing the\n * planted-`npm.cmd` vector.\n */\nexport function resolveExecutable(\n name: string,\n opts: ResolveExecutableOpts = {},\n): string | null {\n const platform = opts.platform ?? process.platform\n const env = opts.env ?? process.env\n // Defensive: some test harnesses mock `node:process` without `cwd`.\n // Absent a usable cwd we simply skip the cwd-exclusion guard (the\n // returned absolute path already bypasses cmd.exe's cwd-first lookup).\n const cwdRaw =\n opts.cwd ?? (typeof process.cwd === \"function\" ? process.cwd() : undefined)\n const resolvedCwd = cwdRaw ? path.resolve(cwdRaw) : null\n\n const dirs = pathValueOf(env)\n .split(path.delimiter)\n // Drop empty entries and a literal \".\" — both denote the cwd on\n // Windows, which we explicitly refuse to resolve against.\n .filter((d) => d.length > 0 && d !== \".\")\n\n const isWin = platform === \"win32\"\n\n // POSIX: an explicit path component → resolve directly.\n if (!isWin && name.includes(\"/\")) {\n return existsSync(name) ? path.resolve(name) : null\n }\n // Windows: an explicit path component (with a separator) → direct.\n if (isWin && (name.includes(\"\\\\\") || name.includes(\"/\"))) {\n return existsSync(name) ? path.resolve(name) : null\n }\n\n const exts =\n isWin && path.extname(name) === \"\"\n ? (env.PATHEXT ?? \".COM;.EXE;.BAT;.CMD\")\n .split(\";\")\n .map((e) => e.trim())\n .filter(Boolean)\n : [\"\"]\n\n for (const dir of dirs) {\n // Belt-and-suspenders: never resolve against the cwd even if it is\n // listed explicitly in PATH.\n if (resolvedCwd && path.resolve(dir) === resolvedCwd) continue\n for (const ext of exts) {\n const candidate = path.join(dir, name + ext)\n if (existsSync(candidate)) return candidate\n }\n }\n return null\n}\n\n/**\n * Quote one argument for a `cmd.exe /c \"<line>\"` command line so the\n * target program receives it verbatim and no `cmd.exe` metacharacter\n * retains shell meaning.\n *\n * Two phases (the canonical Windows approach — Colascione / Rust std):\n * 1. **argv quoting** so `CommandLineToArgvW` in the target parses\n * the token as one argument (double-quote, backslash-escape).\n * 2. **caret-escaping** every `cmd.exe` metacharacter — including the\n * quotes from phase 1 — so `cmd.exe` is never in quote-mode, strips\n * the carets, and hands the argv-quoted string to the program.\n *\n * `%` is special: it cannot be reliably escaped on the `cmd.exe`\n * *command line* (caret does not stop `%VAR%` expansion there). Rather\n * than mis-escape, we **throw** — our callers never pass `%`, so this\n * fails closed on the one unescapable injection vector.\n */\nexport function quoteWinArg(arg: string): string {\n if (arg.includes(\"%\")) {\n throw new Error(\n \"buildExecInvocation: argument contains '%', which cannot be safely \" +\n \"escaped on the Windows command line; refusing to build the command.\",\n )\n }\n\n // Phase 1 — argv quoting.\n let quoted: string\n if (arg.length > 0 && !/[ \\t\\n\\v\"&|<>()^!]/.test(arg)) {\n quoted = arg\n } else {\n let s = '\"'\n let backslashes = 0\n for (const ch of arg) {\n if (ch === \"\\\\\") {\n backslashes++\n } else if (ch === '\"') {\n s += \"\\\\\".repeat(backslashes * 2 + 1) + '\"'\n backslashes = 0\n } else {\n s += \"\\\\\".repeat(backslashes) + ch\n backslashes = 0\n }\n }\n s += \"\\\\\".repeat(backslashes * 2) + '\"'\n quoted = s\n }\n\n // Phase 2 — caret-escape all cmd.exe metacharacters (and the carets\n // themselves). cmd strips these; the program sees the phase-1 string.\n return quoted.replace(/[()!^\"<>&|]/g, \"^$&\")\n}\n\nexport interface ExecInvocation {\n command: string\n args: string[]\n shell: boolean\n}\n\n/**\n * Build the platform-correct `spawn` invocation for a command given as\n * an argv array. Pure / unit-testable (no spawn).\n *\n * - win32 → a single caret/argv-quoted command string + `shell:true`\n * + empty args array (the empty array avoids the DEP0190 warning\n * that fires when args and `shell:true` are combined). `cmd[0]`\n * should already be an absolute path from `resolveExecutable`.\n * - posix → `(cmd[0], cmd.slice(1))` with `shell:false` — no shell,\n * no injection surface.\n */\nexport function buildExecInvocation(\n cmd: string[],\n platform: NodeJS.Platform = process.platform,\n): ExecInvocation {\n if (cmd.length === 0) throw new Error(\"buildExecInvocation: empty command\")\n if (platform === \"win32\") {\n return { command: cmd.map(quoteWinArg).join(\" \"), args: [], shell: true }\n }\n return { command: cmd[0], args: cmd.slice(1), shell: false }\n}\n\nexport interface RunOpts {\n cwd?: string\n /** Hard timeout in ms; the process tree is killed on expiry. */\n timeoutMs?: number\n /** Extra env to merge over the parent env for the child. */\n env?: NodeJS.ProcessEnv\n}\n\nexport interface RunResult {\n stdout: string\n stderr: string\n /** Exit code; `null` when killed by signal/timeout. */\n code: number | null\n timedOut: boolean\n}\n\nfunction runInternal(\n cmd: string[],\n stdoutMode: \"pipe\" | \"inherit\" | \"ignore\",\n opts: RunOpts,\n): Promise<RunResult> {\n const { command, args, shell } = buildExecInvocation(cmd)\n return new Promise<RunResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, args, {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell,\n windowsHide: true,\n stdio: [\n \"ignore\",\n stdoutMode,\n stdoutMode === \"inherit\" ? \"inherit\" : \"pipe\",\n ],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n let stdout = \"\"\n let stderr = \"\"\n let timedOut = false\n let settled = false\n\n const timer = opts.timeoutMs\n ? setTimeout(() => {\n timedOut = true\n killTree(child.pid)\n }, opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n stdout += c.toString(\"utf8\")\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n stderr += c.toString(\"utf8\")\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n resolve({ stdout, stderr, code, timedOut })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/** Kill a process tree best-effort (taskkill /T on Windows). */\nfunction killTree(pid: number | undefined): void {\n if (!pid) return\n try {\n if (process.platform === \"win32\") {\n spawn(\"taskkill\", [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n })\n } else {\n process.kill(pid, \"SIGTERM\")\n }\n } catch {\n // already gone\n }\n}\n\n/** Run a command and capture stdout/stderr. Rejects on spawn error. */\nexport function runCommandCapture(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command discarding output (still captures stderr for errors). */\nexport function runCommandVoid(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command with the child's stdout/stderr inherited to the user. */\nexport function runCommandInherit(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"inherit\", opts)\n}\n\nexport interface ManagedExeOpts extends RunOpts {\n /**\n * Hard cap on captured stdout bytes. On overflow the child is\n * tree-killed and the result carries `stdoutTruncated: true`. Defends\n * against a full-`CodeUnit` colgrep `--json` payload (source + 5\n * analysis layers per hit) bloating memory.\n */\n maxStdoutBytes?: number\n /**\n * When true, exceeding `maxStdoutBytes` does NOT tree-kill the child — it\n * sets `stdoutTruncated` and stops BUFFERING further stdout while still\n * draining the pipe (no backpressure). Use for a child whose kill is unsafe\n * (e.g. colgrep, which writes a non-atomic index — a byte-cap kill during\n * its result output could interrupt a write). The child runs to completion\n * (bounded by `timeoutMs` / `inactivityTimeoutMs`). Default false (kill).\n */\n truncateInsteadOfKill?: boolean\n /**\n * Called synchronously with the spawned child right after spawn\n * succeeds, BEFORE any output arrives. The colbert lifecycle ledger\n * uses this to register the child so a session-exit sweep can\n * tree-kill an orphan. Never throws into the runner.\n */\n onSpawn?: (child: ReturnType<typeof spawn>) => void\n /**\n * Inactivity (stall) watchdog. When set, a timer is armed for\n * `inactivityTimeoutMs` and RESET on every stdout/stderr data chunk. On\n * expiry (no output for the window) it consults `onInactivityCheck`: a\n * `true` return means \"still making progress via an out-of-band signal\"\n * (e.g. the colgrep index dir is still growing on disk even though the\n * process is silent on a non-TTY pipe) and the watchdog re-arms; a\n * `false`/absent return tree-kills the child and sets `stalled: true`.\n * This is the progress-based \"stuck\" detector that lets a long-but-\n * progressing build run to completion while still killing a hung one —\n * independent of the coarse total `timeoutMs` backstop.\n */\n inactivityTimeoutMs?: number\n /** External progress probe consulted when the inactivity timer fires.\n * Return `true` to re-arm (still progressing), `false`/throw to kill.\n * MUST be cheap + synchronous (called on a timer, not awaited). */\n onInactivityCheck?: () => boolean\n}\n\nexport interface ManagedExeResult extends RunResult {\n /** True iff stdout was truncated at `maxStdoutBytes` (child was killed). */\n stdoutTruncated: boolean\n /** True iff the inactivity watchdog killed the child (no progress). */\n stalled: boolean\n}\n\n/**\n * Run a **native executable** (a real `.exe`/Mach-O/ELF, NOT a `.cmd`\n * shim) capturing stdout/stderr, with `shell:false` on EVERY platform.\n *\n * Why a separate runner from `runCommandCapture`: that path routes\n * through `buildExecInvocation`, which on Windows builds a\n * `cmd.exe`-quoted command string and **throws on `%`** (`quoteWinArg`).\n * A workspace path can legally contain `%` (and `&`, `(`, `)`, `!`, …),\n * so the managed colgrep binary — which IS a native `.exe`, not a shim —\n * must bypass cmd.exe entirely. `spawn(absExe, args, {shell:false})`\n * resolves the `.exe` via CreateProcess directly: no cmd.exe, no\n * metacharacter hazard, no `%` refusal. POSIX was already `shell:false`.\n * This is what makes \"ANY absolute workspace\" hold on Windows.\n *\n * Lifecycle:\n * - `timeoutMs` → tree-kill on expiry (`taskkill /T /F` on Windows,\n * POSIX process-group `kill(-pgid)` so colgrep's rayon worker\n * children die too). `timedOut: true` in the result.\n * - `maxStdoutBytes` → tree-kill + `stdoutTruncated: true` once the\n * captured stdout exceeds the cap.\n * - `onSpawn(child)` → register the child with the caller's ledger.\n *\n * `command` MUST be an absolute path to the executable (the caller\n * resolves it; we never search PATH here — there is nothing to inject).\n */\nexport function runManagedExeCapture(\n command: string,\n args: ReadonlyArray<string>,\n opts: ManagedExeOpts = {},\n): Promise<ManagedExeResult> {\n const isWin = process.platform === \"win32\"\n return new Promise<ManagedExeResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, [...args], {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell: false,\n windowsHide: true,\n // POSIX: new process group so we can kill the whole tree\n // (colgrep + rayon workers) with kill(-pgid). Windows uses\n // taskkill /T instead; `detached` there has no group semantics.\n detached: !isWin,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n try {\n opts.onSpawn?.(child)\n } catch {\n // ledger registration must never break the spawn\n }\n\n const chunks: Array<Buffer> = []\n let stdoutBytes = 0\n const stderrChunks: Array<Buffer> = []\n let stderrBytes = 0\n const STDERR_CAP = 64 * 1024\n let timedOut = false\n let stdoutTruncated = false\n let stalled = false\n let settled = false\n\n // Exactly one terminator wins: the first to fire records its reason and\n // tree-kills; later ones no-op. Keeps timedOut / stalled / stdoutTruncated\n // mutually exclusive and avoids a double tree-kill.\n let terminated = false\n const terminate = (reason: \"timeout\" | \"stall\" | \"truncate\"): void => {\n if (terminated) return\n terminated = true\n if (reason === \"timeout\") timedOut = true\n else if (reason === \"stall\") stalled = true\n else stdoutTruncated = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n killManagedTree(child, isWin)\n }\n\n const timer = opts.timeoutMs\n ? setTimeout(() => terminate(\"timeout\"), opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n // Inactivity (stall) watchdog. Re-arms itself: when the window elapses\n // with no output, consult the external progress probe — re-arm if it\n // says \"still progressing\" (e.g. the index dir grew), else kill + mark\n // stalled. Reset on every data chunk (a chatty process never stalls).\n let inactivityTimer: ReturnType<typeof setTimeout> | undefined\n const armInactivity = (): void => {\n if (opts.inactivityTimeoutMs === undefined || settled || terminated) return\n inactivityTimer = setTimeout(() => {\n if (settled) return\n // No probe → pure output-inactivity kill. A probe that THROWS is\n // inconclusive → don't kill (re-arm); the absolute timeoutMs backstop\n // still catches a genuinely wedged process.\n let progressing = false\n if (opts.onInactivityCheck) {\n try {\n progressing = opts.onInactivityCheck() === true\n } catch {\n progressing = true\n }\n }\n if (progressing) {\n armInactivity()\n return\n }\n terminate(\"stall\")\n }, opts.inactivityTimeoutMs)\n inactivityTimer?.unref?.()\n }\n const resetInactivity = (): void => {\n if (inactivityTimer) clearTimeout(inactivityTimer)\n armInactivity()\n }\n armInactivity()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n if (stdoutTruncated) return\n stdoutBytes += c.length\n if (\n opts.maxStdoutBytes !== undefined &&\n stdoutBytes > opts.maxStdoutBytes\n ) {\n stdoutTruncated = true\n // Default: tree-kill on overflow. Opt-in: keep the child alive and\n // just stop buffering (the data handler keeps firing + discarding via\n // the `if (stdoutTruncated) return` guard above, so the pipe drains\n // and the child never blocks on a full buffer).\n if (!opts.truncateInsteadOfKill) terminate(\"truncate\")\n return\n }\n chunks.push(c)\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n // Hard byte cap on stderr — append only the slice that fits so a\n // single huge chunk can't overshoot. Never logged raw (it can\n // embed source code from colgrep).\n if (stderrBytes >= STDERR_CAP) return\n const remaining = STDERR_CAP - stderrBytes\n const slice = c.length > remaining ? c.subarray(0, remaining) : c\n stderrChunks.push(slice)\n stderrBytes += slice.length\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n resolve({\n stdout: Buffer.concat(chunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n code,\n timedOut,\n stdoutTruncated,\n stalled,\n })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/**\n * Tree-kill a managed child. Windows: `taskkill /T /F /PID` (whole\n * tree). POSIX: kill the process GROUP (`-pgid`) so colgrep's rayon\n * worker children die with the parent.\n *\n * `runManagedExeCapture` always spawns POSIX children `detached:true`\n * (their own process group), so the group kill is the correct and\n * sufficient primitive. We deliberately do NOT fall back to a positive-\n * pid `process.kill(pid)` when the group kill fails: by the time a kill\n * fires (timeout / byte-cap race), the child may have already exited and\n * its PID been recycled by an unrelated process — a positive-pid kill\n * would then target the wrong process. `ESRCH` (group already gone) is\n * the success case for our purposes; any other error is swallowed.\n */\nexport function killManagedTree(\n child: ReturnType<typeof spawn>,\n isWin: boolean = process.platform === \"win32\",\n): void {\n const pid = child.pid\n if (!pid) return\n try {\n if (isWin) {\n spawn(\"taskkill\", [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n })\n } else {\n // Negative pid → the process group (we spawned detached). No\n // positive-pid fallback (PID-reuse hazard — see doc comment).\n process.kill(-pid, \"SIGKILL\")\n }\n } catch {\n // ESRCH (already gone) / EPERM — best-effort, nothing more to do.\n }\n}\n\n/**\n * Absolute path to Windows `taskkill.exe`. Bare `\"taskkill\"` would let\n * `CreateProcess`'s search order (which can include the cwd) run a planted\n * `taskkill.exe`; pinning the System32 path closes that hijack.\n */\nfunction taskkillExe(): string {\n const root =\n process.env.SystemRoot || process.env.windir || \"C:\\\\Windows\"\n return path.join(root, \"System32\", \"taskkill.exe\")\n}\n\n/**\n * Tree-kill a child process on a SHUTDOWN/exit path, deterministically.\n *\n * Distinct from `killManagedTree` in two ways that matter when the proxy\n * is about to call `process.exit`:\n *\n * 1. **Windows uses `spawnSync`, not async `spawn`.** An async taskkill\n * may never issue before `process.exit` tears the event loop down;\n * the synchronous form guarantees the kill command has run. It is\n * time-bounded so a wedged taskkill can't hang shutdown.\n * 2. **POSIX honors `detachedGroup`.** The launched CLI is spawned\n * `detached:true` (its own process group) so `kill(-pid)` reaps the\n * whole group; other callers (e.g. ripgrep) are NOT detached, so\n * `child.kill()` on the positive pid is correct. A negative-pid kill\n * on a non-detached child would throw or hit the wrong target.\n *\n * Both branches escalate SIGTERM → SIGKILL after `graceMs` so a CLI that\n * traps SIGTERM still dies. The escalation timer is `.unref()`'d so it\n * never holds the event loop open. Errors are swallowed (already-gone /\n * EBUSY / ESRCH are the success case for our purposes).\n */\nexport function killChildProcessTree(\n child: ChildProcess,\n opts: { detachedGroup: boolean; graceMs?: number } = { detachedGroup: false },\n): void {\n const pid = child.pid\n if (!pid) return\n const graceMs = opts.graceMs ?? 500\n\n if (process.platform === \"win32\") {\n // /T = whole tree, /F = force. taskkill walks the live parent-PID\n // chain, so it reaps grandchildren regardless of the cmd.exe-vs-direct\n // spawn shape. Sync (completes before an imminent process.exit) and\n // time-bounded (a wedged taskkill must not hang shutdown).\n try {\n spawnSync(taskkillExe(), [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n timeout: 5000,\n })\n } catch {\n // Already gone / EBUSY race — best-effort.\n }\n return\n }\n\n // POSIX: signal the group (detached) or the lone child (non-detached).\n const target = opts.detachedGroup ? -pid : pid\n try {\n process.kill(target, \"SIGTERM\")\n } catch {\n // ESRCH — already gone.\n }\n const t = setTimeout(() => {\n // For a lone (non-detached) child, skip escalation once it has exited\n // — its positive PID could have been recycled. For a process GROUP the\n // leader's exit does NOT mean the group is empty (a SIGTERM-ignoring\n // grandchild may survive), and the group id is only reused once the\n // group is fully gone, so always attempt the group SIGKILL (ESRCH when\n // already empty is the success case).\n if (\n !opts.detachedGroup &&\n (child.exitCode !== null || child.signalCode !== null)\n ) {\n return\n }\n try {\n process.kill(target, \"SIGKILL\")\n } catch {\n // Already dead.\n }\n }, graceMs)\n t.unref?.()\n}\n","/**\n * ColBERT sidecar lifecycle: in-memory PID ledger for the short-lived\n * `colgrep` children this proxy spawns, signal-handler tree-kill on\n * exit, and a boot-time metadata reclassification sweep.\n *\n * Because colgrep is CLI-per-invocation (no daemon), the lifecycle\n * problem is **process tracking + cancellation + boot/exit sweep**, NOT\n * keep-alive. Modeled on `worker-agent/lifecycle.ts` (PID ledger + boot\n * sweep + per-proxy-run instance UUID) and `exec.ts`'s tree-kill.\n *\n * Three cooperating layers (none sufficient alone):\n * 1. Per-call cleanup — the runner's `finally` force-kills the child\n * it spawned (handled in runner.ts).\n * 2. Session-end signal sweep (this file) — SIGINT/SIGTERM/exit kill\n * every still-tracked child of THIS run.\n * 3. Boot-time sweep (`sweepStaleColbertMetaAtBoot`) — reclassifies\n * `.gh-router-meta/*.json` entries whose `buildPid` is dead from\n * `building` → `failed`. It NEVER issues a kill to a PID from a\n * prior boot (a reused PID may belong to an unrelated process);\n * only the in-memory ledger (this run's spawns) is ever killed.\n */\n\nimport { spawn } from \"node:child_process\"\nimport { randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { killManagedTree } from \"../exec\"\nimport { PATHS } from \"../paths\"\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID (mirrors worker-agent/lifecycle.ts)\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Written into the\n * sidecar metadata `ownerInstanceId` so the boot sweep can tell \"this\n * proxy's still-live build\" from \"a stranded `building` entry from a\n * prior proxy whose PID got recycled\" (Docker PID-1 across restarts).\n */\nexport function getColbertInstanceUuid(): string {\n if (_instanceUuid === null) _instanceUuid = randomUUID()\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetColbertInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// In-memory PID ledger of THIS run's live colgrep children\n// ---------------------------------------------------------------------\n\ntype TrackedChild = ReturnType<typeof spawn>\n\nconst _liveChildren = new Set<TrackedChild>()\n\n/**\n * Register a freshly-spawned colgrep child so the exit sweep can reap\n * it. The runner also removes it on natural close via `untrackChild`.\n */\nexport function trackChild(child: TrackedChild): void {\n _liveChildren.add(child)\n child.once(\"close\", () => _liveChildren.delete(child))\n child.once(\"error\", () => _liveChildren.delete(child))\n}\n\n/** Remove a child from the ledger (e.g. after a clean per-call kill). */\nexport function untrackChild(child: TrackedChild): void {\n _liveChildren.delete(child)\n}\n\n/** Count of live tracked children (test/diagnostic). */\nexport function liveChildCount(): number {\n return _liveChildren.size\n}\n\n/**\n * Synchronous best-effort tree-kill of every tracked child. Called from\n * the signal/exit handlers. After killing, the set is cleared so a\n * second call is a no-op.\n */\nexport function sweepLiveChildren(): void {\n const isWin = process.platform === \"win32\"\n for (const child of _liveChildren) {\n try {\n killManagedTree(child, isWin)\n } catch {\n // already gone\n }\n }\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers (mirror worker-agent/lifecycle.ts re-raise pattern)\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Wire SIGINT/SIGTERM/exit handlers that tree-kill every tracked\n * colgrep child. Idempotent — subsequent calls are a no-op (we never\n * leak listeners). The signal handlers re-raise after sweeping so Node's\n * default terminate-on-signal behavior is restored (otherwise attaching\n * a listener cancels the default and Ctrl-C would clean but not exit).\n */\nexport function registerColbertExitHandlers(): void {\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepLiveChildren()\n _sigintHandler = () => {\n sweepLiveChildren()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepLiveChildren()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n process.on(\"exit\", _exitHandler)\n}\n\n/** Test-only: unregister handlers + reset module state. */\nexport function __unregisterColbertExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Boot-time metadata reclassification sweep\n// ---------------------------------------------------------------------\n\n/**\n * True iff `pid` names a live process. `process.kill(pid, 0)` probes\n * existence without signalling; `EPERM` means the process exists but is\n * owned by another user (still alive). Exported so the per-query freshness\n * verdict can mirror the boot sweep's liveness check.\n */\nexport function isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. Walks `.gh-router-meta/*.json`; any entry stuck in\n * `status:\"building\"` whose `buildPid` is DEAD is a crashed-build\n * escapee → reset to `status:\"failed\"` so the next search re-kicks a\n * build instead of routing to a never-finishing one.\n *\n * It NEVER kills anything: a live PID matching a stale `buildPid` from a\n * prior boot may be a recycled PID belonging to an unrelated process, so\n * the boot sweep only RECLASSIFIES metadata. The in-memory ledger (this\n * run's spawns) is the only thing the SIGINT/SIGTERM handler ever kills.\n *\n * Best-effort; never throws (wrapped by the caller in `ensurePaths`).\n */\nexport async function sweepStaleColbertMetaAtBoot(): Promise<void> {\n const metaDir = PATHS.COLBERT_META_DIR\n let names: Array<string>\n try {\n names = await fs.readdir(metaDir)\n } catch {\n return // no meta dir yet — nothing to sweep\n }\n for (const name of names) {\n if (!name.endsWith(\".json\")) continue\n const file = path.join(metaDir, name)\n let meta: Record<string, unknown>\n try {\n meta = JSON.parse(await fs.readFile(file, \"utf8\")) as Record<string, unknown>\n } catch {\n continue // corrupt — leave it; index-store re-derives on next access\n }\n if (meta.status !== \"building\") continue\n const buildPid = typeof meta.buildPid === \"number\" ? meta.buildPid : 0\n if (buildPid > 0 && isPidAlive(buildPid)) {\n // A live PID — could be ours (this run re-kicked) or a recycled\n // unrelated PID. Either way: never kill from the boot sweep. Leave\n // the entry; the runner's own ownership check governs.\n continue\n }\n // Dead build PID → reclassify to failed (atomic temp+rename). Stamp the\n // crash class so the per-query self-heal treats it as transient (re-kick)\n // rather than operator-actionable.\n meta.status = \"failed\"\n meta.failureClass = \"crashed\"\n const tmp = `${file}.${process.pid}.tmp`\n try {\n await fs.writeFile(tmp, JSON.stringify(meta, null, 2))\n await fs.rename(tmp, file)\n } catch {\n await fs.rm(tmp, { force: true }).catch(() => {})\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle-DrDEjx_c.js","names":["path","quoted: string","child: ReturnType<typeof spawn>","chunks: Array<Buffer>","stderrChunks: Array<Buffer>","inactivityTimer: ReturnType<typeof setTimeout> | undefined","_instanceUuid: string | null","_exitHandler: (() => void) | null","_sigintHandler: (() => void) | null","_sigtermHandler: (() => void) | null","names: Array<string>","path","meta: Record<string, unknown>"],"sources":["../src/lib/exec.ts","../src/lib/colbert/lifecycle.ts"],"sourcesContent":["/**\n * Windows-safe, injection-safe command execution helpers.\n *\n * Why this module exists: on Windows, npm-installed CLIs (`claude`,\n * `npm`, `codex`) are `.cmd`/`.bat` shims. Node's `execFile`/`spawn`\n * with `shell:false` cannot launch them (CreateProcess only resolves\n * `.exe`), so callers must go through `cmd.exe` (`shell:true`). That in\n * turn opens two hazards this module closes:\n *\n * 1. **Metacharacter injection** (`& | < > ^ ( ) ! %`). A naive\n * \"quote only tokens with spaces\" scheme lets `pkg@latest&calc`\n * run `calc` as a second command. `buildExecInvocation` applies\n * real cmd.exe quoting (argv-quote + caret-escape) and fails\n * closed on `%` (which cannot be reliably escaped on the cmd\n * command line).\n *\n * 2. **CWD shadowing.** `cmd.exe` resolves a bare `npm` from the\n * current directory before PATH, so an untrusted repo can plant\n * `npm.cmd`. `resolveExecutable` resolves to an absolute path\n * against PATH only (honoring PATHEXT), never the cwd, so callers\n * spawn the real binary by absolute path.\n *\n * All runners are best-effort and timeout-bounded; callers wrap in\n * try/catch and never let an update/probe failure block launch.\n */\n\nimport { spawn, spawnSync, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\n/**\n * Parse a boolean-ish env value. Returns `undefined` when unset or\n * unrecognized so callers can apply their own default. Accepts\n * `1|true|yes|on` (true) and `0|false|no|off|<empty>` (false),\n * case-insensitive. The single shared parser for all new `GH_ROUTER_*`\n * flags so on/off semantics don't drift per call site.\n */\nexport function parseBoolEnv(value: string | undefined): boolean | undefined {\n if (value === undefined) return undefined\n const v = value.trim().toLowerCase()\n if (v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\") return true\n if (v === \"0\" || v === \"false\" || v === \"no\" || v === \"off\" || v === \"\") {\n return false\n }\n return undefined\n}\n\n/** Read the PATH value from an env object, case-insensitively. */\nfunction pathValueOf(env: NodeJS.ProcessEnv): string {\n for (const key of Object.keys(env)) {\n if (key.toLowerCase() === \"path\") return env[key] ?? \"\"\n }\n return \"\"\n}\n\nexport interface ResolveExecutableOpts {\n env?: NodeJS.ProcessEnv\n platform?: NodeJS.Platform\n /** Directory to treat as \"current dir\" and exclude from resolution. */\n cwd?: string\n}\n\n/**\n * Resolve an executable name to an absolute path against PATH, honoring\n * `PATHEXT` on Windows and **excluding the current working directory**.\n *\n * Returns `null` when unresolved — callers treat that as \"tool absent\"\n * and skip (best-effort). Spawning the returned absolute path means\n * `cmd.exe`'s implicit cwd-first lookup never applies, closing the\n * planted-`npm.cmd` vector.\n */\nexport function resolveExecutable(\n name: string,\n opts: ResolveExecutableOpts = {},\n): string | null {\n const platform = opts.platform ?? process.platform\n const env = opts.env ?? process.env\n // Defensive: some test harnesses mock `node:process` without `cwd`.\n // Absent a usable cwd we simply skip the cwd-exclusion guard (the\n // returned absolute path already bypasses cmd.exe's cwd-first lookup).\n const cwdRaw =\n opts.cwd ?? (typeof process.cwd === \"function\" ? process.cwd() : undefined)\n const resolvedCwd = cwdRaw ? path.resolve(cwdRaw) : null\n\n const dirs = pathValueOf(env)\n .split(path.delimiter)\n // Drop empty entries and a literal \".\" — both denote the cwd on\n // Windows, which we explicitly refuse to resolve against.\n .filter((d) => d.length > 0 && d !== \".\")\n\n const isWin = platform === \"win32\"\n\n // POSIX: an explicit path component → resolve directly.\n if (!isWin && name.includes(\"/\")) {\n return existsSync(name) ? path.resolve(name) : null\n }\n // Windows: an explicit path component (with a separator) → direct.\n if (isWin && (name.includes(\"\\\\\") || name.includes(\"/\"))) {\n return existsSync(name) ? path.resolve(name) : null\n }\n\n const exts =\n isWin && path.extname(name) === \"\"\n ? (env.PATHEXT ?? \".COM;.EXE;.BAT;.CMD\")\n .split(\";\")\n .map((e) => e.trim())\n .filter(Boolean)\n : [\"\"]\n\n for (const dir of dirs) {\n // Belt-and-suspenders: never resolve against the cwd even if it is\n // listed explicitly in PATH.\n if (resolvedCwd && path.resolve(dir) === resolvedCwd) continue\n for (const ext of exts) {\n const candidate = path.join(dir, name + ext)\n if (existsSync(candidate)) return candidate\n }\n }\n return null\n}\n\n/**\n * Quote one argument for a `cmd.exe /c \"<line>\"` command line so the\n * target program receives it verbatim and no `cmd.exe` metacharacter\n * retains shell meaning.\n *\n * Two phases (the canonical Windows approach — Colascione / Rust std):\n * 1. **argv quoting** so `CommandLineToArgvW` in the target parses\n * the token as one argument (double-quote, backslash-escape).\n * 2. **caret-escaping** every `cmd.exe` metacharacter — including the\n * quotes from phase 1 — so `cmd.exe` is never in quote-mode, strips\n * the carets, and hands the argv-quoted string to the program.\n *\n * `%` is special: it cannot be reliably escaped on the `cmd.exe`\n * *command line* (caret does not stop `%VAR%` expansion there). Rather\n * than mis-escape, we **throw** — our callers never pass `%`, so this\n * fails closed on the one unescapable injection vector.\n */\nexport function quoteWinArg(arg: string): string {\n if (arg.includes(\"%\")) {\n throw new Error(\n \"buildExecInvocation: argument contains '%', which cannot be safely \" +\n \"escaped on the Windows command line; refusing to build the command.\",\n )\n }\n\n // Phase 1 — argv quoting.\n let quoted: string\n if (arg.length > 0 && !/[ \\t\\n\\v\"&|<>()^!]/.test(arg)) {\n quoted = arg\n } else {\n let s = '\"'\n let backslashes = 0\n for (const ch of arg) {\n if (ch === \"\\\\\") {\n backslashes++\n } else if (ch === '\"') {\n s += \"\\\\\".repeat(backslashes * 2 + 1) + '\"'\n backslashes = 0\n } else {\n s += \"\\\\\".repeat(backslashes) + ch\n backslashes = 0\n }\n }\n s += \"\\\\\".repeat(backslashes * 2) + '\"'\n quoted = s\n }\n\n // Phase 2 — caret-escape all cmd.exe metacharacters (and the carets\n // themselves). cmd strips these; the program sees the phase-1 string.\n return quoted.replace(/[()!^\"<>&|]/g, \"^$&\")\n}\n\nexport interface ExecInvocation {\n command: string\n args: string[]\n shell: boolean\n}\n\n/**\n * Build the platform-correct `spawn` invocation for a command given as\n * an argv array. Pure / unit-testable (no spawn).\n *\n * - win32 → a single caret/argv-quoted command string + `shell:true`\n * + empty args array (the empty array avoids the DEP0190 warning\n * that fires when args and `shell:true` are combined). `cmd[0]`\n * should already be an absolute path from `resolveExecutable`.\n * - posix → `(cmd[0], cmd.slice(1))` with `shell:false` — no shell,\n * no injection surface.\n */\nexport function buildExecInvocation(\n cmd: string[],\n platform: NodeJS.Platform = process.platform,\n): ExecInvocation {\n if (cmd.length === 0) throw new Error(\"buildExecInvocation: empty command\")\n if (platform === \"win32\") {\n return { command: cmd.map(quoteWinArg).join(\" \"), args: [], shell: true }\n }\n return { command: cmd[0], args: cmd.slice(1), shell: false }\n}\n\nexport interface RunOpts {\n cwd?: string\n /** Hard timeout in ms; the process tree is killed on expiry. */\n timeoutMs?: number\n /** Extra env to merge over the parent env for the child. */\n env?: NodeJS.ProcessEnv\n}\n\nexport interface RunResult {\n stdout: string\n stderr: string\n /** Exit code; `null` when killed by signal/timeout. */\n code: number | null\n timedOut: boolean\n}\n\nfunction runInternal(\n cmd: string[],\n stdoutMode: \"pipe\" | \"inherit\" | \"ignore\",\n opts: RunOpts,\n): Promise<RunResult> {\n const { command, args, shell } = buildExecInvocation(cmd)\n return new Promise<RunResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, args, {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell,\n windowsHide: true,\n stdio: [\n \"ignore\",\n stdoutMode,\n stdoutMode === \"inherit\" ? \"inherit\" : \"pipe\",\n ],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n let stdout = \"\"\n let stderr = \"\"\n let timedOut = false\n let settled = false\n\n const timer = opts.timeoutMs\n ? setTimeout(() => {\n timedOut = true\n killTree(child.pid)\n }, opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n stdout += c.toString(\"utf8\")\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n stderr += c.toString(\"utf8\")\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n resolve({ stdout, stderr, code, timedOut })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/** Kill a process tree best-effort (taskkill /T on Windows). */\nfunction killTree(pid: number | undefined): void {\n if (!pid) return\n try {\n if (process.platform === \"win32\") {\n spawn(\"taskkill\", [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n })\n } else {\n process.kill(pid, \"SIGTERM\")\n }\n } catch {\n // already gone\n }\n}\n\n/** Run a command and capture stdout/stderr. Rejects on spawn error. */\nexport function runCommandCapture(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command discarding output (still captures stderr for errors). */\nexport function runCommandVoid(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command with the child's stdout/stderr inherited to the user. */\nexport function runCommandInherit(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"inherit\", opts)\n}\n\nexport interface ManagedExeOpts extends RunOpts {\n /**\n * Hard cap on captured stdout bytes. On overflow the child is\n * tree-killed and the result carries `stdoutTruncated: true`. Defends\n * against a full-`CodeUnit` colgrep `--json` payload (source + 5\n * analysis layers per hit) bloating memory.\n */\n maxStdoutBytes?: number\n /**\n * When true, exceeding `maxStdoutBytes` does NOT tree-kill the child — it\n * sets `stdoutTruncated` and stops BUFFERING further stdout while still\n * draining the pipe (no backpressure). Use for a child whose kill is unsafe\n * (e.g. colgrep, which writes a non-atomic index — a byte-cap kill during\n * its result output could interrupt a write). The child runs to completion\n * (bounded by `timeoutMs` / `inactivityTimeoutMs`). Default false (kill).\n */\n truncateInsteadOfKill?: boolean\n /**\n * Called synchronously with the spawned child right after spawn\n * succeeds, BEFORE any output arrives. The colbert lifecycle ledger\n * uses this to register the child so a session-exit sweep can\n * tree-kill an orphan. Never throws into the runner.\n */\n onSpawn?: (child: ReturnType<typeof spawn>) => void\n /**\n * Inactivity (stall) watchdog. When set, a timer is armed for\n * `inactivityTimeoutMs` and RESET on every stdout/stderr data chunk. On\n * expiry (no output for the window) it consults `onInactivityCheck`: a\n * `true` return means \"still making progress via an out-of-band signal\"\n * (e.g. the colgrep index dir is still growing on disk even though the\n * process is silent on a non-TTY pipe) and the watchdog re-arms; a\n * `false`/absent return tree-kills the child and sets `stalled: true`.\n * This is the progress-based \"stuck\" detector that lets a long-but-\n * progressing build run to completion while still killing a hung one —\n * independent of the coarse total `timeoutMs` backstop.\n */\n inactivityTimeoutMs?: number\n /** External progress probe consulted when the inactivity timer fires.\n * Return `true` to re-arm (still progressing), `false`/throw to kill.\n * MUST be cheap + synchronous (called on a timer, not awaited). */\n onInactivityCheck?: () => boolean\n}\n\nexport interface ManagedExeResult extends RunResult {\n /** True iff stdout was truncated at `maxStdoutBytes` (child was killed). */\n stdoutTruncated: boolean\n /** True iff the inactivity watchdog killed the child (no progress). */\n stalled: boolean\n}\n\n/**\n * Run a **native executable** (a real `.exe`/Mach-O/ELF, NOT a `.cmd`\n * shim) capturing stdout/stderr, with `shell:false` on EVERY platform.\n *\n * Why a separate runner from `runCommandCapture`: that path routes\n * through `buildExecInvocation`, which on Windows builds a\n * `cmd.exe`-quoted command string and **throws on `%`** (`quoteWinArg`).\n * A workspace path can legally contain `%` (and `&`, `(`, `)`, `!`, …),\n * so the managed colgrep binary — which IS a native `.exe`, not a shim —\n * must bypass cmd.exe entirely. `spawn(absExe, args, {shell:false})`\n * resolves the `.exe` via CreateProcess directly: no cmd.exe, no\n * metacharacter hazard, no `%` refusal. POSIX was already `shell:false`.\n * This is what makes \"ANY absolute workspace\" hold on Windows.\n *\n * Lifecycle:\n * - `timeoutMs` → tree-kill on expiry (`taskkill /T /F` on Windows,\n * POSIX process-group `kill(-pgid)` so colgrep's rayon worker\n * children die too). `timedOut: true` in the result.\n * - `maxStdoutBytes` → tree-kill + `stdoutTruncated: true` once the\n * captured stdout exceeds the cap.\n * - `onSpawn(child)` → register the child with the caller's ledger.\n *\n * `command` MUST be an absolute path to the executable (the caller\n * resolves it; we never search PATH here — there is nothing to inject).\n */\nexport function runManagedExeCapture(\n command: string,\n args: ReadonlyArray<string>,\n opts: ManagedExeOpts = {},\n): Promise<ManagedExeResult> {\n const isWin = process.platform === \"win32\"\n return new Promise<ManagedExeResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, [...args], {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell: false,\n windowsHide: true,\n // POSIX: new process group so we can kill the whole tree\n // (colgrep + rayon workers) with kill(-pgid). Windows uses\n // taskkill /T instead; `detached` there has no group semantics.\n detached: !isWin,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n try {\n opts.onSpawn?.(child)\n } catch {\n // ledger registration must never break the spawn\n }\n\n const chunks: Array<Buffer> = []\n let stdoutBytes = 0\n const stderrChunks: Array<Buffer> = []\n let stderrBytes = 0\n const STDERR_CAP = 64 * 1024\n let timedOut = false\n let stdoutTruncated = false\n let stalled = false\n let settled = false\n\n // Exactly one terminator wins: the first to fire records its reason and\n // tree-kills; later ones no-op. Keeps timedOut / stalled / stdoutTruncated\n // mutually exclusive and avoids a double tree-kill.\n let terminated = false\n const terminate = (reason: \"timeout\" | \"stall\" | \"truncate\"): void => {\n if (terminated) return\n terminated = true\n if (reason === \"timeout\") timedOut = true\n else if (reason === \"stall\") stalled = true\n else stdoutTruncated = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n killManagedTree(child, isWin)\n }\n\n const timer = opts.timeoutMs\n ? setTimeout(() => terminate(\"timeout\"), opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n // Inactivity (stall) watchdog. Re-arms itself: when the window elapses\n // with no output, consult the external progress probe — re-arm if it\n // says \"still progressing\" (e.g. the index dir grew), else kill + mark\n // stalled. Reset on every data chunk (a chatty process never stalls).\n let inactivityTimer: ReturnType<typeof setTimeout> | undefined\n const armInactivity = (): void => {\n if (opts.inactivityTimeoutMs === undefined || settled || terminated) return\n inactivityTimer = setTimeout(() => {\n if (settled) return\n // No probe → pure output-inactivity kill. A probe that THROWS is\n // inconclusive → don't kill (re-arm); the absolute timeoutMs backstop\n // still catches a genuinely wedged process.\n let progressing = false\n if (opts.onInactivityCheck) {\n try {\n progressing = opts.onInactivityCheck() === true\n } catch {\n progressing = true\n }\n }\n if (progressing) {\n armInactivity()\n return\n }\n terminate(\"stall\")\n }, opts.inactivityTimeoutMs)\n inactivityTimer?.unref?.()\n }\n const resetInactivity = (): void => {\n if (inactivityTimer) clearTimeout(inactivityTimer)\n armInactivity()\n }\n armInactivity()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n if (stdoutTruncated) return\n stdoutBytes += c.length\n if (\n opts.maxStdoutBytes !== undefined &&\n stdoutBytes > opts.maxStdoutBytes\n ) {\n stdoutTruncated = true\n // Default: tree-kill on overflow. Opt-in: keep the child alive and\n // just stop buffering (the data handler keeps firing + discarding via\n // the `if (stdoutTruncated) return` guard above, so the pipe drains\n // and the child never blocks on a full buffer).\n if (!opts.truncateInsteadOfKill) terminate(\"truncate\")\n return\n }\n chunks.push(c)\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n // Hard byte cap on stderr — append only the slice that fits so a\n // single huge chunk can't overshoot. Never logged raw (it can\n // embed source code from colgrep).\n if (stderrBytes >= STDERR_CAP) return\n const remaining = STDERR_CAP - stderrBytes\n const slice = c.length > remaining ? c.subarray(0, remaining) : c\n stderrChunks.push(slice)\n stderrBytes += slice.length\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n resolve({\n stdout: Buffer.concat(chunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n code,\n timedOut,\n stdoutTruncated,\n stalled,\n })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/**\n * Tree-kill a managed child. Windows: `taskkill /T /F /PID` (whole\n * tree). POSIX: kill the process GROUP (`-pgid`) so colgrep's rayon\n * worker children die with the parent.\n *\n * `runManagedExeCapture` always spawns POSIX children `detached:true`\n * (their own process group), so the group kill is the correct and\n * sufficient primitive. We deliberately do NOT fall back to a positive-\n * pid `process.kill(pid)` when the group kill fails: by the time a kill\n * fires (timeout / byte-cap race), the child may have already exited and\n * its PID been recycled by an unrelated process — a positive-pid kill\n * would then target the wrong process. `ESRCH` (group already gone) is\n * the success case for our purposes; any other error is swallowed.\n */\nexport function killManagedTree(\n child: ReturnType<typeof spawn>,\n isWin: boolean = process.platform === \"win32\",\n): void {\n const pid = child.pid\n if (!pid) return\n try {\n if (isWin) {\n spawn(\"taskkill\", [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n })\n } else {\n // Negative pid → the process group (we spawned detached). No\n // positive-pid fallback (PID-reuse hazard — see doc comment).\n process.kill(-pid, \"SIGKILL\")\n }\n } catch {\n // ESRCH (already gone) / EPERM — best-effort, nothing more to do.\n }\n}\n\n/**\n * Absolute path to Windows `taskkill.exe`. Bare `\"taskkill\"` would let\n * `CreateProcess`'s search order (which can include the cwd) run a planted\n * `taskkill.exe`; pinning the System32 path closes that hijack.\n */\nfunction taskkillExe(): string {\n const root =\n process.env.SystemRoot || process.env.windir || \"C:\\\\Windows\"\n return path.join(root, \"System32\", \"taskkill.exe\")\n}\n\n/**\n * Tree-kill a child process on a SHUTDOWN/exit path, deterministically.\n *\n * Distinct from `killManagedTree` in two ways that matter when the proxy\n * is about to call `process.exit`:\n *\n * 1. **Windows uses `spawnSync`, not async `spawn`.** An async taskkill\n * may never issue before `process.exit` tears the event loop down;\n * the synchronous form guarantees the kill command has run. It is\n * time-bounded so a wedged taskkill can't hang shutdown.\n * 2. **POSIX honors `detachedGroup`.** The launched CLI is spawned\n * `detached:true` (its own process group) so `kill(-pid)` reaps the\n * whole group; other callers (e.g. ripgrep) are NOT detached, so\n * `child.kill()` on the positive pid is correct. A negative-pid kill\n * on a non-detached child would throw or hit the wrong target.\n *\n * Both branches escalate SIGTERM → SIGKILL after `graceMs` so a CLI that\n * traps SIGTERM still dies. The escalation timer is `.unref()`'d so it\n * never holds the event loop open. Errors are swallowed (already-gone /\n * EBUSY / ESRCH are the success case for our purposes).\n */\nexport function killChildProcessTree(\n child: ChildProcess,\n opts: { detachedGroup: boolean; graceMs?: number } = { detachedGroup: false },\n): void {\n const pid = child.pid\n if (!pid) return\n const graceMs = opts.graceMs ?? 500\n\n if (process.platform === \"win32\") {\n // /T = whole tree, /F = force. taskkill walks the live parent-PID\n // chain, so it reaps grandchildren regardless of the cmd.exe-vs-direct\n // spawn shape. Sync (completes before an imminent process.exit) and\n // time-bounded (a wedged taskkill must not hang shutdown).\n try {\n spawnSync(taskkillExe(), [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n timeout: 5000,\n })\n } catch {\n // Already gone / EBUSY race — best-effort.\n }\n return\n }\n\n // POSIX: signal the group (detached) or the lone child (non-detached).\n const target = opts.detachedGroup ? -pid : pid\n try {\n process.kill(target, \"SIGTERM\")\n } catch {\n // ESRCH — already gone.\n }\n const t = setTimeout(() => {\n // For a lone (non-detached) child, skip escalation once it has exited\n // — its positive PID could have been recycled. For a process GROUP the\n // leader's exit does NOT mean the group is empty (a SIGTERM-ignoring\n // grandchild may survive), and the group id is only reused once the\n // group is fully gone, so always attempt the group SIGKILL (ESRCH when\n // already empty is the success case).\n if (\n !opts.detachedGroup &&\n (child.exitCode !== null || child.signalCode !== null)\n ) {\n return\n }\n try {\n process.kill(target, \"SIGKILL\")\n } catch {\n // Already dead.\n }\n }, graceMs)\n t.unref?.()\n}\n","/**\n * ColBERT sidecar lifecycle: in-memory PID ledger for the short-lived\n * `colgrep` children this proxy spawns, signal-handler tree-kill on\n * exit, and a boot-time metadata reclassification sweep.\n *\n * Because colgrep is CLI-per-invocation (no daemon), the lifecycle\n * problem is **process tracking + cancellation + boot/exit sweep**, NOT\n * keep-alive. Modeled on `worker-agent/lifecycle.ts` (PID ledger + boot\n * sweep + per-proxy-run instance UUID) and `exec.ts`'s tree-kill.\n *\n * Three cooperating layers (none sufficient alone):\n * 1. Per-call cleanup — the runner's `finally` force-kills the child\n * it spawned (handled in runner.ts).\n * 2. Session-end signal sweep (this file) — SIGINT/SIGTERM/exit kill\n * every still-tracked child of THIS run.\n * 3. Boot-time sweep (`sweepStaleColbertMetaAtBoot`) — reclassifies\n * `.gh-router-meta/*.json` entries whose `buildPid` is dead from\n * `building` → `failed`. It NEVER issues a kill to a PID from a\n * prior boot (a reused PID may belong to an unrelated process);\n * only the in-memory ledger (this run's spawns) is ever killed.\n */\n\nimport { spawn } from \"node:child_process\"\nimport { randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { killManagedTree } from \"../exec\"\nimport { PATHS } from \"../paths\"\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID (mirrors worker-agent/lifecycle.ts)\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Written into the\n * sidecar metadata `ownerInstanceId` so the boot sweep can tell \"this\n * proxy's still-live build\" from \"a stranded `building` entry from a\n * prior proxy whose PID got recycled\" (Docker PID-1 across restarts).\n */\nexport function getColbertInstanceUuid(): string {\n if (_instanceUuid === null) _instanceUuid = randomUUID()\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetColbertInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// In-memory PID ledger of THIS run's live colgrep children\n// ---------------------------------------------------------------------\n\ntype TrackedChild = ReturnType<typeof spawn>\n\nconst _liveChildren = new Set<TrackedChild>()\n\n/**\n * Register a freshly-spawned colgrep child so the exit sweep can reap\n * it. The runner also removes it on natural close via `untrackChild`.\n */\nexport function trackChild(child: TrackedChild): void {\n _liveChildren.add(child)\n child.once(\"close\", () => _liveChildren.delete(child))\n child.once(\"error\", () => _liveChildren.delete(child))\n}\n\n/** Remove a child from the ledger (e.g. after a clean per-call kill). */\nexport function untrackChild(child: TrackedChild): void {\n _liveChildren.delete(child)\n}\n\n/** Count of live tracked children (test/diagnostic). */\nexport function liveChildCount(): number {\n return _liveChildren.size\n}\n\n/**\n * Synchronous best-effort tree-kill of every tracked child. Called from\n * the signal/exit handlers. After killing, the set is cleared so a\n * second call is a no-op.\n */\nexport function sweepLiveChildren(): void {\n const isWin = process.platform === \"win32\"\n for (const child of _liveChildren) {\n try {\n killManagedTree(child, isWin)\n } catch {\n // already gone\n }\n }\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers (mirror worker-agent/lifecycle.ts re-raise pattern)\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Wire SIGINT/SIGTERM/exit handlers that tree-kill every tracked\n * colgrep child. Idempotent — subsequent calls are a no-op (we never\n * leak listeners). The signal handlers re-raise after sweeping so Node's\n * default terminate-on-signal behavior is restored (otherwise attaching\n * a listener cancels the default and Ctrl-C would clean but not exit).\n */\nexport function registerColbertExitHandlers(): void {\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepLiveChildren()\n _sigintHandler = () => {\n sweepLiveChildren()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepLiveChildren()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n process.on(\"exit\", _exitHandler)\n}\n\n/** Test-only: unregister handlers + reset module state. */\nexport function __unregisterColbertExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _liveChildren.clear()\n}\n\n// ---------------------------------------------------------------------\n// Boot-time metadata reclassification sweep\n// ---------------------------------------------------------------------\n\n/**\n * True iff `pid` names a live process. `process.kill(pid, 0)` probes\n * existence without signalling; `EPERM` means the process exists but is\n * owned by another user (still alive). Exported so the per-query freshness\n * verdict can mirror the boot sweep's liveness check.\n */\nexport function isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. Walks `.gh-router-meta/*.json`; any entry stuck in\n * `status:\"building\"` whose `buildPid` is DEAD is a crashed-build\n * escapee → reset to `status:\"failed\"` so the next search re-kicks a\n * build instead of routing to a never-finishing one.\n *\n * It NEVER kills anything: a live PID matching a stale `buildPid` from a\n * prior boot may be a recycled PID belonging to an unrelated process, so\n * the boot sweep only RECLASSIFIES metadata. The in-memory ledger (this\n * run's spawns) is the only thing the SIGINT/SIGTERM handler ever kills.\n *\n * Best-effort; never throws (wrapped by the caller in `ensurePaths`).\n */\nexport async function sweepStaleColbertMetaAtBoot(): Promise<void> {\n const metaDir = PATHS.COLBERT_META_DIR\n let names: Array<string>\n try {\n names = await fs.readdir(metaDir)\n } catch {\n return // no meta dir yet — nothing to sweep\n }\n for (const name of names) {\n if (!name.endsWith(\".json\")) continue\n const file = path.join(metaDir, name)\n let meta: Record<string, unknown>\n try {\n meta = JSON.parse(await fs.readFile(file, \"utf8\")) as Record<string, unknown>\n } catch {\n continue // corrupt — leave it; index-store re-derives on next access\n }\n if (meta.status !== \"building\") continue\n const buildPid = typeof meta.buildPid === \"number\" ? meta.buildPid : 0\n if (buildPid > 0 && isPidAlive(buildPid)) {\n // A live PID — could be ours (this run re-kicked) or a recycled\n // unrelated PID. Either way: never kill from the boot sweep. Leave\n // the entry; the runner's own ownership check governs.\n continue\n }\n // Dead build PID → reclassify to failed (atomic temp+rename). Stamp the\n // crash class so the per-query self-heal treats it as transient (re-kick)\n // rather than operator-actionable.\n meta.status = \"failed\"\n meta.failureClass = \"crashed\"\n const tmp = `${file}.${process.pid}.tmp`\n try {\n await fs.writeFile(tmp, JSON.stringify(meta, null, 2))\n await fs.rename(tmp, file)\n } catch {\n await fs.rm(tmp, { force: true }).catch(() => {})\n }\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as PATHS, u as writeRuntimeFileSecure } from "./paths-CTlT1nTo.js";
1
+ import { d as writeRuntimeFileSecure, t as PATHS } from "./paths-BZPgxKUl.js";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import nodePath from "node:path";
@@ -307,4 +307,4 @@ async function sweepStaleWorktreesAtBoot() {
307
307
 
308
308
  //#endregion
309
309
  export { sweepRegistry as a, registerExitHandlers as i, getInstanceUuid as n, sweepStaleWorktreesAtBoot as o, recordWorkerRepo as r, WorktreeRegistry as t };
310
- //# sourceMappingURL=lifecycle-BUXxiltc.js.map
310
+ //# sourceMappingURL=lifecycle-Dvr3pGGG.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle-BUXxiltc.js","names":["_instanceUuid: string | null","_activeRegistry: WorktreeRegistry | null","_exitHandler: (() => void) | null","_sigintHandler: (() => void) | null","_sigtermHandler: (() => void) | null","path","raw: string","cleaned: Array<LedgerEntry>","_ledgerChain: Promise<void>","ledger: LedgerFile","names: Array<string>"],"sources":["../src/lib/worker-agent/lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle plumbing for worker worktrees: in-memory registry, signal\n * handlers, ledger of repos touched, and the boot-time PID+instance\n * safety net.\n *\n * Plan: see `plans/we-have-added-a-dreamy-tide.md` (\"Worktree mode\" →\n * \"Cleanup paths\"). Three layers cooperate, none of them sufficient\n * alone:\n *\n * 1. Per-call cleanup (`engine.ts` finally block invoking\n * `WorktreeHandle.remove()`) — covers the happy path.\n *\n * 2. Session-end signal sweep (this file, registered via\n * `registerExitHandlers`) — covers Ctrl+C, service-manager stop,\n * and (in `github-router claude` mode) the spawned child's exit.\n * Synchronous `execFileSync` is intentional: exit handlers can't\n * reliably await async work.\n *\n * 3. Boot-time PID+instance sweep (`sweepStaleWorktreesAtBoot`) —\n * covers SIGKILL, OOM, container restart. Walks the ledger of\n * repos this proxy has touched and removes worktree dirs whose\n * `<pid>` is dead OR whose `<instance>` UUID doesn't match the\n * current proxy's UUID.\n *\n * Ledger writes are ATOMIC (temp + rename) per peer review — a\n * concurrent-RMW corruption would silently strand worktrees because\n * the boot sweep can't find their repo roots.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { PATHS, writeRuntimeFileSecure } from \"../paths\"\n\n/**\n * Same regex worktree.ts uses for its per-call age sweep — kept in\n * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.\n */\nconst WORKTREE_DIR_NAME_RE =\n /^(\\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/\n\n/**\n * Cap on the ledger: how many repos we remember across boots, and how\n * old an entry may be before it's pruned. Both are belt-and-suspenders\n * — the per-call age sweep is the primary guard against accumulation\n * inside any single repo.\n */\nconst LEDGER_MAX_ENTRIES = 100\nconst LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\nexport interface WorktreeRegistryEntry {\n repoRoot: string\n dir: string\n branch: string\n}\n\n/**\n * Set-like in-memory registry of worktrees this proxy created. Engine\n * passes it to `createWorktree` so per-call cleanup deletes the entry\n * on success; the signal handlers walk what's left at shutdown.\n *\n * Not a bare `Set` because we want to expose only the operations we\n * actually use, and we want a stable testable surface.\n */\nexport class WorktreeRegistry {\n private readonly entries = new Set<WorktreeRegistryEntry>()\n\n add(entry: WorktreeRegistryEntry): void {\n this.entries.add(entry)\n }\n delete(entry: WorktreeRegistryEntry): void {\n this.entries.delete(entry)\n }\n has(entry: WorktreeRegistryEntry): boolean {\n return this.entries.has(entry)\n }\n values(): IterableIterator<WorktreeRegistryEntry> {\n return this.entries.values()\n }\n get size(): number {\n return this.entries.size\n }\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Used in worktree\n * dir/branch names so the boot sweep can reliably distinguish \"this\n * proxy's still-live worktrees\" from \"stranded dirs from a prior\n * proxy that happens to have a recycled PID\" — Docker PID-1 across\n * container restarts is the classic case (peer-review HIGH finding).\n */\nexport function getInstanceUuid(): string {\n if (_instanceUuid === null) {\n _instanceUuid = randomUUID()\n }\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers + sweepRegistry\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _activeRegistry: WorktreeRegistry | null = null\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Synchronous cleanup of every registry entry. Best-effort:\n * `execFileSync` failures are swallowed (the dir may have been\n * removed already, or git may not be on PATH any more in some\n * environments). After a successful removal we drop the entry from\n * the registry so a second call is a true no-op.\n *\n * Synchronous on purpose — exit handlers can't reliably await async\n * work; the process would die before the promise settled.\n */\nexport function sweepRegistry(): void {\n if (!_activeRegistry) return\n // Snapshot the values first so we can mutate the underlying set\n // during iteration without skipping entries.\n const snapshot = [..._activeRegistry.values()]\n for (const entry of snapshot) {\n try {\n // `-C entry.repoRoot` is load-bearing: without it git resolves\n // the worktree path relative to the proxy's cwd (which is the\n // user's launch dir, typically NOT inside the target repo), and\n // fails with `fatal: '<path>' is not a working tree`. The E2E\n // boot-sweep test (worker-agent-boot-sweep.test.ts) is what\n // caught the missing flag.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", entry.dir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // Already gone, EBUSY, or git not on PATH — best effort.\n }\n try {\n execFileSync(\"git\", [\"-C\", entry.repoRoot, \"branch\", \"-D\", entry.branch], {\n stdio: \"ignore\",\n timeout: 5_000,\n windowsHide: true,\n })\n } catch {\n // Same as above.\n }\n _activeRegistry.delete(entry)\n }\n}\n\n/**\n * Windows ConPTY / node-pty signal behavior:\n *\n * When a ConPTY host (VS Code terminal, Windows Terminal, node-pty) closes\n * the pseudo-console, the ConPTY layer sends CTRL_CLOSE_EVENT to the\n * process group. Node.js translates this into SIGINT (NOT SIGTERM). The\n * process has a ~5-second window before forced termination.\n *\n * Implication: the SIGTERM handler below may NEVER fire in node-pty\n * environments. This is by design — the three-layer cleanup architecture\n * ensures coverage:\n * 1. Per-call cleanup (engine.ts finally block) — happy path\n * 2. SIGINT handler (this file) — ConPTY close, Ctrl+C\n * 3. `exit` handler (this file) — unconditional, fires on any exit\n * 4. Boot-time PID+instance sweep (sweepStaleWorktreesAtBoot) — crash recovery\n *\n * Layers 1+2+3 cover ConPTY; layer 4 covers SIGKILL/OOM/container restart.\n */\n\n/**\n * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and\n * remove every entry. Idempotent: subsequent calls swap the registry\n * pointer but do NOT register additional process listeners (otherwise\n * we'd leak listeners on every `runWorkerAgent`).\n *\n * Signal handlers re-raise the signal after sweeping. Naively running\n * the sweep on SIGINT/SIGTERM and returning would *suppress* the\n * signal: Node defaults to terminating the process on these, but only\n * if no user listener is attached. Once we attach a listener, the\n * default action is cancelled and the process keeps running — which\n * means Ctrl-C would clean worktrees but not actually exit, leaving\n * orphan processes in dev. The `process.kill(pid, sig)` re-raise\n * after removing our own listener restores the default behaviour\n * (the second delivery now hits an empty listener list, so Node\n * terminates with the conventional `128 + signum` exit code).\n */\nexport function registerExitHandlers(registry: WorktreeRegistry): void {\n _activeRegistry = registry\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepRegistry()\n _sigintHandler = () => {\n sweepRegistry()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepRegistry()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n // `exit` handlers can only run synchronous code — exactly what\n // sweepRegistry does. Async work here would never complete.\n process.on(\"exit\", _exitHandler)\n}\n\n/**\n * Test-only: unregister the handlers and reset module state. Tests\n * that want to verify `registerExitHandlers` semantics must clean up\n * after themselves or future tests in the same process inherit the\n * (now stale) registry pointer.\n */\nexport function __unregisterExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _activeRegistry = null\n}\n\n// ---------------------------------------------------------------------\n// Ledger: which repos has this proxy touched?\n// ---------------------------------------------------------------------\n\ninterface LedgerEntry {\n repoRoot: string\n lastSeenMs: number\n}\n\ninterface LedgerFile {\n entries: Array<LedgerEntry>\n}\n\nfunction ledgerPath(): string {\n return path.join(PATHS.APP_DIR, \"worker-repos.json\")\n}\n\nasync function readLedger(): Promise<LedgerFile> {\n let raw: string\n try {\n raw = await fs.readFile(ledgerPath(), \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { entries: [] }\n }\n return { entries: [] }\n }\n try {\n const parsed = JSON.parse(raw) as Partial<LedgerFile>\n if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] }\n const cleaned: Array<LedgerEntry> = []\n for (const e of parsed.entries) {\n if (\n e &&\n typeof e === \"object\" &&\n typeof (e as LedgerEntry).repoRoot === \"string\" &&\n typeof (e as LedgerEntry).lastSeenMs === \"number\"\n ) {\n cleaned.push({\n repoRoot: (e as LedgerEntry).repoRoot,\n lastSeenMs: (e as LedgerEntry).lastSeenMs,\n })\n }\n }\n return { entries: cleaned }\n } catch {\n // Corrupted JSON — start fresh rather than crashing the proxy.\n return { entries: [] }\n }\n}\n\n/**\n * Per-process serializer for ledger writes. Multiple concurrent\n * `recordWorkerRepo` calls (legitimate: several workers may start at\n * once) would otherwise race read-modify-write on the JSON file. Each\n * call chains onto the previous so the on-disk sequence is\n * deterministic from this process's perspective.\n *\n * Cross-process safety is provided by the atomic temp+rename below,\n * which makes the final state of the file always be a well-formed\n * full snapshot from ONE writer — never a partial write or\n * interleaved JSON.\n */\nlet _ledgerChain: Promise<void> = Promise.resolve()\n\n/**\n * Append `repoRoot` to the ledger (or update its `lastSeenMs`).\n * Atomic temp+rename per peer review.\n */\nexport function recordWorkerRepo(repoRoot: string): Promise<void> {\n const next = _ledgerChain.then(async () => {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n const current = await readLedger()\n // Dedup: drop any existing entry for this root before appending\n // the fresh one so the array doesn't grow unbounded with repeats.\n const filtered = current.entries.filter((e) => e.repoRoot !== repoRoot)\n filtered.push({ repoRoot, lastSeenMs: Date.now() })\n // Prune by age and cap entry count (newest wins).\n const now = Date.now()\n const pruned = filtered\n .filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS)\n .slice(-LEDGER_MAX_ENTRIES)\n const ledger: LedgerFile = { entries: pruned }\n\n // Atomic temp+rename. The temp filename is unique per call\n // (PID + 8 random hex chars) so concurrent processes don't\n // collide on the temp name; the final `rename` is atomic on\n // POSIX and on Windows (both with same filesystem).\n const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString(\n \"hex\",\n )}`\n try {\n await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2))\n await fs.rename(tmp, ledgerPath())\n } catch (err) {\n // Clean up the temp file if rename failed midway.\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n })\n // Swallow chain-internal errors so one failed write doesn't poison\n // the chain for every subsequent caller. Each call still sees its\n // own rejection (we return `next`, not the catch-handler chain).\n _ledgerChain = next.catch(() => undefined)\n return next\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n // EPERM = process exists but we can't signal it — still alive\n // for our purposes (we just need to know whether to clean up).\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. For every repo we recorded in the ledger,\n * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional\n * location — for repos already inside a worktree, the actual\n * `git-common-dir` may differ, in which case we'll miss this batch\n * and the per-call age sweep will catch them within 7 days) and\n * remove dirs that aren't owned by THIS proxy.\n *\n * Ownership rule: dir is \"ours\" iff its embedded PID is alive AND\n * its embedded UUID equals `getInstanceUuid()`. Either condition\n * failing → remove.\n */\nexport async function sweepStaleWorktreesAtBoot(): Promise<void> {\n const ledger = await readLedger()\n if (ledger.entries.length === 0) return\n const currentUuid = getInstanceUuid()\n for (const entry of ledger.entries) {\n const parent = path.join(entry.repoRoot, \".git\", \"worker-worktrees\")\n let names: Array<string>\n try {\n names = await fs.readdir(parent)\n } catch {\n continue\n }\n for (const name of names) {\n const m = WORKTREE_DIR_NAME_RE.exec(name)\n if (!m) continue\n const pid = Number.parseInt(m[1], 10)\n const uuid = m[2]\n const isOurs = isPidAlive(pid) && uuid === currentUuid\n if (isOurs) continue\n\n const fullDir = path.join(parent, name)\n const branch = `worker/${pid}-${uuid}-${m[3]}`\n try {\n // `-C entry.repoRoot` is load-bearing here too — see the\n // matching comment in `sweepRegistry`. The boot sweep runs\n // BEFORE any worker tool has set cwd, so the proxy's cwd is\n // the user's launch dir, which is almost never inside the\n // target repo.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", fullDir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"branch\", \"-D\", branch],\n { stdio: \"ignore\", timeout: 5_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n await fs.rm(fullDir, { recursive: true, force: true })\n } catch {\n // ignore — git may have removed it already\n }\n }\n }\n}\n\n/** Test-only: clear the ledger file (does NOT remove on-disk worktrees). */\nexport async function __clearLedgerForTests(): Promise<void> {\n await fs.unlink(ledgerPath()).catch(() => {})\n}\n\n/** Test-only: read the ledger as a plain array (no side effects). */\nexport async function __readLedgerForTests(): Promise<Array<LedgerEntry>> {\n return (await readLedger()).entries\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle-Dvr3pGGG.js","names":["_instanceUuid: string | null","_activeRegistry: WorktreeRegistry | null","_exitHandler: (() => void) | null","_sigintHandler: (() => void) | null","_sigtermHandler: (() => void) | null","path","raw: string","cleaned: Array<LedgerEntry>","_ledgerChain: Promise<void>","ledger: LedgerFile","names: Array<string>"],"sources":["../src/lib/worker-agent/lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle plumbing for worker worktrees: in-memory registry, signal\n * handlers, ledger of repos touched, and the boot-time PID+instance\n * safety net.\n *\n * Plan: see `plans/we-have-added-a-dreamy-tide.md` (\"Worktree mode\" →\n * \"Cleanup paths\"). Three layers cooperate, none of them sufficient\n * alone:\n *\n * 1. Per-call cleanup (`engine.ts` finally block invoking\n * `WorktreeHandle.remove()`) — covers the happy path.\n *\n * 2. Session-end signal sweep (this file, registered via\n * `registerExitHandlers`) — covers Ctrl+C, service-manager stop,\n * and (in `github-router claude` mode) the spawned child's exit.\n * Synchronous `execFileSync` is intentional: exit handlers can't\n * reliably await async work.\n *\n * 3. Boot-time PID+instance sweep (`sweepStaleWorktreesAtBoot`) —\n * covers SIGKILL, OOM, container restart. Walks the ledger of\n * repos this proxy has touched and removes worktree dirs whose\n * `<pid>` is dead OR whose `<instance>` UUID doesn't match the\n * current proxy's UUID.\n *\n * Ledger writes are ATOMIC (temp + rename) per peer review — a\n * concurrent-RMW corruption would silently strand worktrees because\n * the boot sweep can't find their repo roots.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { PATHS, writeRuntimeFileSecure } from \"../paths\"\n\n/**\n * Same regex worktree.ts uses for its per-call age sweep — kept in\n * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.\n */\nconst WORKTREE_DIR_NAME_RE =\n /^(\\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/\n\n/**\n * Cap on the ledger: how many repos we remember across boots, and how\n * old an entry may be before it's pruned. Both are belt-and-suspenders\n * — the per-call age sweep is the primary guard against accumulation\n * inside any single repo.\n */\nconst LEDGER_MAX_ENTRIES = 100\nconst LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\nexport interface WorktreeRegistryEntry {\n repoRoot: string\n dir: string\n branch: string\n}\n\n/**\n * Set-like in-memory registry of worktrees this proxy created. Engine\n * passes it to `createWorktree` so per-call cleanup deletes the entry\n * on success; the signal handlers walk what's left at shutdown.\n *\n * Not a bare `Set` because we want to expose only the operations we\n * actually use, and we want a stable testable surface.\n */\nexport class WorktreeRegistry {\n private readonly entries = new Set<WorktreeRegistryEntry>()\n\n add(entry: WorktreeRegistryEntry): void {\n this.entries.add(entry)\n }\n delete(entry: WorktreeRegistryEntry): void {\n this.entries.delete(entry)\n }\n has(entry: WorktreeRegistryEntry): boolean {\n return this.entries.has(entry)\n }\n values(): IterableIterator<WorktreeRegistryEntry> {\n return this.entries.values()\n }\n get size(): number {\n return this.entries.size\n }\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Used in worktree\n * dir/branch names so the boot sweep can reliably distinguish \"this\n * proxy's still-live worktrees\" from \"stranded dirs from a prior\n * proxy that happens to have a recycled PID\" — Docker PID-1 across\n * container restarts is the classic case (peer-review HIGH finding).\n */\nexport function getInstanceUuid(): string {\n if (_instanceUuid === null) {\n _instanceUuid = randomUUID()\n }\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers + sweepRegistry\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _activeRegistry: WorktreeRegistry | null = null\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Synchronous cleanup of every registry entry. Best-effort:\n * `execFileSync` failures are swallowed (the dir may have been\n * removed already, or git may not be on PATH any more in some\n * environments). After a successful removal we drop the entry from\n * the registry so a second call is a true no-op.\n *\n * Synchronous on purpose — exit handlers can't reliably await async\n * work; the process would die before the promise settled.\n */\nexport function sweepRegistry(): void {\n if (!_activeRegistry) return\n // Snapshot the values first so we can mutate the underlying set\n // during iteration without skipping entries.\n const snapshot = [..._activeRegistry.values()]\n for (const entry of snapshot) {\n try {\n // `-C entry.repoRoot` is load-bearing: without it git resolves\n // the worktree path relative to the proxy's cwd (which is the\n // user's launch dir, typically NOT inside the target repo), and\n // fails with `fatal: '<path>' is not a working tree`. The E2E\n // boot-sweep test (worker-agent-boot-sweep.test.ts) is what\n // caught the missing flag.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", entry.dir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // Already gone, EBUSY, or git not on PATH — best effort.\n }\n try {\n execFileSync(\"git\", [\"-C\", entry.repoRoot, \"branch\", \"-D\", entry.branch], {\n stdio: \"ignore\",\n timeout: 5_000,\n windowsHide: true,\n })\n } catch {\n // Same as above.\n }\n _activeRegistry.delete(entry)\n }\n}\n\n/**\n * Windows ConPTY / node-pty signal behavior:\n *\n * When a ConPTY host (VS Code terminal, Windows Terminal, node-pty) closes\n * the pseudo-console, the ConPTY layer sends CTRL_CLOSE_EVENT to the\n * process group. Node.js translates this into SIGINT (NOT SIGTERM). The\n * process has a ~5-second window before forced termination.\n *\n * Implication: the SIGTERM handler below may NEVER fire in node-pty\n * environments. This is by design — the three-layer cleanup architecture\n * ensures coverage:\n * 1. Per-call cleanup (engine.ts finally block) — happy path\n * 2. SIGINT handler (this file) — ConPTY close, Ctrl+C\n * 3. `exit` handler (this file) — unconditional, fires on any exit\n * 4. Boot-time PID+instance sweep (sweepStaleWorktreesAtBoot) — crash recovery\n *\n * Layers 1+2+3 cover ConPTY; layer 4 covers SIGKILL/OOM/container restart.\n */\n\n/**\n * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and\n * remove every entry. Idempotent: subsequent calls swap the registry\n * pointer but do NOT register additional process listeners (otherwise\n * we'd leak listeners on every `runWorkerAgent`).\n *\n * Signal handlers re-raise the signal after sweeping. Naively running\n * the sweep on SIGINT/SIGTERM and returning would *suppress* the\n * signal: Node defaults to terminating the process on these, but only\n * if no user listener is attached. Once we attach a listener, the\n * default action is cancelled and the process keeps running — which\n * means Ctrl-C would clean worktrees but not actually exit, leaving\n * orphan processes in dev. The `process.kill(pid, sig)` re-raise\n * after removing our own listener restores the default behaviour\n * (the second delivery now hits an empty listener list, so Node\n * terminates with the conventional `128 + signum` exit code).\n */\nexport function registerExitHandlers(registry: WorktreeRegistry): void {\n _activeRegistry = registry\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepRegistry()\n _sigintHandler = () => {\n sweepRegistry()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepRegistry()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n // `exit` handlers can only run synchronous code — exactly what\n // sweepRegistry does. Async work here would never complete.\n process.on(\"exit\", _exitHandler)\n}\n\n/**\n * Test-only: unregister the handlers and reset module state. Tests\n * that want to verify `registerExitHandlers` semantics must clean up\n * after themselves or future tests in the same process inherit the\n * (now stale) registry pointer.\n */\nexport function __unregisterExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _activeRegistry = null\n}\n\n// ---------------------------------------------------------------------\n// Ledger: which repos has this proxy touched?\n// ---------------------------------------------------------------------\n\ninterface LedgerEntry {\n repoRoot: string\n lastSeenMs: number\n}\n\ninterface LedgerFile {\n entries: Array<LedgerEntry>\n}\n\nfunction ledgerPath(): string {\n return path.join(PATHS.APP_DIR, \"worker-repos.json\")\n}\n\nasync function readLedger(): Promise<LedgerFile> {\n let raw: string\n try {\n raw = await fs.readFile(ledgerPath(), \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { entries: [] }\n }\n return { entries: [] }\n }\n try {\n const parsed = JSON.parse(raw) as Partial<LedgerFile>\n if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] }\n const cleaned: Array<LedgerEntry> = []\n for (const e of parsed.entries) {\n if (\n e &&\n typeof e === \"object\" &&\n typeof (e as LedgerEntry).repoRoot === \"string\" &&\n typeof (e as LedgerEntry).lastSeenMs === \"number\"\n ) {\n cleaned.push({\n repoRoot: (e as LedgerEntry).repoRoot,\n lastSeenMs: (e as LedgerEntry).lastSeenMs,\n })\n }\n }\n return { entries: cleaned }\n } catch {\n // Corrupted JSON — start fresh rather than crashing the proxy.\n return { entries: [] }\n }\n}\n\n/**\n * Per-process serializer for ledger writes. Multiple concurrent\n * `recordWorkerRepo` calls (legitimate: several workers may start at\n * once) would otherwise race read-modify-write on the JSON file. Each\n * call chains onto the previous so the on-disk sequence is\n * deterministic from this process's perspective.\n *\n * Cross-process safety is provided by the atomic temp+rename below,\n * which makes the final state of the file always be a well-formed\n * full snapshot from ONE writer — never a partial write or\n * interleaved JSON.\n */\nlet _ledgerChain: Promise<void> = Promise.resolve()\n\n/**\n * Append `repoRoot` to the ledger (or update its `lastSeenMs`).\n * Atomic temp+rename per peer review.\n */\nexport function recordWorkerRepo(repoRoot: string): Promise<void> {\n const next = _ledgerChain.then(async () => {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n const current = await readLedger()\n // Dedup: drop any existing entry for this root before appending\n // the fresh one so the array doesn't grow unbounded with repeats.\n const filtered = current.entries.filter((e) => e.repoRoot !== repoRoot)\n filtered.push({ repoRoot, lastSeenMs: Date.now() })\n // Prune by age and cap entry count (newest wins).\n const now = Date.now()\n const pruned = filtered\n .filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS)\n .slice(-LEDGER_MAX_ENTRIES)\n const ledger: LedgerFile = { entries: pruned }\n\n // Atomic temp+rename. The temp filename is unique per call\n // (PID + 8 random hex chars) so concurrent processes don't\n // collide on the temp name; the final `rename` is atomic on\n // POSIX and on Windows (both with same filesystem).\n const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString(\n \"hex\",\n )}`\n try {\n await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2))\n await fs.rename(tmp, ledgerPath())\n } catch (err) {\n // Clean up the temp file if rename failed midway.\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n })\n // Swallow chain-internal errors so one failed write doesn't poison\n // the chain for every subsequent caller. Each call still sees its\n // own rejection (we return `next`, not the catch-handler chain).\n _ledgerChain = next.catch(() => undefined)\n return next\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n // EPERM = process exists but we can't signal it — still alive\n // for our purposes (we just need to know whether to clean up).\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. For every repo we recorded in the ledger,\n * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional\n * location — for repos already inside a worktree, the actual\n * `git-common-dir` may differ, in which case we'll miss this batch\n * and the per-call age sweep will catch them within 7 days) and\n * remove dirs that aren't owned by THIS proxy.\n *\n * Ownership rule: dir is \"ours\" iff its embedded PID is alive AND\n * its embedded UUID equals `getInstanceUuid()`. Either condition\n * failing → remove.\n */\nexport async function sweepStaleWorktreesAtBoot(): Promise<void> {\n const ledger = await readLedger()\n if (ledger.entries.length === 0) return\n const currentUuid = getInstanceUuid()\n for (const entry of ledger.entries) {\n const parent = path.join(entry.repoRoot, \".git\", \"worker-worktrees\")\n let names: Array<string>\n try {\n names = await fs.readdir(parent)\n } catch {\n continue\n }\n for (const name of names) {\n const m = WORKTREE_DIR_NAME_RE.exec(name)\n if (!m) continue\n const pid = Number.parseInt(m[1], 10)\n const uuid = m[2]\n const isOurs = isPidAlive(pid) && uuid === currentUuid\n if (isOurs) continue\n\n const fullDir = path.join(parent, name)\n const branch = `worker/${pid}-${uuid}-${m[3]}`\n try {\n // `-C entry.repoRoot` is load-bearing here too — see the\n // matching comment in `sweepRegistry`. The boot sweep runs\n // BEFORE any worker tool has set cwd, so the proxy's cwd is\n // the user's launch dir, which is almost never inside the\n // target repo.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", fullDir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"branch\", \"-D\", branch],\n { stdio: \"ignore\", timeout: 5_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n await fs.rm(fullDir, { recursive: true, force: true })\n } catch {\n // ignore — git may have removed it already\n }\n }\n }\n}\n\n/** Test-only: clear the ledger file (does NOT remove on-disk worktrees). */\nexport async function __clearLedgerForTests(): Promise<void> {\n await fs.unlink(ledgerPath()).catch(() => {})\n}\n\n/** Test-only: read the ledger as a plain array (no side effects). */\nexport async function __readLedgerForTests(): Promise<Array<LedgerEntry>> {\n return (await readLedger()).entries\n}\n"],"mappings":"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"}
package/dist/main.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $ as isAdvisorRequested, $t as cacheModels, A as liveExec, At as hasSupportedBrowserInstalled, Bt as DEFAULT_CODEX_MODEL_FALLBACKS, C as fileReviewDebounce, Ct as pickEndpoint, D as stopGateEnabledForRepo, Dt as readResponseBodyCapped, E as repoRoot, Et as MAX_RESPONSE_BODY_BYTES, Ft as ArtifactClient, G as vscodeRipgrepPath, Gt as pickClaudeDefault, H as buildToolbeltAwareness, Ht as UPSTREAM_FETCH_TIMEOUT_MS, It as collapsePathKeys, J as searchWeb, Jt as setupCopilotToken, K as TOOLBELT_TOOLS, Kt as getPackageVersion, Lt as toolbeltPathOverride, Mt as extractTarGzMember, Nt as extractZipMember, O as stopReviewStateDir, Ot as parseJsonOrDiagnose, Pt as shouldUseInsecureTls, Q as injectAdvisorTool, Qt as cacheCopilotVersion, Rt as DEFAULT_CLAUDE_MODEL_FALLBACKS, S as fileLastPromptStore, St as resolveMcpToolTimeoutMs, T as repoFingerprint, Tt as createChatCompletions, U as toolbeltEnabled, Ut as UPSTREAM_INACTIVITY_TIMEOUT_MS, V as availableToolCommands, Vt as DEFAULT_PORT, W as toolbeltSkipSet, Wt as generateRandomPort, X as ADVISOR_TOOL_INSTRUCTIONS, Xt as setupGitHubToken, Y as ADVISOR_INTERNAL_TOOL_NAME, Yt as setupGitHubAgentToken, Z as buildAdvisorStream, Zt as tryRefreshAndRetry, _ as stopGateId, _t as workerToolsEnabled, a as buildPeerAwarenessSnippet, an as sleep, at as relayAnthropicStream, b as fileBaselineStore, bt as getTokenCount, c as buildArtifactOpenHookCommand, cn as HTTPError, ct as agentToolsEnabled, d as captureLaunchBaseline, dn as copilotBaseUrl, dt as browserCompoundToolsEnabled, en as cacheVSCodeVersion, et as buildAnthropicErrorEvent, f as decideStopHook, fn as copilotHeaders, ft as browserToolsEnabled, g as stopGateDisabled, gt as standInToolEnabled, h as launchBaselineKey, ht as implementerSubagentModel, i as buildAgentPrompt, in as resolveModel, it as readIteratorWithTimeout, j as resolveSealedGate, jt as provisionAndIndexColbert, k as trustRepo, kt as provisionBrowserAssets, l as buildSessionBindHookCommand, ln as forwardError, lt as artifactToolsEnabled, m as injectStopHookIntoSettingsFile, mn as state, mt as geminiAvailable, n as MCP_GROUPS, nn as isNullish, nt as isControllerClosedError, o as buildPeerAwarenessSummary, on as getModels, ot as handleMcpDelete, p as fileBlockBudget, pn as githubHeaders, pt as fleetToolsEnabled, q as assetFor, qt as withInstallLock, r as assertMcpToolSurfaceConsistent, rn as resolveCodexModel, rt as logStreamError, s as personasFor, sn as fetchWithTransientRetry, st as handleMcpPost, t as GROUP_META, tn as filterBetaHeader, tt as buildOpenAIErrorEvent, u as buildStopHookCommand, un as GITHUB_API_BASE_URL, ut as browseAgentEnabled, v as stopGatePlanMode, vt as countTokens, w as isSubagentContext, wt as createResponses, x as fileFindingsStore, xt as assembleResponsesPayload, y as stopReviewEnabled, yt as createMessages, zt as DEFAULT_CODEX_MODEL } from "./peer-mcp-personas-BKkdfOyK.js";
3
- import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeArtifactCredsToMirror, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS, u as writeRuntimeFileSecure } from "./paths-CTlT1nTo.js";
4
- import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-ChPBRt6K.js";
5
- import { a as sweepRegistry } from "./lifecycle-BUXxiltc.js";
2
+ import { $ as isAdvisorRequested, $t as cacheCopilotVersion, A as liveExec, At as provisionBrowserAssets, Bt as DEFAULT_CODEX_MODEL, C as fileReviewDebounce, Ct as resolveMcpToolTimeoutMs, D as stopGateEnabledForRepo, Dt as MAX_RESPONSE_BODY_BYTES, E as repoRoot, Et as createChatCompletions, Ft as shouldUseInsecureTls, G as vscodeRipgrepPath, Gt as generateRandomPort, H as buildToolbeltAwareness, Ht as DEFAULT_PORT, It as ArtifactClient, J as searchWeb, Jt as withInstallLock, K as TOOLBELT_TOOLS, Kt as pickClaudeDefault, Lt as collapsePathKeys, Mt as provisionAndIndexColbert, Nt as extractTarGzMember, O as stopReviewStateDir, Ot as readResponseBodyCapped, Pt as extractZipMember, Q as injectAdvisorTool, Qt as tryRefreshAndRetry, Rt as toolbeltPathOverride, S as fileLastPromptStore, St as assembleResponsesPayload, T as repoFingerprint, Tt as createResponses, U as toolbeltEnabled, Ut as UPSTREAM_FETCH_TIMEOUT_MS, V as availableToolCommands, Vt as DEFAULT_CODEX_MODEL_FALLBACKS, W as toolbeltSkipSet, Wt as UPSTREAM_INACTIVITY_TIMEOUT_MS, X as ADVISOR_TOOL_INSTRUCTIONS, Xt as setupGitHubAgentToken, Y as ADVISOR_INTERNAL_TOOL_NAME, Yt as setupCopilotToken, Z as buildAdvisorStream, Zt as setupGitHubToken, _ as stopGateId, _t as workerToolsEnabled, a as buildPeerAwarenessSnippet, an as resolveModel, at as relayAnthropicStream, b as fileBaselineStore, bt as createMessages, c as buildArtifactOpenHookCommand, cn as fetchWithTransientRetry, ct as agentToolsEnabled, d as captureLaunchBaseline, dn as GITHUB_API_BASE_URL, dt as browserCompoundToolsEnabled, en as cacheModels, et as buildAnthropicErrorEvent, f as decideStopHook, fn as copilotBaseUrl, ft as browserToolsEnabled, g as stopGateDisabled, gt as standInToolEnabled, h as launchBaselineKey, hn as state, ht as nativeSubagentModel, i as buildAgentPrompt, in as resolveCodexModel, it as readIteratorWithTimeout, j as resolveSealedGate, jt as hasSupportedBrowserInstalled, k as trustRepo, kt as parseJsonOrDiagnose, l as buildSessionBindHookCommand, ln as HTTPError, lt as artifactToolsEnabled, m as injectStopHookIntoSettingsFile, mn as githubHeaders, mt as geminiAvailable, n as MCP_GROUPS, nn as filterBetaHeader, nt as isControllerClosedError, o as buildPeerAwarenessSummary, on as sleep, ot as handleMcpDelete, p as fileBlockBudget, pn as copilotHeaders, pt as fleetToolsEnabled, q as assetFor, qt as getPackageVersion, r as assertMcpToolSurfaceConsistent, rn as isNullish, rt as logStreamError, s as personasFor, sn as getModels, st as handleMcpPost, t as GROUP_META, tn as cacheVSCodeVersion, tt as buildOpenAIErrorEvent, u as buildStopHookCommand, un as forwardError, ut as browseAgentEnabled, v as stopGatePlanMode, vt as shimDefaultsToXhigh, w as isSubagentContext, wt as pickEndpoint, x as fileFindingsStore, xt as getTokenCount, y as stopReviewEnabled, yt as countTokens, zt as DEFAULT_CLAUDE_MODEL_FALLBACKS } from "./peer-mcp-personas-B1-1mVPB.js";
3
+ import { a as isUnderClaudeConfigMirror, d as writeRuntimeFileSecure, i as ensurePaths, o as removeOwnClaudeConfigMirror, r as ensureClaudeConfigMirror, t as PATHS, u as writeArtifactCredsToMirror } from "./paths-BZPgxKUl.js";
4
+ import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-DrDEjx_c.js";
5
+ import { a as sweepRegistry } from "./lifecycle-Dvr3pGGG.js";
6
6
  import { defineCommand, runMain } from "citty";
7
7
  import consola from "consola";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -1205,10 +1205,22 @@ function buildPeerAgentDefinitions(opts) {
1205
1205
  codexCli: opts.codexCli,
1206
1206
  geminiAvailable: opts.geminiAvailable
1207
1207
  });
1208
- if (opts.implementerModel && opts.implementerModel.length > 0) out.implementer = {
1209
- description: `Bounded implementation subagent running ${opts.implementerModel} (strong non-Claude coder, high reasoning). Use for well-scoped coding tasks — edits, small features, fixes — you want implemented in an integrated subagent. Model is overridable at spawn.`,
1208
+ const nativeModel = opts.nativeSubagentModel && opts.nativeSubagentModel.length > 0 ? opts.nativeSubagentModel : void 0;
1209
+ const modelField = nativeModel ? { model: nativeModel } : {};
1210
+ out.implementer = {
1211
+ description: nativeModel ? `Bounded implementation subagent running ${nativeModel} (strong non-Claude coder, maximum reasoning). Use proactively for well-scoped coding tasks — edits, small features, fixes — to keep the lead's context focused; runs in its own context. Model is overridable at spawn.` : `Bounded implementation subagent (native tools, runs on the lead's model in its own context). Use proactively for well-scoped coding tasks — edits, small features, fixes — to keep the lead's context focused. Model is overridable at spawn.`,
1210
1212
  prompt: "You are a bounded implementation subagent for well-scoped coding tasks. Implement the requested change surgically, matching the surrounding code style and minimizing unrelated churn. Use the dedicated Edit/Write/Read tools for file changes and Grep/Glob for search; reserve Bash for running builds, tests, and git — do not shell out (sed/awk/python/here-docs) to read or edit files. Verify with the project's build or tests where applicable. Do the work yourself — do not spawn further subagents. Report exactly what changed and any risks.",
1211
- model: opts.implementerModel
1213
+ ...modelField
1214
+ };
1215
+ out.debugger = {
1216
+ description: nativeModel ? `Root-cause & debugging subagent running ${nativeModel} at maximum reasoning. Use proactively for reproducing a bug end to end, isolating the true root cause, and proposing a minimal fix — investigation-heavy work best kept off the lead's context; runs in its own context. Model is overridable at spawn.` : `Root-cause & debugging subagent (native tools, runs on the lead's model in its own context). Use proactively for reproducing a bug end to end, isolating the true root cause, and proposing a minimal fix — kept off the lead's context. Model is overridable at spawn.`,
1217
+ prompt: "You are a root-cause debugging subagent. Reproduce the failure end to end first — as close to how a real user hits it as you can — before theorizing. Form hypotheses and test them against the actual code and runtime: read the code, run the repro with Bash, and add temporary instrumentation only if needed (remove it after). Identify the true root cause, not a symptom; if a fix is in scope, make it minimal and verify the repro now passes. Use the dedicated Edit/Write/Read tools for file changes and Grep/Glob for search; reserve Bash for running repros, tests, and git — do not shell out (sed/awk/python/here-docs) to read or edit files. Do the work yourself — do not spawn further subagents. Report the root cause with evidence, the fix (if any), and any residual risks.",
1218
+ ...modelField
1219
+ };
1220
+ out["qa-engineer"] = {
1221
+ description: nativeModel ? `Review, testing & QA subagent running ${nativeModel} at maximum reasoning. Use proactively to review a change for correctness, author and run tests that try to break it, and give a severity-ranked go/no-go while keeping the lead's context focused; runs in its own context. Model is overridable at spawn.` : `Review, testing & QA subagent (native tools, runs on the lead's model in its own context). Use proactively to review a change for correctness, author and run tests that try to break it, and give a severity-ranked go/no-go. Model is overridable at spawn.`,
1222
+ prompt: "You are a review, testing, and QA subagent. Verify correctness against the ACTUAL code by reading it — never assume. Where the change warrants it, author tests that try to BREAK the implementation (edge cases, error paths, and the acceptance criteria as executable checks), then run them and report which pass and which fail; do NOT modify production code just to make tests pass. Use the dedicated Edit/Write/Read tools for file changes and Grep/Glob for search; reserve Bash for running builds, tests, and git — do not shell out (sed/awk/python/here-docs) to read or edit files. Do the work yourself — do not spawn further subagents. Report severity-ranked findings with `file:line` citations and end with a clear go/no-go.",
1223
+ ...modelField
1212
1224
  };
1213
1225
  if (opts.workerToolsAvailable) {
1214
1226
  const workersKey = workersKeyOf(opts.groupKeys);
@@ -1506,7 +1518,7 @@ async function writePeerMcpRuntimeFiles(serverUrl, opts) {
1506
1518
  groupKeys: opts.groupKeys,
1507
1519
  workerToolsAvailable: opts.workerToolsAvailable,
1508
1520
  browseAvailable: opts.browseAvailable,
1509
- implementerModel: opts.implementerModel,
1521
+ nativeSubagentModel: opts.nativeSubagentModel,
1510
1522
  nonce,
1511
1523
  codexHome
1512
1524
  });
@@ -2556,7 +2568,7 @@ async function discoverGateCommands(cwd, opts) {
2556
2568
  if (files.length === 0) return null;
2557
2569
  let result;
2558
2570
  try {
2559
- const { runWorkerAgent } = await import("./engine-BWoDQ-3C.js");
2571
+ const { runWorkerAgent } = await import("./engine-D7mUKnIZ.js");
2560
2572
  result = await runWorkerAgent({
2561
2573
  mode: "explore",
2562
2574
  workspace: root,
@@ -3575,7 +3587,7 @@ const STYLE_DIRECTIVE = "Write concisely without losing detail. Use a natural hu
3575
3587
  * Self-referentially compliant with the style directive: no em dashes, no
3576
3588
  * Claude / Anthropic attribution.
3577
3589
  */
3578
- const OPERATING_DEFAULTS_DIRECTIVE = "## Operating defaults (apply when the user has not specified otherwise; the user's explicit direction and the domain's own standards always override)\n\nOrchestrate. Delegate research, implementation, review, and large reads to the right subagent, worker, or model (worker-* agents for background non-blocking runs, Task subagents for parallel work, peer critics for review) so your own context stays free to reason and collaborate with the user. Prefer parallel delegation for independent work. Do trivial, surgical, and last-mile work directly; delegate the rest rather than doing it yourself and filling your own context.\n\nAim high. Default to radical simplicity and a relentless focus on the user's real experience: design for the person and the job to be done, not the demo. Reason about the whole system from first principles, anticipating scale and the long arc rather than patching the surface. Work backwards from the outcome the user actually needs. Question every assumption and prefer what you can derive, reproduce, or test.\n\nEngineering excellence. When making technical decisions, give little weight to development cost; prefer quality, simplicity, robustness, scalability, and long-term maintainability. Fix a bug by first reproducing it end to end, as close to how a real user hits it as you can, so you solve the real problem and not a symptom. When testing a product end to end, be picky about the UI and obsessed with pixel perfection: if something clearly looks off, even when it is unrelated to your task, get it fixed along the way. Hold that same bar for the codebase itself: a lint error, a failing test, or a flaky test is worth fixing the moment you see it, whoever introduced it. Fold it into your current work rather than letting it derail the task the user actually asked for.";
3590
+ const OPERATING_DEFAULTS_DIRECTIVE = "## Operating defaults (apply when the user has not specified otherwise; the user's explicit direction and the domain's own standards always override)\n\nOrchestrate. Delegate research, implementation, review, and large reads to the right subagent, worker, or model (the `implementer`, `debugger`, and `qa-engineer` native subagents for bounded implementation, root-cause debugging, and review/testing respectively; worker-* agents for background non-blocking runs; Task subagents for parallel work; peer critics for review) so your own context stays free to reason and collaborate with the user. Prefer parallel delegation for independent work. Do trivial, surgical, and last-mile work directly; delegate the rest rather than doing it yourself and filling your own context. Delegate to a subagent by default whenever one can do the work without loss of quality: the main thread is where you think with and respond to the user, and its context window is a finite shared resource, so protect it by pushing heavy or exploratory work outward and keeping only the conclusions.\n\nAim high. Default to radical simplicity and a relentless focus on the user's real experience: design for the person and the job to be done, not the demo. Reason about the whole system from first principles, anticipating scale and the long arc rather than patching the surface. Work backwards from the outcome the user actually needs. Question every assumption and prefer what you can derive, reproduce, or test.\n\nEngineering excellence. When making technical decisions, give little weight to development cost; prefer quality, simplicity, robustness, scalability, and long-term maintainability. Fix a bug by first reproducing it end to end, as close to how a real user hits it as you can, so you solve the real problem and not a symptom. When testing a product end to end, be picky about the UI and obsessed with pixel perfection: if something clearly looks off, even when it is unrelated to your task, get it fixed along the way. Hold that same bar for the codebase itself: a lint error, a failing test, or a flaky test is worth fixing the moment you see it, whoever introduced it. Fold it into your current work rather than letting it derail the task the user actually asked for.";
3579
3591
  /**
3580
3592
  * Condensed digest of OPERATING_DEFAULTS_DIRECTIVE for the spawned session's
3581
3593
  * system prompt (--append-system-prompt). The FULL directive is prepended to
@@ -3583,7 +3595,7 @@ const OPERATING_DEFAULTS_DIRECTIVE = "## Operating defaults (apply when the user
3583
3595
  * keeps both behavioral directives at top salience without duplicating the full
3584
3596
  * ~310-token block in the context window every turn. Points to the full copy.
3585
3597
  */
3586
- const OPERATING_DEFAULTS_DIGEST = "## Operating defaults (the user's explicit direction and the domain's standards always override)\n\nOrchestrate: delegate research, implementation, review, and large reads to the right subagent, worker, or model, preferring parallel delegation for independent work, so your own context stays free to reason and collaborate with the user; do trivial, surgical, and last-mile work directly. Aim high: default to radical simplicity and the user's real experience, whole-system first-principles thinking that anticipates scale and the long arc, and working backwards from the outcome the user actually needs; question every assumption and prefer what you can derive, reproduce, or test. Engineering excellence: prefer quality and long-term maintainability over dev cost; reproduce bugs end to end before fixing; keep a pixel-perfect UI bar; fix any lint, test failure, or flake on sight without letting it derail the task at hand. The full statement of these defaults is in your CLAUDE.md project instructions.";
3598
+ const OPERATING_DEFAULTS_DIGEST = "## Operating defaults (the user's explicit direction and the domain's standards always override)\n\nOrchestrate: delegate research, implementation, review, and large reads to the right subagent, worker, or model by default wherever one can do the work without losing quality, preferring parallel delegation for independent work, to protect the main thread's finite context and keep it free for reasoning and interacting with the user; do trivial, surgical, and last-mile work directly. Aim high: default to radical simplicity and the user's real experience, whole-system first-principles thinking that anticipates scale and the long arc, and working backwards from the outcome the user actually needs; question every assumption and prefer what you can derive, reproduce, or test. Engineering excellence: prefer quality and long-term maintainability over dev cost; reproduce bugs end to end before fixing; keep a pixel-perfect UI bar; fix any lint, test failure, or flake on sight without letting it derail the task at hand. The full statement of these defaults is in your CLAUDE.md project instructions.";
3587
3599
  /**
3588
3600
  * Skip the helper if the user's `~/.claude/CLAUDE.md` (or, equivalently,
3589
3601
  * the would-be post-write file) has grown past this size.
@@ -4716,7 +4728,7 @@ function initProxyFromEnv() {
4716
4728
  //#endregion
4717
4729
  //#region package.json
4718
4730
  var name = "github-router";
4719
- var version$1 = "0.3.176";
4731
+ var version$1 = "0.3.178";
4720
4732
 
4721
4733
  //#endregion
4722
4734
  //#region src/lib/approval.ts
@@ -5799,13 +5811,21 @@ function parseDisableParallelToolUse(toolChoice) {
5799
5811
  if (!toolChoice || typeof toolChoice !== "object") return void 0;
5800
5812
  return toolChoice.disable_parallel_tool_use === true ? false : void 0;
5801
5813
  }
5802
- /** Default absent Anthropic `thinking` to high effort, clamped by the model.
5803
- * Returns undefined for a model that advertises NO `reasoning_effort` allowlist
5804
- * such a model may not support reasoning at all, so forcing an effort could
5805
- * 400; leaving it unset preserves the pre-default safe behavior for that case. */
5814
+ /** Default absent Anthropic `thinking` to an effort, clamped by the model.
5815
+ * OpenAI-frontier shim models (gpt-5.6-sol/gpt-5.5) default to xhigh so the
5816
+ * native gpt-5.6-sol subagents (implementer/debugger/qa-engineer) and any
5817
+ * `-m gpt-5.6-sol` main-loop session reason at max when the client sends no
5818
+ * `thinking` block; every other shim model stays high. The
5819
+ * `supported.includes("xhigh")` guard makes the intent explicit rather than
5820
+ * trusting clampEffort to silently degrade. Opt out with
5821
+ * `GH_ROUTER_DISABLE_FRONTIER_XHIGH_DEFAULT=1`. Returns undefined for a model
5822
+ * that advertises NO `reasoning_effort` allowlist (may not support reasoning;
5823
+ * forcing an effort could 400). Note: an explicit client `thinking` budget is
5824
+ * handled by parseReasoningEffort and is NOT affected by this default. */
5806
5825
  function defaultReasoningEffort(model) {
5807
5826
  const supported = model?.capabilities?.supports?.reasoning_effort;
5808
- return Array.isArray(supported) && supported.length > 0 ? clampEffort("high", supported) : void 0;
5827
+ if (!(Array.isArray(supported) && supported.length > 0)) return void 0;
5828
+ return clampEffort(parseBoolEnv(process.env.GH_ROUTER_DISABLE_FRONTIER_XHIGH_DEFAULT) !== true && model?.id != null && shimDefaultsToXhigh(model.id) && supported.includes("xhigh") ? "xhigh" : "high", supported);
5809
5829
  }
5810
5830
  /**
5811
5831
  * Map Anthropic `thinking` to a Responses reasoning effort, clamped to the
@@ -8499,7 +8519,7 @@ const claude = defineCommand({
8499
8519
  groupKeys,
8500
8520
  workerToolsAvailable: workerToolsEnabled(),
8501
8521
  browseAvailable: browseAgentEnabled(),
8502
- implementerModel: implementerSubagentModel()
8522
+ nativeSubagentModel: nativeSubagentModel()
8503
8523
  });
8504
8524
  state.peerMcpNonce = runtime.nonce;
8505
8525
  envVars.GH_ROUTER_HOOK_MCP_URL = serverUrl;
@@ -8696,7 +8716,6 @@ const claude = defineCommand({
8696
8716
  powerBrowseAvailable: state.powerBrowseEnabled,
8697
8717
  fleetAvailable: fleetToolsEnabled(),
8698
8718
  agentToolsAvailable: agentToolsEnabled(),
8699
- implementerAvailable: implementerSubagentModel() != null,
8700
8719
  groupKeys
8701
8720
  });
8702
8721
  peerAwarenessSnippet = peerSnippet;