github-router 0.3.37 → 0.3.39
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.
- package/dist/{lifecycle-DxRKANCV.js → lifecycle-CMnTe0W7.js} +2 -2
- package/dist/{lifecycle-3OXRVrtQ.js → lifecycle-Ho67_Rew.js} +20 -2
- package/dist/lifecycle-Ho67_Rew.js.map +1 -0
- package/dist/main.js +71 -49
- package/dist/main.js.map +1 -1
- package/dist/{paths-Cr2gfGiA.js → paths-C-GyxwCW.js} +2 -2
- package/dist/{paths-Cr2gfGiA.js.map → paths-C-GyxwCW.js.map} +1 -1
- package/dist/{paths-Cf3OVCaJ.js → paths-DZwqh1p5.js} +1 -1
- package/package.json +1 -1
- package/dist/lifecycle-3OXRVrtQ.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./paths-
|
|
2
|
-
import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, o as sweepStaleWorktreesAtBoot, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-
|
|
1
|
+
import "./paths-C-GyxwCW.js";
|
|
2
|
+
import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, o as sweepStaleWorktreesAtBoot, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-Ho67_Rew.js";
|
|
3
3
|
|
|
4
4
|
export { WorktreeRegistry, getInstanceUuid, recordWorkerRepo, registerExitHandlers, sweepRegistry, sweepStaleWorktreesAtBoot };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as writeRuntimeFileSecure, t as PATHS } from "./paths-
|
|
1
|
+
import { c as writeRuntimeFileSecure, t as PATHS } from "./paths-C-GyxwCW.js";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
@@ -110,6 +110,24 @@ function sweepRegistry() {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
+
* Windows ConPTY / node-pty signal behavior:
|
|
114
|
+
*
|
|
115
|
+
* When a ConPTY host (VS Code terminal, Windows Terminal, node-pty) closes
|
|
116
|
+
* the pseudo-console, the ConPTY layer sends CTRL_CLOSE_EVENT to the
|
|
117
|
+
* process group. Node.js translates this into SIGINT (NOT SIGTERM). The
|
|
118
|
+
* process has a ~5-second window before forced termination.
|
|
119
|
+
*
|
|
120
|
+
* Implication: the SIGTERM handler below may NEVER fire in node-pty
|
|
121
|
+
* environments. This is by design — the three-layer cleanup architecture
|
|
122
|
+
* ensures coverage:
|
|
123
|
+
* 1. Per-call cleanup (engine.ts finally block) — happy path
|
|
124
|
+
* 2. SIGINT handler (this file) — ConPTY close, Ctrl+C
|
|
125
|
+
* 3. `exit` handler (this file) — unconditional, fires on any exit
|
|
126
|
+
* 4. Boot-time PID+instance sweep (sweepStaleWorktreesAtBoot) — crash recovery
|
|
127
|
+
*
|
|
128
|
+
* Layers 1+2+3 cover ConPTY; layer 4 covers SIGKILL/OOM/container restart.
|
|
129
|
+
*/
|
|
130
|
+
/**
|
|
113
131
|
* Wire up SIGINT/SIGTERM/exit handlers that walk the registry and
|
|
114
132
|
* remove every entry. Idempotent: subsequent calls swap the registry
|
|
115
133
|
* pointer but do NOT register additional process listeners (otherwise
|
|
@@ -289,4 +307,4 @@ async function sweepStaleWorktreesAtBoot() {
|
|
|
289
307
|
|
|
290
308
|
//#endregion
|
|
291
309
|
export { sweepRegistry as a, registerExitHandlers as i, getInstanceUuid as n, sweepStaleWorktreesAtBoot as o, recordWorkerRepo as r, WorktreeRegistry as t };
|
|
292
|
-
//# sourceMappingURL=lifecycle-
|
|
310
|
+
//# sourceMappingURL=lifecycle-Ho67_Rew.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle-Ho67_Rew.js","names":["_instanceUuid: string | null","_activeRegistry: WorktreeRegistry | null","_exitHandler: (() => void) | null","_sigintHandler: (() => void) | null","_sigtermHandler: (() => void) | null","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,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as writeRuntimeFileSecure, i as removeOwnClaudeConfigMirror, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS } from "./paths-
|
|
3
|
-
import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-
|
|
2
|
+
import { c as writeRuntimeFileSecure, i as removeOwnClaudeConfigMirror, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS } from "./paths-C-GyxwCW.js";
|
|
3
|
+
import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-Ho67_Rew.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
6
6
|
import consola from "consola";
|
|
@@ -464,7 +464,7 @@ function resolveModel(modelId) {
|
|
|
464
464
|
const oneMs = models.filter((m) => m.id.includes("opus") && /-1m(?:$|-)/.test(m.id));
|
|
465
465
|
const versionMatch = lower.match(/opus-(\d+)[.-](\d+)/);
|
|
466
466
|
const requestedVersion = versionMatch ? `${versionMatch[1]}.${versionMatch[2]}` : void 0;
|
|
467
|
-
const oneM = (requestedVersion ? oneMs.find((m) => m.id.includes(`opus-${requestedVersion}-`)) : void 0) ?? oneMs[0];
|
|
467
|
+
const oneM = (requestedVersion ? oneMs.find((m) => m.id.includes(`opus-${requestedVersion}-`)) : void 0) ?? (requestedVersion ? void 0 : oneMs[0]);
|
|
468
468
|
if (oneM) return oneM.id;
|
|
469
469
|
}
|
|
470
470
|
if (lower.includes("codex")) {
|
|
@@ -1553,7 +1553,11 @@ function buildExpansionPattern(variants) {
|
|
|
1553
1553
|
return "(?:" + variants.join("|") + ")";
|
|
1554
1554
|
}
|
|
1555
1555
|
function buildRgArgs(input) {
|
|
1556
|
-
const args = [
|
|
1556
|
+
const args = [
|
|
1557
|
+
"--json",
|
|
1558
|
+
"--no-binary",
|
|
1559
|
+
"--no-follow"
|
|
1560
|
+
];
|
|
1557
1561
|
if (input.contextLines > 0) args.push(`-C`, String(input.contextLines));
|
|
1558
1562
|
if (!input.expansionPattern && (input.mode === "literal" || input.mode === "ranked")) args.push("-F");
|
|
1559
1563
|
if (input.fileGlob && input.fileGlob !== "**/*") args.push("-g", input.fileGlob);
|
|
@@ -1596,6 +1600,7 @@ async function parseRgJsonStream(child, opts) {
|
|
|
1596
1600
|
cancelled: false,
|
|
1597
1601
|
stdoutBytes: 0
|
|
1598
1602
|
};
|
|
1603
|
+
child.stdout.setEncoding("utf8");
|
|
1599
1604
|
const rl = createInterface({
|
|
1600
1605
|
input: child.stdout,
|
|
1601
1606
|
crlfDelay: Infinity
|
|
@@ -1615,10 +1620,11 @@ async function parseRgJsonStream(child, opts) {
|
|
|
1615
1620
|
killChild(child);
|
|
1616
1621
|
break;
|
|
1617
1622
|
}
|
|
1618
|
-
|
|
1623
|
+
const line = rawLine.includes("\0") ? rawLine.replace(/\0/g, "") : rawLine;
|
|
1624
|
+
if (line.length === 0) continue;
|
|
1619
1625
|
let evt;
|
|
1620
1626
|
try {
|
|
1621
|
-
evt = JSON.parse(
|
|
1627
|
+
evt = JSON.parse(line);
|
|
1622
1628
|
} catch {
|
|
1623
1629
|
continue;
|
|
1624
1630
|
}
|
|
@@ -3191,7 +3197,7 @@ function logAudit$1(record) {
|
|
|
3191
3197
|
try {
|
|
3192
3198
|
const fs$2 = await import("node:fs/promises");
|
|
3193
3199
|
const path$2 = await import("node:path");
|
|
3194
|
-
const { PATHS: PATHS$1 } = await import("./paths-
|
|
3200
|
+
const { PATHS: PATHS$1 } = await import("./paths-DZwqh1p5.js");
|
|
3195
3201
|
const dir = path$2.join(PATHS$1.APP_DIR, "browser-mcp");
|
|
3196
3202
|
await fs$2.mkdir(dir, { recursive: true });
|
|
3197
3203
|
const line = JSON.stringify({
|
|
@@ -8236,6 +8242,33 @@ function resolvePathOrThrow(rawPath, workspace, opts = {}) {
|
|
|
8236
8242
|
* with the truncated text + a flag. The caller appends the
|
|
8237
8243
|
* truncation marker to the formatted output.
|
|
8238
8244
|
*/
|
|
8245
|
+
/**
|
|
8246
|
+
* Platform-aware kill for child processes. On Windows `child.kill()`
|
|
8247
|
+
* does NOT reliably terminate descendant processes — we use
|
|
8248
|
+
* `taskkill /T /F /PID` instead (same pattern as `bash.ts:killProcessTree`
|
|
8249
|
+
* and `code-search.ts:killChild`). EBUSY is swallowed because taskkill
|
|
8250
|
+
* occasionally races with the child's own teardown.
|
|
8251
|
+
*/
|
|
8252
|
+
function killChildTree(child) {
|
|
8253
|
+
if (!child.pid || child.killed) return;
|
|
8254
|
+
if (process$1.platform === "win32") {
|
|
8255
|
+
try {
|
|
8256
|
+
spawnSync("taskkill", [
|
|
8257
|
+
"/T",
|
|
8258
|
+
"/F",
|
|
8259
|
+
"/PID",
|
|
8260
|
+
String(child.pid)
|
|
8261
|
+
], {
|
|
8262
|
+
stdio: "ignore",
|
|
8263
|
+
windowsHide: true
|
|
8264
|
+
});
|
|
8265
|
+
} catch {}
|
|
8266
|
+
return;
|
|
8267
|
+
}
|
|
8268
|
+
try {
|
|
8269
|
+
child.kill("SIGTERM");
|
|
8270
|
+
} catch {}
|
|
8271
|
+
}
|
|
8239
8272
|
async function runRipgrep(args, cwd, signal) {
|
|
8240
8273
|
const RG_STDOUT_CAP = 10 * 1024 * 1024;
|
|
8241
8274
|
const { rgPath } = resolveRipgrep();
|
|
@@ -8262,9 +8295,7 @@ async function runRipgrep(args, cwd, signal) {
|
|
|
8262
8295
|
let truncated = false;
|
|
8263
8296
|
let settled = false;
|
|
8264
8297
|
const onAbort = () => {
|
|
8265
|
-
if (child.pid && !child.killed)
|
|
8266
|
-
child.kill("SIGTERM");
|
|
8267
|
-
} catch {}
|
|
8298
|
+
if (child.pid && !child.killed) killChildTree(child);
|
|
8268
8299
|
};
|
|
8269
8300
|
if (signal.aborted) onAbort();
|
|
8270
8301
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -8277,9 +8308,7 @@ async function runRipgrep(args, cwd, signal) {
|
|
|
8277
8308
|
stdoutBytes += slice.length;
|
|
8278
8309
|
if (chunk.length > room) {
|
|
8279
8310
|
truncated = true;
|
|
8280
|
-
if (child.pid && !child.killed)
|
|
8281
|
-
child.kill("SIGTERM");
|
|
8282
|
-
} catch {}
|
|
8311
|
+
if (child.pid && !child.killed) killChildTree(child);
|
|
8283
8312
|
}
|
|
8284
8313
|
});
|
|
8285
8314
|
child.stderr?.setEncoding("utf8");
|
|
@@ -9134,7 +9163,7 @@ async function createWorktree(workspaceAbs, opts) {
|
|
|
9134
9163
|
"HEAD"
|
|
9135
9164
|
])).stdout;
|
|
9136
9165
|
} catch {}
|
|
9137
|
-
const lineCount = stat$1.split(
|
|
9166
|
+
const lineCount = stat$1.split(/\r?\n/).filter((l) => l.length > 0).length;
|
|
9138
9167
|
return `[diff truncated at 256KB; ${Math.max(0, lineCount - 1)} files changed]\n${stat$1}`;
|
|
9139
9168
|
};
|
|
9140
9169
|
return {
|
|
@@ -9869,7 +9898,7 @@ const PERSONAS_READ = Object.freeze([
|
|
|
9869
9898
|
toolNameHttp: "codex_critic",
|
|
9870
9899
|
model: "gpt-5.5",
|
|
9871
9900
|
endpoint: "/v1/responses",
|
|
9872
|
-
description: "Adversarial second opinion on plans, designs, or code tradeoffs. Backed by gpt-5.5 (OpenAI) — different lab than Opus. Pass artifact verbatim.",
|
|
9901
|
+
description: "Adversarial second opinion on plans, designs, or code tradeoffs. Backed by gpt-5.5 (OpenAI, 400K context) — strongest reasoning model in the critic lineup, different lab than Opus. Best for architecture decisions, design reviews, and tradeoff analysis where cross-lab diversity matters. Not for line-level code review (use codex_reviewer). Pass artifact verbatim.",
|
|
9873
9902
|
baseInstructions: CRITIC_BASE,
|
|
9874
9903
|
agentPrompt: "",
|
|
9875
9904
|
writeCapable: false,
|
|
@@ -9887,7 +9916,7 @@ const PERSONAS_READ = Object.freeze([
|
|
|
9887
9916
|
toolNameHttp: "gemini_critic",
|
|
9888
9917
|
model: "gemini-3.1-pro-preview",
|
|
9889
9918
|
endpoint: "/v1/chat/completions",
|
|
9890
|
-
description: "Adversarial second opinion. Backed by gemini-3.1-pro (Google) — third-lab triangulation, strong on
|
|
9919
|
+
description: "Adversarial second opinion. Backed by gemini-3.1-pro (Google) — third-lab triangulation, strong on formal reasoning, proofs, and invariants. Useful for cross-checking findings from codex_critic or codex_reviewer when you want a third perspective. Pass artifact verbatim.",
|
|
9891
9920
|
baseInstructions: GEMINI_CRITIC_BASE,
|
|
9892
9921
|
agentPrompt: "",
|
|
9893
9922
|
writeCapable: false,
|
|
@@ -9905,7 +9934,7 @@ const PERSONAS_READ = Object.freeze([
|
|
|
9905
9934
|
toolNameHttp: "codex_reviewer",
|
|
9906
9935
|
model: "gpt-5.3-codex",
|
|
9907
9936
|
endpoint: "/v1/responses",
|
|
9908
|
-
description: "Line-level review of a concrete diff or single file. Backed by gpt-5.3-codex (OpenAI) — code-specialist,
|
|
9937
|
+
description: "Line-level review of a concrete diff or single file. Backed by gpt-5.3-codex (OpenAI, 400K context) — code-specialist, fastest critic (~16s). Surfaces bugs, edge cases, security issues, and idiom violations at specific line numbers. Not suited for architecture or design review (use codex_critic for plans). Pass artifact verbatim.",
|
|
9909
9938
|
baseInstructions: REVIEWER_BASE,
|
|
9910
9939
|
agentPrompt: "",
|
|
9911
9940
|
writeCapable: false,
|
|
@@ -9923,7 +9952,7 @@ const PERSONAS_READ = Object.freeze([
|
|
|
9923
9952
|
toolNameHttp: "opus_critic",
|
|
9924
9953
|
model: "claude-opus-4-7",
|
|
9925
9954
|
endpoint: "/v1/messages",
|
|
9926
|
-
description: "Adversarial second opinion from a fresh-context Opus 4.7 —
|
|
9955
|
+
description: "Adversarial second opinion from a fresh-context Opus 4.7 — same lab as the lead, limited blind-spot diversity vs cross-lab critics, but has the largest context window (up to 1M tokens on enterprise tiers). Handles large artifacts without decomposition. Fast (~22s), catches confabulation and motivated reasoning. Pass artifact verbatim.",
|
|
9927
9956
|
baseInstructions: OPUS_CRITIC_BASE,
|
|
9928
9957
|
agentPrompt: "",
|
|
9929
9958
|
writeCapable: false,
|
|
@@ -10000,41 +10029,33 @@ function buildAgentPrompt(persona, opts) {
|
|
|
10000
10029
|
}
|
|
10001
10030
|
/**
|
|
10002
10031
|
* Build the awareness snippet appended to the spawned `claude` session's
|
|
10003
|
-
* system prompt via `--append-system-prompt`.
|
|
10004
|
-
*
|
|
10005
|
-
* to Claude's judgment
|
|
10006
|
-
*
|
|
10007
|
-
*
|
|
10008
|
-
*
|
|
10009
|
-
*
|
|
10010
|
-
*
|
|
10011
|
-
*
|
|
10012
|
-
* - the "subagents you spawn inherit these" claim (the load-bearing
|
|
10013
|
-
* UX payoff of the holistic subagent-MCP-inheritance fix),
|
|
10014
|
-
* - the worker-tools "offload to save your context" framing (the
|
|
10015
|
-
* per-tool MCP descriptions cover capabilities; the snippet adds
|
|
10016
|
-
* the strategic when-to-use signal).
|
|
10032
|
+
* system prompt via `--append-system-prompt`. Descriptive awareness layer
|
|
10033
|
+
* — Claude sees what tools exist and their strategic value; *when* to
|
|
10034
|
+
* invoke is left to Claude's judgment informed by each tool's own
|
|
10035
|
+
* `description` field.
|
|
10036
|
+
*
|
|
10037
|
+
* Per Anthropic's guidance for Opus 4.8: tool descriptions carry the
|
|
10038
|
+
* routing signal (when/when-not); the system prompt should describe
|
|
10039
|
+
* capabilities in prose, not encode prescriptive decision trees. Opus 4.8
|
|
10040
|
+
* is responsive enough to overtrigger on aggressive routing language.
|
|
10017
10041
|
*
|
|
10018
10042
|
* Surface contract (regression-pinned in tests/peer-mcp-personas.test.ts):
|
|
10019
10043
|
* - Always lists codex_critic, codex_reviewer, opus_critic, advisor,
|
|
10020
10044
|
* peer-review-coordinator, and the subagent-inheritance fact.
|
|
10021
10045
|
* - Conditionally lists gemini_critic only when `geminiAvailable`.
|
|
10022
10046
|
* - Mentions `codex-cli` stdio bridge only when `codexCli`.
|
|
10023
|
-
*
|
|
10024
|
-
* The snippet is the awareness layer; the auto-invocation triggers
|
|
10025
|
-
* (CALL BEFORE / CALL AFTER) remain in each MCP tool's own `description`.
|
|
10026
|
-
* The two layers are intentionally complementary — keep the snippet
|
|
10027
|
-
* terse and never re-encode the prescriptive triggers here.
|
|
10028
10047
|
*/
|
|
10029
10048
|
function buildPeerAwarenessSnippet(opts) {
|
|
10030
10049
|
const criticList = ["`codex_critic` (gpt-5.5)", "`codex_reviewer` (gpt-5.3-codex)"];
|
|
10031
10050
|
if (opts.geminiAvailable) criticList.push("`gemini_critic` (gemini-3.1-pro)");
|
|
10032
10051
|
criticList.push("`opus_critic` (Opus 4.7)");
|
|
10033
|
-
const codexCliClause = opts.codexCli ? "
|
|
10052
|
+
const codexCliClause = opts.codexCli ? " `mcp__codex-cli__codex` dispatches to `codex-implementer` (gpt-5.3-codex with workspace-write) for end-to-end coding tasks." : "";
|
|
10034
10053
|
return [
|
|
10035
10054
|
"## Peer review and advisor",
|
|
10036
10055
|
"",
|
|
10037
|
-
`Cross-lab peer critics under \`mcp__gh-router-peers__*\` — ${criticList.join(", ")} —
|
|
10056
|
+
`Cross-lab peer critics under \`mcp__gh-router-peers__*\` — ${criticList.join(", ")} — are available at your discretion for adversarial review. Each tool's description explains its scope and when it applies. The \`peer-review-coordinator\` subagent fans out to the appropriate critics in parallel and aggregates findings by severity. Claude Code's built-in \`advisor\` tool catches approach drift and confabulation. Subagents you spawn inherit all of these.${codexCliClause}`,
|
|
10057
|
+
"",
|
|
10058
|
+
`\`code_search\` provides accurate ranked code discovery (BM25F + tree-sitter) — multiple parallel calls with different queries triangulate faster than sequential Grep. \`web_search\` surfaces citable sources for docs, errors, and upstream issues. \`worker_explore\` and \`worker_implement\` delegate bounded work to an autonomous Gemini worker, preserving your context; use \`worktree: true\` on \`worker_implement\` for isolated diffs. \`stand_in\` provides three-lab consensus for decision tiebreak when the user is unavailable.`
|
|
10038
10059
|
].join("\n");
|
|
10039
10060
|
}
|
|
10040
10061
|
/** Convenience: every persona that should be registered for the given mode. */
|
|
@@ -10047,7 +10068,7 @@ function personasFor(opts) {
|
|
|
10047
10068
|
if (opts.codexCli) for (const p of PERSONAS_WRITE) result.push(p);
|
|
10048
10069
|
return result;
|
|
10049
10070
|
}
|
|
10050
|
-
const WEB_SEARCH_DESCRIPTION = "Web search via GitHub Copilot's MCP. Prefer over Claude Code's built-in WebSearch — surfaces source URLs you can cite.";
|
|
10071
|
+
const WEB_SEARCH_DESCRIPTION = "Web search via GitHub Copilot's MCP. Prefer over Claude Code's built-in WebSearch — surfaces source URLs you can cite. Use for API documentation lookups, error message diagnosis, upstream issue searches, and verifying claims against current sources. Returns content with reference links.";
|
|
10051
10072
|
/**
|
|
10052
10073
|
* Format a `searchWeb()` result as an MCP-friendly text block. Mirrors
|
|
10053
10074
|
* the legacy inject format that `injectWebSearchIfNeeded` produces and
|
|
@@ -10103,7 +10124,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
10103
10124
|
},
|
|
10104
10125
|
{
|
|
10105
10126
|
toolNameHttp: "code_search",
|
|
10106
|
-
description: "Fast structured code search over a local workspace. Returns ranked, deduplicated hits with snippets. Ranks with BM25F across matched-line / file-path / surrounding-context / symbol-context fields, then refines `symbol-context` with tree-sitter AST analysis on the top hits so identifier definitions outrank incidental string matches. Prefer this over Grep/Bash+grep for ranked discovery (\"where is X defined\", \"which files reference Y\", \"find code that does Z\") — ranked mode surfaces the few right answers instead of every match. Use Grep for exact-pattern enumeration when you need every hit unranked, and Glob for file-name patterns (no content match). `workspace` is any absolute path the proxy process can read — typically the project root or a sub-tree you're working in.",
|
|
10127
|
+
description: "Fast structured code search over a local workspace. Returns ranked, deduplicated hits with snippets. Ranks with BM25F across matched-line / file-path / surrounding-context / symbol-context fields, then refines `symbol-context` with tree-sitter AST analysis on the top hits so identifier definitions outrank incidental string matches. Launch multiple code_search calls in parallel to triangulate — e.g. definition + callers + tests in one round-trip. Prefer this over Grep/Bash+grep for ranked discovery (\"where is X defined\", \"which files reference Y\", \"find code that does Z\") — ranked mode surfaces the few right answers instead of every match. Use Grep for exact-pattern enumeration when you need every hit unranked, and Glob for file-name patterns (no content match). `workspace` is any absolute path the proxy process can read — typically the project root or a sub-tree you're working in.",
|
|
10107
10128
|
inputSchema: {
|
|
10108
10129
|
type: "object",
|
|
10109
10130
|
required: ["query", "workspace"],
|
|
@@ -10193,7 +10214,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
10193
10214
|
{
|
|
10194
10215
|
toolNameHttp: "worker_explore",
|
|
10195
10216
|
capability: "worker",
|
|
10196
|
-
description: "Read-only investigation by an autonomous worker (Gemini via Pi). Tools: read, glob, grep, code_search, web_search, fetch_url, peer_review, advisor.
|
|
10217
|
+
description: "Read-only investigation by an autonomous worker (Gemini via Pi). Tools: read, glob, grep, code_search, web_search, fetch_url, peer_review, advisor. Offloads bounded research that would otherwise eat your context window — the worker plans its own tool calls and returns a single text answer. Examples: \"find files matching X then summarize\", \"how does library Y handle Z\", \"survey this codebase for usages of deprecated API\".",
|
|
10197
10218
|
inputSchema: {
|
|
10198
10219
|
type: "object",
|
|
10199
10220
|
required: ["prompt"],
|
|
@@ -10592,15 +10613,16 @@ function buildCoordinatorAgent(opts) {
|
|
|
10592
10613
|
"",
|
|
10593
10614
|
"The lead's brief will include an artifact (plan, design, diff, or code) and a goal (e.g. 'review before exit-plan', 'review the commit I just made', 'cross-check codex-critic's verdict'). Pick the right peers for the artifact type:",
|
|
10594
10615
|
"",
|
|
10595
|
-
"- **Plan / design / architecture choice** → fan out to `codex-critic`" + (opts.geminiAvailable ? " AND `gemini-critic` in parallel" : "") + ". codex-reviewer is the wrong tool for plans (it's a code-specialist, not an architecture critic).",
|
|
10596
|
-
"- **Concrete diff or single file** → fan out to `codex-reviewer`" + (opts.geminiAvailable ? " AND `gemini-critic`
|
|
10597
|
-
"- **
|
|
10598
|
-
"- **
|
|
10599
|
-
"- **
|
|
10616
|
+
"- **Plan / design / architecture choice** → fan out to `codex-critic` (gpt-5.5, strongest reasoning, cross-lab)" + (opts.geminiAvailable ? " AND `gemini-critic` (third-lab triangulation, strong on formal reasoning) in parallel" : "") + ". codex-reviewer is the wrong tool for plans (it's a code-specialist, not an architecture critic).",
|
|
10617
|
+
"- **Concrete diff or single file** → fan out to `codex-reviewer` (gpt-5.3-codex, line-level code specialist, fastest at ~16s)" + (opts.geminiAvailable ? " AND `gemini-critic` for cross-lab triangulation" : "") + ". For very small changes (<20 lines), one `codex-reviewer` call is enough.",
|
|
10618
|
+
"- **Large artifact (>50 KB)** → prefer `opus-critic` (Opus 4.7, up to 1M context — the largest window in the lineup, no decomposition needed for most artifacts). For cross-lab diversity on large artifacts, pair with `codex-critic` and decompose the artifact into 2-4 semantic batches for codex.",
|
|
10619
|
+
"- **Formal reasoning, proofs, or invariants** → prefer `gemini-critic`" + (opts.geminiAvailable ? " (gemini-3.1-pro, strong on math and formally-stated properties)" : " (NOT REGISTERED in this session — gemini-3.x not in catalog)") + ".",
|
|
10620
|
+
"- **Tie-breaker after codex-critic has weighed in** → call `gemini-critic`" + (opts.geminiAvailable ? "" : " (NOT REGISTERED in this session)") + " or `opus-critic` with the artifact AND codex-critic's verdict for cross-check.",
|
|
10621
|
+
"- **Fast sanity check** → `opus-critic` (~22s, same lab as lead but fresh context — catches confabulation and motivated reasoning).",
|
|
10600
10622
|
"",
|
|
10601
10623
|
"## Decomposition for large artifacts",
|
|
10602
10624
|
"",
|
|
10603
|
-
"Each per-call MCP wait is bounded (~60s SDK default on Claude Code v2.1.113+ per regressions #50289 / #52137 — empirically reproduced 2026-05-14). The proxy enforces per-persona effort allowlists AND a pre-flight `predictedTooLong` cap (codex_critic@high >8 KB, codex_reviewer@high >12 KB, opus_critic@medium >6 KB) to surface would-be-timeouts as fast actionable errors. For artifacts that exceed the cap, split into 2-4 logical batches BY CONCERN (not by raw size — semantic batches give better per-batch reviews) and call peers in parallel. The proxy's MCP cap allows up to 8 in-flight calls. Aggregate findings yourself before reporting back.",
|
|
10625
|
+
"Each per-call MCP wait is bounded (~60s SDK default on Claude Code v2.1.113+ per regressions #50289 / #52137 — empirically reproduced 2026-05-14). The proxy enforces per-persona effort allowlists AND a pre-flight `predictedTooLong` cap (codex_critic@high >8 KB, codex_reviewer@high >12 KB, opus_critic@medium >6 KB) to surface would-be-timeouts as fast actionable errors. For artifacts that exceed the cap but fit within opus-critic's context window (up to 1M tokens when available), route the full artifact to opus-critic. Otherwise, split into 2-4 logical batches BY CONCERN (not by raw size — semantic batches give better per-batch reviews) and call peers in parallel. The proxy's MCP cap allows up to 8 in-flight calls. Aggregate findings yourself before reporting back.",
|
|
10604
10626
|
"",
|
|
10605
10627
|
"## Aggregation contract",
|
|
10606
10628
|
"",
|
|
@@ -11098,7 +11120,7 @@ function initProxyFromEnv() {
|
|
|
11098
11120
|
//#endregion
|
|
11099
11121
|
//#region package.json
|
|
11100
11122
|
var name = "github-router";
|
|
11101
|
-
var version = "0.3.
|
|
11123
|
+
var version = "0.3.39";
|
|
11102
11124
|
|
|
11103
11125
|
//#endregion
|
|
11104
11126
|
//#region src/lib/approval.ts
|