github-router 0.3.292 → 0.3.295
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/{attribution-settings-CpLUCi8R.js → attribution-settings-pkIop1NU.js} +69 -30
- package/dist/attribution-settings-pkIop1NU.js.map +1 -0
- package/dist/{auth-BwUHopJz.js → auth-CluG5e-l.js} +3 -3
- package/dist/{auth-BwUHopJz.js.map → auth-CluG5e-l.js.map} +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{check-usage-BTda5753.js → check-usage-iOzfnKJe.js} +4 -4
- package/dist/{check-usage-BTda5753.js.map → check-usage-iOzfnKJe.js.map} +1 -1
- package/dist/{claude-_DYGKCw8.js → claude-DNmxk-Rm.js} +162 -42
- package/dist/claude-DNmxk-Rm.js.map +1 -0
- package/dist/{codex-rTJ8jW5G.js → codex-Dl4gpHn-.js} +5 -5
- package/dist/{codex-rTJ8jW5G.js.map → codex-Dl4gpHn-.js.map} +1 -1
- package/dist/{debug-B3UrZTHQ.js → debug-DsEwleGg.js} +2 -2
- package/dist/{debug-B3UrZTHQ.js.map → debug-DsEwleGg.js.map} +1 -1
- package/dist/engine-XnluJUs6.js +2 -0
- package/dist/{gate-discovery-Bjar5dgv.js → gate-discovery-WcduofL4.js} +5 -5
- package/dist/{gate-discovery-Bjar5dgv.js.map → gate-discovery-WcduofL4.js.map} +1 -1
- package/dist/{get-copilot-usage-CRrf1ZSC.js → get-copilot-usage-B7Z4XVWj.js} +2 -2
- package/dist/{get-copilot-usage-CRrf1ZSC.js.map → get-copilot-usage-B7Z4XVWj.js.map} +1 -1
- package/dist/hooks.mjs +180 -3
- package/dist/hooks.sha256 +1 -1
- package/dist/{internal-artifact-open-BsEQRvDi.js → internal-artifact-open-D6tKMg8v.js} +2 -2
- package/dist/{internal-artifact-open-BsEQRvDi.js.map → internal-artifact-open-D6tKMg8v.js.map} +1 -1
- package/dist/internal-fast-dispatch-guard-CPlCrTlU.js +2 -0
- package/dist/internal-fast-dispatch-guard-Xyu17k9X.js +187 -0
- package/dist/internal-fast-dispatch-guard-Xyu17k9X.js.map +1 -0
- package/dist/{internal-first-mate-guard-CVFFJriA.js → internal-first-mate-guard-C9BV7J4U.js} +3 -3
- package/dist/{internal-first-mate-guard-CVFFJriA.js.map → internal-first-mate-guard-C9BV7J4U.js.map} +1 -1
- package/dist/{internal-first-mate-guard-DJdX6t48.js → internal-first-mate-guard-WJApyc8j.js} +1 -1
- package/dist/{internal-plan-review-8TKDLco6.js → internal-plan-review-t3PuNfuL.js} +3 -3
- package/dist/{internal-plan-review-8TKDLco6.js.map → internal-plan-review-t3PuNfuL.js.map} +1 -1
- package/dist/{internal-prompt-submit-Da7pxqua.js → internal-prompt-submit-C3KovHQG.js} +4 -4
- package/dist/{internal-prompt-submit-Da7pxqua.js.map → internal-prompt-submit-C3KovHQG.js.map} +1 -1
- package/dist/{internal-session-bind-BOFytA1f.js → internal-session-bind-d2uoja8I.js} +2 -2
- package/dist/{internal-session-bind-BOFytA1f.js.map → internal-session-bind-d2uoja8I.js.map} +1 -1
- package/dist/{internal-stop-hook-DrX2xlj0.js → internal-stop-hook-DJFtGCsE.js} +5 -5
- package/dist/{internal-stop-hook-DrX2xlj0.js.map → internal-stop-hook-DJFtGCsE.js.map} +1 -1
- package/dist/{internal-stop-review-CdouacHL.js → internal-stop-review-YtDUGXpX.js} +2 -2
- package/dist/{internal-stop-review-CdouacHL.js.map → internal-stop-review-YtDUGXpX.js.map} +1 -1
- package/dist/{internal-worker-guard-Bx-itoP8.js → internal-worker-guard-CdkeS9PN.js} +2 -2
- package/dist/{internal-worker-guard-Bx-itoP8.js.map → internal-worker-guard-CdkeS9PN.js.map} +1 -1
- package/dist/{internal-workspace-header-8WT0iB5K.js → internal-workspace-header-tuBXJV22.js} +2 -2
- package/dist/{internal-workspace-header-8WT0iB5K.js.map → internal-workspace-header-tuBXJV22.js.map} +1 -1
- package/dist/{lifecycle-nuOHfwgj.js → lifecycle-Bg6doY3-.js} +2 -2
- package/dist/{lifecycle-nuOHfwgj.js.map → lifecycle-Bg6doY3-.js.map} +1 -1
- package/dist/lifecycle-CGLt1cJQ.js +2 -0
- package/dist/{lifecycle-LeSfa7wH.js → lifecycle-CM9eTzvk.js} +2 -2
- package/dist/{lifecycle-LeSfa7wH.js.map → lifecycle-CM9eTzvk.js.map} +1 -1
- package/dist/lifecycle-DoUwpVDB.js +2 -0
- package/dist/main.js +19 -18
- package/dist/main.js.map +1 -1
- package/dist/{mcp-workspace-header-q34H_4wL.js → mcp-workspace-header-dERl2YTT.js} +2 -2
- package/dist/{mcp-workspace-header-q34H_4wL.js.map → mcp-workspace-header-dERl2YTT.js.map} +1 -1
- package/dist/{models-hhJcrZhr.js → models-DZ4hYsR7.js} +3 -3
- package/dist/{models-hhJcrZhr.js.map → models-DZ4hYsR7.js.map} +1 -1
- package/dist/{orchestration-pzbrKkgD.js → orchestration-BiGCEwbH.js} +2 -2
- package/dist/{orchestration-pzbrKkgD.js.map → orchestration-BiGCEwbH.js.map} +1 -1
- package/dist/{paths-BH4J7slC.js → paths-Ci485qSJ.js} +4 -4
- package/dist/{paths-BH4J7slC.js.map → paths-Ci485qSJ.js.map} +1 -1
- package/dist/paths-GD7bgGGy.js +2 -0
- package/dist/{peer-mcp-personas-CHbl6MwM.js → peer-mcp-personas-BwNtC8jt.js} +411 -84
- package/dist/peer-mcp-personas-BwNtC8jt.js.map +1 -0
- package/dist/{plan-review-hook-CfcanA7_.js → plan-review-hook-DuW0pSoM.js} +3 -3
- package/dist/{plan-review-hook-CfcanA7_.js.map → plan-review-hook-DuW0pSoM.js.map} +1 -1
- package/dist/{prompt-submit-hook-Bqf9ORgb.js → prompt-submit-hook-IMl7ssfr.js} +3 -3
- package/dist/{prompt-submit-hook-Bqf9ORgb.js.map → prompt-submit-hook-IMl7ssfr.js.map} +1 -1
- package/dist/{provision-BYFd9nPK.js → provision-DeNqzvSM.js} +4 -4
- package/dist/{provision-BYFd9nPK.js.map → provision-DeNqzvSM.js.map} +1 -1
- package/dist/{self-invocation-DhO1Z8iD.js → self-invocation-DAB_od0C.js} +2 -2
- package/dist/{self-invocation-DhO1Z8iD.js.map → self-invocation-DAB_od0C.js.map} +1 -1
- package/dist/{serve-BWMxDLnD.js → serve-BS5EmCpM.js} +12 -12
- package/dist/{serve-BWMxDLnD.js.map → serve-BS5EmCpM.js.map} +1 -1
- package/dist/{server-setup-CqlaZukJ.js → server-setup-DsGJnJ_O.js} +398 -283
- package/dist/server-setup-DsGJnJ_O.js.map +1 -0
- package/dist/{start-5MgGT4IF.js → start-vJdt2MiM.js} +3 -3
- package/dist/{start-5MgGT4IF.js.map → start-vJdt2MiM.js.map} +1 -1
- package/dist/{stop-gate-hook-BiBp5aGm.js → stop-gate-hook-DQnh_KfI.js} +3 -3
- package/dist/{stop-gate-hook-BiBp5aGm.js.map → stop-gate-hook-DQnh_KfI.js.map} +1 -1
- package/dist/{stop-gate-policy-BGd6b5hR.js → stop-gate-policy-DMr3KVmw.js} +2 -2
- package/dist/{stop-gate-policy-BGd6b5hR.js.map → stop-gate-policy-DMr3KVmw.js.map} +1 -1
- package/dist/{token-8drORhXg.js → token-BtJhjXXu.js} +54 -7
- package/dist/token-BtJhjXXu.js.map +1 -0
- package/dist/{worker-dispatch-D5fGroNr.js → worker-dispatch-BEHkTbnH.js} +2 -2
- package/dist/{worker-dispatch-D5fGroNr.js.map → worker-dispatch-BEHkTbnH.js.map} +1 -1
- package/package.json +1 -1
- package/dist/attribution-settings-CpLUCi8R.js.map +0 -1
- package/dist/claude-_DYGKCw8.js.map +0 -1
- package/dist/engine-C9axTIu7.js +0 -2
- package/dist/lifecycle-C8fOsQke.js +0 -2
- package/dist/lifecycle-D4Yc1aap.js +0 -2
- package/dist/paths-DJZoXfAS.js +0 -2
- package/dist/peer-mcp-personas-CHbl6MwM.js.map +0 -1
- package/dist/server-setup-CqlaZukJ.js.map +0 -1
- package/dist/token-8drORhXg.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as sharedServerArgs, i as setupAndServe, n as getCodexEnvVars, r as parseSharedArgs, s as startKeepAwake, t as getClaudeCodeEnvVars
|
|
1
|
+
import { cn as provisionBrowserAssets, dn as provisionAndIndexColbert, ft as browserToolsEnabled, mn as warmTreeSitterPool, un as colbertDegradedWarning } from "./peer-mcp-personas-BwNtC8jt.js";
|
|
2
|
+
import { a as sharedServerArgs, d as runSelfUpdate, i as setupAndServe, n as getCodexEnvVars, r as parseSharedArgs, s as startKeepAwake, t as getClaudeCodeEnvVars } from "./server-setup-DsGJnJ_O.js";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import consola from "consola";
|
|
5
5
|
import process$1 from "node:process";
|
|
@@ -129,4 +129,4 @@ const start = defineCommand({
|
|
|
129
129
|
//#endregion
|
|
130
130
|
export { start };
|
|
131
131
|
|
|
132
|
-
//# sourceMappingURL=start-
|
|
132
|
+
//# sourceMappingURL=start-vJdt2MiM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-5MgGT4IF.js","names":["process"],"sources":["../src/lib/shell.ts","../src/start.ts"],"sourcesContent":["import process from \"node:process\"\n\ntype ShellName = \"bash\" | \"zsh\" | \"fish\" | \"powershell\" | \"cmd\" | \"sh\"\ntype EnvVars = Record<string, string | undefined>\n\nfunction getShell(): ShellName {\n const { platform, env } = process\n\n if (platform === \"win32\") {\n // Git Bash / MSYS2 / Cygwin set SHELL even on Windows\n if (env.SHELL) {\n if (env.SHELL.endsWith(\"zsh\")) return \"zsh\"\n if (env.SHELL.endsWith(\"fish\")) return \"fish\"\n if (env.SHELL.endsWith(\"bash\")) return \"bash\"\n return \"sh\"\n }\n\n // Windows PowerShell 5.x sets this\n if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return \"powershell\"\n\n // PowerShell (both 5.x and 7+/pwsh) adds user-scoped module paths\n // at runtime. The system-level PSModulePath in CMD lacks these paths.\n if (env.PSModulePath) {\n const lower = env.PSModulePath.toLowerCase()\n if (\n lower.includes(\"documents\\\\powershell\")\n || lower.includes(\"documents\\\\windowspowershell\")\n ) {\n return \"powershell\"\n }\n }\n\n return \"cmd\"\n }\n\n const shellPath = env.SHELL\n if (shellPath) {\n if (shellPath.endsWith(\"zsh\")) return \"zsh\"\n if (shellPath.endsWith(\"fish\")) return \"fish\"\n if (shellPath.endsWith(\"bash\")) return \"bash\"\n }\n\n return \"sh\"\n}\n\nfunction quotePosixValue(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`\n}\n\nfunction quotePowerShellValue(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Generates a copy-pasteable script to set multiple environment variables\n * and run a subsequent command.\n * @param {EnvVars} envVars - An object of environment variables to set.\n * @param {string} commandToRun - The command to run after setting the variables.\n * @returns {string} The formatted script string.\n */\nexport function generateEnvScript(\n envVars: EnvVars,\n commandToRun: string = \"\",\n): string {\n const shell = getShell()\n const filteredEnvVars = Object.entries(envVars).filter(\n ([, value]) => value !== undefined,\n ) as Array<[string, string]>\n\n let commandBlock: string\n\n switch (shell) {\n case \"powershell\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)\n .join(\"; \")\n break\n }\n case \"cmd\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set \"${key}=${value}\"`)\n .join(\" & \")\n break\n }\n case \"fish\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`)\n .join(\"; \")\n break\n }\n default: {\n // bash, zsh, sh\n const assignments = filteredEnvVars\n .map(([key, value]) => `${key}=${quotePosixValue(value)}`)\n .join(\" \")\n commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : \"\"\n break\n }\n }\n\n if (commandBlock && commandToRun) {\n const separator =\n shell === \"cmd\" ? \" & \" : shell === \"powershell\" ? \"; \" : \" && \"\n return `${commandBlock}${separator}${commandToRun}`\n }\n\n return commandBlock || commandToRun\n}\n","#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport clipboard from \"clipboardy\"\nimport consola from \"consola\"\n\nimport { generateEnvScript } from \"./lib/shell\"\nimport { DEFAULT_CODEX_MODEL, DEFAULT_PORT } from \"./lib/port\"\nimport { provisionBrowserAssets } from \"./lib/browser-mcp/provision\"\nimport { browserToolsEnabled } from \"./lib/mcp-capabilities\"\nimport { colbertDegradedWarning, provisionAndIndexColbert } from \"./lib/colbert\"\nimport { startKeepAwake } from \"./lib/keep-awake\"\nimport { warmTreeSitterPool } from \"./lib/tree-sitter-pool/pool\"\nimport {\n getClaudeCodeEnvVars,\n getCodexEnvVars,\n parseSharedArgs,\n setupAndServe,\n sharedServerArgs,\n} from \"./lib/server-setup\"\nimport { runSelfUpdate } from \"./lib/self-update\"\n\nfunction printAndCopyCommand(command: string, label: string): void {\n consola.box(`${label}\\n\\n${command}`)\n try {\n clipboard.writeSync(command)\n consola.success(`Copied ${label} command to clipboard!`)\n } catch {\n consola.warn(\"Failed to copy to clipboard. Copy the command above manually.\")\n }\n}\n\nfunction generateClaudeCodeCommand(serverUrl: string, model?: string) {\n const envVars = getClaudeCodeEnvVars(serverUrl, model)\n const command = generateEnvScript(envVars, \"claude --dangerously-skip-permissions\")\n printAndCopyCommand(command, \"Claude Code\")\n}\n\nfunction generateCodexCommand(serverUrl: string, model?: string) {\n const codexModel = model ?? DEFAULT_CODEX_MODEL\n const envVars = getCodexEnvVars(serverUrl)\n const command = generateEnvScript(envVars, `codex -m ${codexModel}`)\n printAndCopyCommand(command, \"Codex CLI\")\n}\n\nexport const start = defineCommand({\n meta: {\n name: \"start\",\n description: \"Start the github-router server\",\n },\n args: {\n ...sharedServerArgs,\n cc: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Claude Code with Copilot API config\",\n },\n cx: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Codex CLI with Copilot API config\",\n },\n model: {\n alias: \"m\",\n type: \"string\",\n description: \"Override the default model (used with --cc or --cx)\",\n },\n },\n async run({ args }) {\n const parsed = parseSharedArgs(args as unknown as Record<string, unknown>)\n\n const { serverUrl } = await setupAndServe({\n ...parsed,\n port: parsed.port ?? DEFAULT_PORT,\n silent: false,\n })\n\n // Best-effort self-update (detached, applies next launch). Runs\n // after the server is listening so the bounded probe can't delay it.\n void runSelfUpdate({ selfUpdate: args[\"self-update\"] !== false })\n\n // Best-effort ColBERT semantic-search provision + background index of\n // the launch cwd (if a git repo). ON by default; never blocks launch,\n // never throws. Opt out with GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1.\n void provisionAndIndexColbert()\n\n // Surface a terminally-failed semantic index to the HUMAN (see the note\n // in claude.ts). Fire-and-forget; lexical search still works.\n void colbertDegradedWarning()\n .then((warning) => {\n if (warning) process.stderr.write(`${warning}\\n`)\n })\n .catch(() => {})\n\n // Ready one tree-sitter worker in the background so the first code search\n // avoids WASM/grammar initialization. One worker keeps launch CPU bounded;\n // demand grows the pool lazily. Opt out with GH_ROUTER_DISABLE_TS_POOL_WARMUP=1.\n void warmTreeSitterPool()\n\n // Best-effort: keep the machine awake while the proxy serves (win32\n // default-on; opt out with GH_ROUTER_DISABLE_KEEP_AWAKE=1). Holds a\n // SetThreadExecutionState assertion via a persistent PowerShell\n // helper. Self-registers its own SIGINT/SIGTERM/exit reaper — load-\n // bearing here, because `start` has no launchChild/onShutdown.\n startKeepAwake()\n\n // Best-effort: materialize the browser extension + bridge into the\n // stable app-dir and stamp the running version, so a one-time \"Load\n // unpacked\" survives npx/bunx upgrades. Gated on --browse; never\n // blocks launch, never throws.\n if (browserToolsEnabled()) {\n void provisionBrowserAssets().catch((err) =>\n consola.debug(\"Browser extension provisioning failed:\", err),\n )\n }\n\n if (args.cc) generateClaudeCodeCommand(serverUrl, args.model)\n if (args.cx) generateCodexCommand(serverUrl, args.model)\n\n consola.box(\n `🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`,\n )\n },\n})\n"],"mappings":";;;;;;;AAKA,SAAS,WAAsB;CAC7B,MAAM,EAAE,UAAU,QAAQA;CAE1B,IAAI,aAAa,SAAS;EAExB,IAAI,IAAI,OAAO;GACb,IAAI,IAAI,MAAM,SAAS,KAAK,GAAG,OAAO;GACtC,IAAI,IAAI,MAAM,SAAS,MAAM,GAAG,OAAO;GACvC,IAAI,IAAI,MAAM,SAAS,MAAM,GAAG,OAAO;GACvC,OAAO;EACT;EAGA,IAAI,IAAI,iCAAiC,OAAO;EAIhD,IAAI,IAAI,cAAc;GACpB,MAAM,QAAQ,IAAI,aAAa,YAAY;GAC3C,IACE,MAAM,SAAS,uBAAuB,KACnC,MAAM,SAAS,8BAA8B,GAEhD,OAAO;EAEX;EAEA,OAAO;CACT;CAEA,MAAM,YAAY,IAAI;CACtB,IAAI,WAAW;EACb,IAAI,UAAU,SAAS,KAAK,GAAG,OAAO;EACtC,IAAI,UAAU,SAAS,MAAM,GAAG,OAAO;EACvC,IAAI,UAAU,SAAS,MAAM,GAAG,OAAO;CACzC;CAEA,OAAO;AACT;AAEA,SAAS,gBAAgB,OAAuB;CAC9C,OAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,EAAE;AAC1C;AAEA,SAAS,qBAAqB,OAAuB;CACnD,OAAO,IAAI,MAAM,QAAQ,MAAM,IAAI,EAAE;AACvC;;;;;;;;AASA,SAAgB,kBACd,SACA,eAAuB,IACf;CACR,MAAM,QAAQ,SAAS;CACvB,MAAM,kBAAkB,OAAO,QAAQ,OAAO,CAAC,CAAC,QAC7C,GAAG,WAAW,UAAU,KAAA,CAC3B;CAEA,IAAI;CAEJ,QAAQ,OAAR;EACE,KAAK;GACH,eAAe,gBACZ,KAAK,CAAC,KAAK,WAAW,QAAQ,IAAI,KAAK,qBAAqB,KAAK,GAAG,CAAC,CACrE,KAAK,IAAI;GACZ;EAEF,KAAK;GACH,eAAe,gBACZ,KAAK,CAAC,KAAK,WAAW,QAAQ,IAAI,GAAG,MAAM,EAAE,CAAC,CAC9C,KAAK,KAAK;GACb;EAEF,KAAK;GACH,eAAe,gBACZ,KAAK,CAAC,KAAK,WAAW,WAAW,IAAI,GAAG,gBAAgB,KAAK,GAAG,CAAC,CACjE,KAAK,IAAI;GACZ;EAEF,SAAS;GAEP,MAAM,cAAc,gBACjB,KAAK,CAAC,KAAK,WAAW,GAAG,IAAI,GAAG,gBAAgB,KAAK,GAAG,CAAC,CACzD,KAAK,GAAG;GACX,eAAe,gBAAgB,SAAS,IAAI,UAAU,gBAAgB;GACtE;EACF;CACF;CAEA,IAAI,gBAAgB,cAGlB,OAAO,GAAG,eADR,UAAU,QAAQ,QAAQ,UAAU,eAAe,OAAO,SACvB;CAGvC,OAAO,gBAAgB;AACzB;;;ACrFA,SAAS,oBAAoB,SAAiB,OAAqB;CACjE,QAAQ,IAAI,GAAG,MAAM,MAAM,SAAS;CACpC,IAAI;EACF,UAAU,UAAU,OAAO;EAC3B,QAAQ,QAAQ,UAAU,MAAM,uBAAuB;CACzD,QAAQ;EACN,QAAQ,KAAK,+DAA+D;CAC9E;AACF;AAEA,SAAS,0BAA0B,WAAmB,OAAgB;CAGpE,oBADgB,kBADA,qBAAqB,WAAW,KACd,GAAS,uCACjB,GAAG,aAAa;AAC5C;AAEA,SAAS,qBAAqB,WAAmB,OAAgB;CAC/D,MAAM,aAAa,SAAA;CAGnB,oBADgB,kBADA,gBAAgB,SACE,GAAS,YAAY,YAC7B,GAAG,WAAW;AAC1C;AAEA,MAAa,QAAQ,cAAc;CACjC,MAAM;EACJ,MAAM;EACN,aAAa;CACf;CACA,MAAM;EACJ,GAAG;EACH,IAAI;GACF,MAAM;GACN,SAAS;GACT,aACE;EACJ;EACA,IAAI;GACF,MAAM;GACN,SAAS;GACT,aACE;EACJ;EACA,OAAO;GACL,OAAO;GACP,MAAM;GACN,aAAa;EACf;CACF;CACA,MAAM,IAAI,EAAE,QAAQ;EAClB,MAAM,SAAS,gBAAgB,IAA0C;EAEzE,MAAM,EAAE,cAAc,MAAM,cAAc;GACxC,GAAG;GACH,MAAM,OAAO,QAAA;GACb,QAAQ;EACV,CAAC;EAID,cAAmB,EAAE,YAAY,KAAK,mBAAmB,MAAM,CAAC;EAKhE,yBAA8B;EAI9B,uBAA4B,CAAC,CAC1B,MAAM,YAAY;GACjB,IAAI,SAAS,QAAQ,OAAO,MAAM,GAAG,QAAQ,GAAG;EAClD,CAAC,CAAC,CACD,YAAY,CAAC,CAAC;EAKjB,mBAAwB;EAOxB,eAAe;EAMf,IAAI,oBAAoB,GACtB,uBAA4B,CAAC,CAAC,OAAO,QACnC,QAAQ,MAAM,0CAA0C,GAAG,CAC7D;EAGF,IAAI,KAAK,IAAI,0BAA0B,WAAW,KAAK,KAAK;EAC5D,IAAI,KAAK,IAAI,qBAAqB,WAAW,KAAK,KAAK;EAEvD,QAAQ,IACN,yFAAyF,UAAU,OACrG;CACF;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"start-vJdt2MiM.js","names":["process"],"sources":["../src/lib/shell.ts","../src/start.ts"],"sourcesContent":["import process from \"node:process\"\n\ntype ShellName = \"bash\" | \"zsh\" | \"fish\" | \"powershell\" | \"cmd\" | \"sh\"\ntype EnvVars = Record<string, string | undefined>\n\nfunction getShell(): ShellName {\n const { platform, env } = process\n\n if (platform === \"win32\") {\n // Git Bash / MSYS2 / Cygwin set SHELL even on Windows\n if (env.SHELL) {\n if (env.SHELL.endsWith(\"zsh\")) return \"zsh\"\n if (env.SHELL.endsWith(\"fish\")) return \"fish\"\n if (env.SHELL.endsWith(\"bash\")) return \"bash\"\n return \"sh\"\n }\n\n // Windows PowerShell 5.x sets this\n if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return \"powershell\"\n\n // PowerShell (both 5.x and 7+/pwsh) adds user-scoped module paths\n // at runtime. The system-level PSModulePath in CMD lacks these paths.\n if (env.PSModulePath) {\n const lower = env.PSModulePath.toLowerCase()\n if (\n lower.includes(\"documents\\\\powershell\")\n || lower.includes(\"documents\\\\windowspowershell\")\n ) {\n return \"powershell\"\n }\n }\n\n return \"cmd\"\n }\n\n const shellPath = env.SHELL\n if (shellPath) {\n if (shellPath.endsWith(\"zsh\")) return \"zsh\"\n if (shellPath.endsWith(\"fish\")) return \"fish\"\n if (shellPath.endsWith(\"bash\")) return \"bash\"\n }\n\n return \"sh\"\n}\n\nfunction quotePosixValue(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`\n}\n\nfunction quotePowerShellValue(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Generates a copy-pasteable script to set multiple environment variables\n * and run a subsequent command.\n * @param {EnvVars} envVars - An object of environment variables to set.\n * @param {string} commandToRun - The command to run after setting the variables.\n * @returns {string} The formatted script string.\n */\nexport function generateEnvScript(\n envVars: EnvVars,\n commandToRun: string = \"\",\n): string {\n const shell = getShell()\n const filteredEnvVars = Object.entries(envVars).filter(\n ([, value]) => value !== undefined,\n ) as Array<[string, string]>\n\n let commandBlock: string\n\n switch (shell) {\n case \"powershell\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)\n .join(\"; \")\n break\n }\n case \"cmd\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set \"${key}=${value}\"`)\n .join(\" & \")\n break\n }\n case \"fish\": {\n commandBlock = filteredEnvVars\n .map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`)\n .join(\"; \")\n break\n }\n default: {\n // bash, zsh, sh\n const assignments = filteredEnvVars\n .map(([key, value]) => `${key}=${quotePosixValue(value)}`)\n .join(\" \")\n commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : \"\"\n break\n }\n }\n\n if (commandBlock && commandToRun) {\n const separator =\n shell === \"cmd\" ? \" & \" : shell === \"powershell\" ? \"; \" : \" && \"\n return `${commandBlock}${separator}${commandToRun}`\n }\n\n return commandBlock || commandToRun\n}\n","#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport clipboard from \"clipboardy\"\nimport consola from \"consola\"\n\nimport { generateEnvScript } from \"./lib/shell\"\nimport { DEFAULT_CODEX_MODEL, DEFAULT_PORT } from \"./lib/port\"\nimport { provisionBrowserAssets } from \"./lib/browser-mcp/provision\"\nimport { browserToolsEnabled } from \"./lib/mcp-capabilities\"\nimport { colbertDegradedWarning, provisionAndIndexColbert } from \"./lib/colbert\"\nimport { startKeepAwake } from \"./lib/keep-awake\"\nimport { warmTreeSitterPool } from \"./lib/tree-sitter-pool/pool\"\nimport {\n getClaudeCodeEnvVars,\n getCodexEnvVars,\n parseSharedArgs,\n setupAndServe,\n sharedServerArgs,\n} from \"./lib/server-setup\"\nimport { runSelfUpdate } from \"./lib/self-update\"\n\nfunction printAndCopyCommand(command: string, label: string): void {\n consola.box(`${label}\\n\\n${command}`)\n try {\n clipboard.writeSync(command)\n consola.success(`Copied ${label} command to clipboard!`)\n } catch {\n consola.warn(\"Failed to copy to clipboard. Copy the command above manually.\")\n }\n}\n\nfunction generateClaudeCodeCommand(serverUrl: string, model?: string) {\n const envVars = getClaudeCodeEnvVars(serverUrl, model)\n const command = generateEnvScript(envVars, \"claude --dangerously-skip-permissions\")\n printAndCopyCommand(command, \"Claude Code\")\n}\n\nfunction generateCodexCommand(serverUrl: string, model?: string) {\n const codexModel = model ?? DEFAULT_CODEX_MODEL\n const envVars = getCodexEnvVars(serverUrl)\n const command = generateEnvScript(envVars, `codex -m ${codexModel}`)\n printAndCopyCommand(command, \"Codex CLI\")\n}\n\nexport const start = defineCommand({\n meta: {\n name: \"start\",\n description: \"Start the github-router server\",\n },\n args: {\n ...sharedServerArgs,\n cc: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Claude Code with Copilot API config\",\n },\n cx: {\n type: \"boolean\",\n default: false,\n description:\n \"Generate a command to launch Codex CLI with Copilot API config\",\n },\n model: {\n alias: \"m\",\n type: \"string\",\n description: \"Override the default model (used with --cc or --cx)\",\n },\n },\n async run({ args }) {\n const parsed = parseSharedArgs(args as unknown as Record<string, unknown>)\n\n const { serverUrl } = await setupAndServe({\n ...parsed,\n port: parsed.port ?? DEFAULT_PORT,\n silent: false,\n })\n\n // Best-effort self-update (detached, applies next launch). Runs\n // after the server is listening so the bounded probe can't delay it.\n void runSelfUpdate({ selfUpdate: args[\"self-update\"] !== false })\n\n // Best-effort ColBERT semantic-search provision + background index of\n // the launch cwd (if a git repo). ON by default; never blocks launch,\n // never throws. Opt out with GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1.\n void provisionAndIndexColbert()\n\n // Surface a terminally-failed semantic index to the HUMAN (see the note\n // in claude.ts). Fire-and-forget; lexical search still works.\n void colbertDegradedWarning()\n .then((warning) => {\n if (warning) process.stderr.write(`${warning}\\n`)\n })\n .catch(() => {})\n\n // Ready one tree-sitter worker in the background so the first code search\n // avoids WASM/grammar initialization. One worker keeps launch CPU bounded;\n // demand grows the pool lazily. Opt out with GH_ROUTER_DISABLE_TS_POOL_WARMUP=1.\n void warmTreeSitterPool()\n\n // Best-effort: keep the machine awake while the proxy serves (win32\n // default-on; opt out with GH_ROUTER_DISABLE_KEEP_AWAKE=1). Holds a\n // SetThreadExecutionState assertion via a persistent PowerShell\n // helper. Self-registers its own SIGINT/SIGTERM/exit reaper — load-\n // bearing here, because `start` has no launchChild/onShutdown.\n startKeepAwake()\n\n // Best-effort: materialize the browser extension + bridge into the\n // stable app-dir and stamp the running version, so a one-time \"Load\n // unpacked\" survives npx/bunx upgrades. Gated on --browse; never\n // blocks launch, never throws.\n if (browserToolsEnabled()) {\n void provisionBrowserAssets().catch((err) =>\n consola.debug(\"Browser extension provisioning failed:\", err),\n )\n }\n\n if (args.cc) generateClaudeCodeCommand(serverUrl, args.model)\n if (args.cx) generateCodexCommand(serverUrl, args.model)\n\n consola.box(\n `🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`,\n )\n },\n})\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as parseBoolEnv } from "./exec-y8C_MU8A.js";
|
|
2
|
-
import { t as buildSelfCommand } from "./self-invocation-
|
|
3
|
-
import { a as isSubagentContext, o as regressions } from "./stop-gate-policy-
|
|
2
|
+
import { t as buildSelfCommand } from "./self-invocation-DAB_od0C.js";
|
|
3
|
+
import { a as isSubagentContext, o as regressions } from "./stop-gate-policy-DMr3KVmw.js";
|
|
4
4
|
import { promises } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -736,4 +736,4 @@ async function injectStopHookIntoSettingsFile(settingsPath, command, event = "St
|
|
|
736
736
|
//#endregion
|
|
737
737
|
export { decideStopHook as a, launchBaselineKey as c, stopReviewEnabled as d, resolveSealedGate as f, captureLaunchBaseline as i, stopGateDisabled as l, runGateChecks as m, buildSessionBindHookCommand as n, fileBlockBudget as o, sealedGateIds as p, buildStopHookCommand as r, injectStopHookIntoSettingsFile as s, buildArtifactOpenHookCommand as t, stopGateId as u };
|
|
738
738
|
|
|
739
|
-
//# sourceMappingURL=stop-gate-hook-
|
|
739
|
+
//# sourceMappingURL=stop-gate-hook-DQnh_KfI.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stop-gate-hook-BiBp5aGm.js","names":["nodePath","fs"],"sources":["../src/lib/orchestration/gate-runner.ts","../src/lib/orchestration/gate-registry.ts","../src/lib/orchestration/gate-immutability.ts","../src/lib/orchestration/stop-gate.ts","../src/lib/orchestration/stop-gate-hook.ts"],"sourcesContent":["/**\n * The executable-gate runner — runs the kernel's SEALED check commands and\n * reports which passed, as a `GateOutcome` the selector compares. A \"check\" is a\n * category (e.g. `tests` → `bun test`, `types` → `tsc`, `lint` → eslint); the\n * commands are owned by the kernel/registry, never authored by a producer\n * (gate-immutability, invariant 5), so a candidate can only PASS them, not\n * rewrite them.\n *\n * The process exec is INJECTED (`ExecFn`) so this is pure + unit-testable; the\n * live adapter (Bun.spawn / the managed-exe helper in a sandboxed cwd) is a thin\n * wrapper. A check that exits non-zero OR fails to run counts as not-passed (but\n * still `ran`) — never a thrown error that crashes the kernel.\n */\n\nimport { type GateOutcome } from \"./select\"\n\nexport interface CheckSpec {\n /** The canonical check id the selector compares (e.g. \"tests\"). */\n id: string\n /** The sealed command for this check (e.g. \"bun test\"). */\n command: string\n}\n\nexport interface ExecResult {\n exitCode: number\n}\n\nexport type ExecFn = (input: { command: string; cwd: string }) => Promise<ExecResult>\n\nexport async function runGateChecks(\n checks: ReadonlyArray<CheckSpec>,\n cwd: string,\n exec: ExecFn,\n): Promise<GateOutcome> {\n // Independent checks run concurrently; a throw or a non-zero exit is a fail,\n // never a kernel crash.\n const results = await Promise.all(\n checks.map(async (c) => {\n try {\n const r = await exec({ command: c.command, cwd })\n return { id: c.id, passed: r.exitCode === 0 }\n } catch {\n return { id: c.id, passed: false }\n }\n }),\n )\n const passed = new Set<string>()\n const ran = new Set<string>()\n for (const r of results) {\n ran.add(r.id)\n if (r.passed) passed.add(r.id)\n }\n return { passed, ran }\n}\n","/**\n * The SEALED executable-gate registry. The kernel owns the gate commands; the\n * workflow IR (and the model that drafted it) references a gate by its `id` ONLY\n * and can never author or alter the command that runs (invariant 5,\n * gate-immutability at the command level). `run_workflow` resolves the caller's\n * `gateId` against this map; an unknown id is rejected before the kernel runs, so\n * a model cannot smuggle an arbitrary shell command in disguised as a \"gate\".\n *\n * The check `id`s (not the commands) are what champion-retention compares, so the\n * selector reasons over stable category names (\"typecheck\"/\"test\"/\"lint\"), not\n * over shell strings that vary per repo.\n */\n\nimport { type CheckSpec } from \"./gate-runner\"\n\nexport interface SealedGate {\n id: string\n /** The sealed checks (id + command). Run by the kernel, never by the model. */\n checks: CheckSpec[]\n}\n\n/**\n * Built-in sealed gates. Commands follow this repo's TS/Bun conventions (the\n * `bun run <script>` indirection means a repo without that script simply fails\n * the check, which the selector treats as not-passed rather than a crash). New\n * ecosystems get a new sealed id here, never a caller-supplied command.\n */\nconst SEALED_GATES: Readonly<Record<string, ReadonlyArray<CheckSpec>>> = {\n \"default-ci\": [\n { id: \"typecheck\", command: \"bun run typecheck\" },\n { id: \"test\", command: \"bun test\" },\n { id: \"lint\", command: \"bun run lint\" },\n ],\n \"typecheck-test\": [\n { id: \"typecheck\", command: \"bun run typecheck\" },\n { id: \"test\", command: \"bun test\" },\n ],\n \"typecheck-only\": [{ id: \"typecheck\", command: \"bun run typecheck\" }],\n}\n\n/** The set of sealed gate ids, used as the kernel's `knownGateIds` so the IR\n * verifier rejects an executable gate that references an unregistered id. */\nexport function sealedGateIds(): ReadonlySet<string> {\n return new Set(Object.keys(SEALED_GATES))\n}\n\n/**\n * Resolve a sealed gate by id. Returns a DEFENSIVE CLONE (fresh objects) so a\n * caller can never mutate the registry's command set. `undefined` for an\n * unknown id, which `run_workflow` rejects before executing anything.\n */\nexport function resolveSealedGate(gateId: string): SealedGate | undefined {\n const checks = SEALED_GATES[gateId]\n if (!checks) return undefined\n return { id: gateId, checks: checks.map((c) => ({ id: c.id, command: c.command })) }\n}\n","/**\n * Gate-immutability detection (floor invariant 5). A producer must not weaken\n * the gates it is judged by — adding `.skip`, `@ts-ignore`, `as any`, or\n * `eslint-disable` turns a failing gate green without fixing anything, which is\n * the cheapest way to defeat the whole floor guarantee.\n *\n * `detectGateWeakening` scans the ADDED lines of a unified git diff for these\n * patterns. It is deliberately a syntactic heuristic over added lines only (a\n * removed `.skip` is a strengthening, not a weakening) — pure, dependency-free,\n * and used by BOTH the Phase-0 structural-gate Stop-hook (reject the diff) and\n * the kernel's runner (gate-immutability check before accepting an artifact).\n *\n * Patterns are LANGUAGE-SCOPED: each added line is tested only against a shared\n * COMMON set plus the patterns for the file's language (by extension, derived\n * from the diff header). An unknown extension is tested against COMMON only —\n * failing OPEN (no false block) is the safe direction for a Stop hook. This both\n * generalizes beyond TS/JS and removes cross-language false positives (e.g. a Go\n * `.only(` substring is no longer flagged as a skipped JS test).\n */\n\nexport interface WeakeningFinding {\n /** Stable category (e.g. \"skipped-test\", \"any-cast\"). */\n pattern: string\n /** The offending added line (trimmed) for the report. */\n line: string\n /** The file the line was added to, when derivable from the diff header. */\n file?: string\n}\n\nexport interface GateImmutabilityResult {\n weakened: boolean\n findings: WeakeningFinding[]\n}\n\ninterface WeakeningPattern {\n name: string\n re: RegExp\n /** Where the syntax is meaningful: executable code or a comment directive. */\n scope: \"code\" | \"slash-comment\" | \"hash-comment\"\n}\n\n/** Patterns that weaken a gate regardless of language (test exclusivity / focus\n * idioms shared across JS test runners; kept in COMMON since several languages\n * reuse them). */\nconst COMMON_PATTERNS: ReadonlyArray<WeakeningPattern> = [\n // Disabling tests: jest/bun/mocha skip + exclusive-focus (`.only` narrows the\n // suite so other failures stop running — also a weakening).\n { name: \"skipped-test\", re: /(\\.\\s*skip\\s*\\(|\\bxit\\s*\\(|\\bxdescribe\\s*\\(|\\.\\s*only\\s*\\()/, scope: \"code\" },\n]\n\n/** Language-specific weakening patterns, selected by the added line's file type. */\nconst LANG_PATTERNS: Readonly<Record<string, ReadonlyArray<WeakeningPattern>>> = {\n ts: [\n // Silencing the type-checker. Scoped to comments because the directive lives there.\n { name: \"ts-suppression\", re: /@ts-(ignore|nocheck|expect-error)\\b/, scope: \"slash-comment\" },\n // Casting away type errors.\n { name: \"any-cast\", re: /\\bas\\s+any\\b|:\\s*any\\b/, scope: \"code\" },\n // Silencing the linter. Scoped to comments because the directive lives there.\n { name: \"eslint-disable\", re: /eslint-disable\\b/, scope: \"slash-comment\" },\n ],\n py: [\n { name: \"py-type-ignore\", re: /#\\s*type:\\s*ignore\\b/, scope: \"hash-comment\" },\n { name: \"py-noqa\", re: /#\\s*noqa\\b/, scope: \"hash-comment\" },\n { name: \"py-skip\", re: /@(pytest\\.mark\\.skip|unittest\\.skip)\\b/, scope: \"code\" },\n ],\n go: [\n { name: \"go-skip\", re: /\\bt\\.Skip\\s*\\(/, scope: \"code\" },\n { name: \"go-nolint\", re: /\\/\\/\\s*nolint\\b/, scope: \"slash-comment\" },\n ],\n rust: [\n { name: \"rust-ignore\", re: /#\\[\\s*ignore\\b/, scope: \"code\" },\n { name: \"rust-allow\", re: /#\\[\\s*allow\\s*\\(/, scope: \"code\" },\n ],\n}\n\n/** Map a file path to a language key for `LANG_PATTERNS` (null → COMMON only). */\nfunction langForFile(file: string | undefined): keyof typeof LANG_PATTERNS | null {\n if (!file) return null\n const ext = file.slice(file.lastIndexOf(\".\")).toLowerCase()\n switch (ext) {\n case \".ts\":\n case \".tsx\":\n case \".mts\":\n case \".cts\":\n case \".js\":\n case \".jsx\":\n case \".mjs\":\n case \".cjs\":\n return \"ts\"\n case \".py\":\n case \".pyi\":\n return \"py\"\n case \".go\":\n return \"go\"\n case \".rs\":\n return \"rust\"\n default:\n return null\n }\n}\n\n/** Union of COMMON + every language — the pre-header default (no file known yet),\n * so a header-less diff is matched permissively, exactly as before this became\n * language-scoped. Once a real file header appears, the set narrows to that\n * file's language (or COMMON only for an unknown extension → fail open). */\nconst ALL_PATTERNS: ReadonlyArray<WeakeningPattern> = [\n ...COMMON_PATTERNS,\n ...Object.values(LANG_PATTERNS).flat(),\n]\n\n/** Lexical views of one added line. Quoted text is omitted from every view;\n * code excludes comments, while directive views retain only their comment text.\n * This prevents fixture/documentation strings from self-matching without hiding\n * real comment-based suppressions. It is intentionally conservative: an\n * unterminated quote returns the raw line as code and no comment views. */\nfunction lexicalViews(line: string): { code: string; slashComment: string; hashComment: string } {\n let code = \"\"\n let slashComment = \"\"\n let hashComment = \"\"\n let quote: \"'\" | '\"' | \"`\" | null = null\n let escaped = false\n for (let i = 0; i < line.length; i++) {\n const ch = line[i]!\n if (quote !== null) {\n if (escaped) {\n escaped = false\n continue\n }\n if (ch === \"\\\\\") {\n escaped = true\n continue\n }\n if (ch === quote) quote = null\n continue\n }\n if (ch === \"'\" || ch === '\"' || ch === \"`\") {\n quote = ch\n code += \" \"\n continue\n }\n if (ch === \"/\" && line[i + 1] === \"/\") {\n slashComment = line.slice(i)\n break\n }\n if (ch === \"/\" && line[i + 1] === \"*\") {\n slashComment = line.slice(i)\n break\n }\n // `#` starts a comment for Python, but Rust attributes begin `#[` and are\n // executable syntax that must remain in the code view.\n if (ch === \"#\" && line[i + 1] !== \"[\") {\n hashComment = line.slice(i)\n break\n }\n code += ch\n }\n return quote === null\n ? { code, slashComment, hashComment }\n : { code: line, slashComment: \"\", hashComment: \"\" }\n}\n\n/** A `diff --git a/x b/x` or `+++ b/x` header → the current file path. */\nfunction fileFromHeader(line: string): string | undefined {\n const git = /^diff --git a\\/.+ b\\/(.+)$/.exec(line)\n if (git) return git[1]\n const plus = /^\\+\\+\\+ b\\/(.+)$/.exec(line)\n if (plus) return plus[1]\n return undefined\n}\n\nexport function detectGateWeakening(diff: string): GateImmutabilityResult {\n const findings: WeakeningFinding[] = []\n let file: string | undefined\n let patterns: ReadonlyArray<WeakeningPattern> = ALL_PATTERNS\n for (const raw of diff.split(\"\\n\")) {\n const headerFile = fileFromHeader(raw)\n if (headerFile !== undefined) {\n file = headerFile\n const lang = langForFile(file)\n patterns = lang ? [...COMMON_PATTERNS, ...LANG_PATTERNS[lang]] : COMMON_PATTERNS\n continue\n }\n // Only ADDED content lines (skip the `+++` file header and context/removed).\n if (!raw.startsWith(\"+\") || raw.startsWith(\"+++\")) continue\n const added = raw.slice(1)\n const views = lexicalViews(added)\n for (const p of patterns) {\n const searchable = p.scope === \"code\"\n ? views.code\n : p.scope === \"slash-comment\"\n ? views.slashComment\n : views.hashComment\n if (p.re.test(searchable)) {\n findings.push(file === undefined ? { pattern: p.name, line: added.trim() } : { pattern: p.name, line: added.trim(), file })\n }\n }\n }\n return { weakened: findings.length > 0, findings }\n}\n","/**\n * The Phase-0 structural-gate decision — the cheap, CERTAIN floor win. It blocks\n * marking work \"done\" when any canonical gate fails OR the diff weakens a gate.\n * A non-model check that fires every run and never ships broken or gate-gamed\n * code; per the floor analysis it is the only component that raises the\n * worst-case floor BY CONSTRUCTION (the orchestration on top is a conditional\n * bet).\n *\n * This is the logic behind the spawned-session Stop-hook AND the kernel runner's\n * pre-accept check. The process exec and the diff are inputs, so the decision is\n * pure + unit-testable; the hook script that runs `git diff` + invokes this is a\n * thin wrapper.\n */\n\nimport { detectGateWeakening, type WeakeningFinding } from \"./gate-immutability\"\nimport { runGateChecks, type CheckSpec, type ExecFn } from \"./gate-runner\"\n\nexport interface StopGateInput {\n /** The canonical gate commands (tests/types/lint/build). */\n checks: ReadonlyArray<CheckSpec>\n /** Workspace the checks run in. */\n cwd: string\n /** Injected process exec (the live wrapper spawns `command` in `cwd`). */\n exec: ExecFn\n /** The working-tree diff to scan for gate-weakening (e.g. `git diff HEAD`). */\n diff: string\n}\n\nexport interface StopGateResult {\n /** True ⇒ refuse \"done\": a gate is red or the diff weakens a gate. */\n block: boolean\n /** Human-readable summary for the hook's stderr / the kernel's reason. */\n reason: string\n /** Canonical check ids that did not pass. */\n failedChecks: string[]\n /** Gate-weakening findings in the diff (added `.skip` / `as any` / …). */\n weakening: WeakeningFinding[]\n}\n\nexport async function evaluateStopGate(input: StopGateInput): Promise<StopGateResult> {\n const gate = await runGateChecks(input.checks, input.cwd, input.exec)\n const weak = detectGateWeakening(input.diff)\n\n const failedChecks = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id))\n const block = failedChecks.length > 0 || weak.weakened\n\n const parts: string[] = []\n if (failedChecks.length > 0) parts.push(`failing gates: ${failedChecks.join(\", \")}`)\n if (weak.weakened) {\n const pats = [...new Set(weak.findings.map((f) => f.pattern))].join(\", \")\n parts.push(`gate-weakening in the diff: ${pats}`)\n }\n\n return {\n block,\n reason: block ? parts.join(\"; \") : \"all canonical gates pass; no gate-weakening in the diff\",\n failedChecks,\n weakening: weak.findings,\n }\n}\n","/**\n * The Phase-0 structural-gate Stop-hook glue. `runStopGateForLaunch` resolves a\n * SEALED gate by id and evaluates it against the working-tree diff, returning the\n * block decision a spawned-session Stop hook (or a manual check) acts on.\n *\n * Safety posture (deliberately different from the kernel): a Stop hook that\n * blocks on a MISCONFIGURATION would wedge the user's session (they could not\n * stop). So this fails OPEN on config problems (unknown gate id) and blocks ONLY\n * on a genuine red gate or a gate-weakening diff. The kernel, by contrast, fails\n * CLOSED to the baseline. Both are correct for their context.\n *\n * The sealed gate + diff + exec are inputs, so the decision is pure and\n * unit-testable; the thin live wrapper (capture `git diff`, spawn the checks,\n * map the result to an exit code) is the only part that needs a real process.\n */\n\nimport { buildSelfCommand, type SelfInvocation } from \"../hook-launcher/self-invocation\"\nimport { resolveSealedGate } from \"./gate-registry\"\nimport { runGateChecks, type CheckSpec, type ExecFn } from \"./gate-runner\"\nimport { evaluateStopGate, type StopGateResult } from \"./stop-gate\"\nimport { isSubagentContext, regressions, type BaselineStore, type ReviewDebounce } from \"./stop-gate-policy\"\nimport { parseBoolEnv } from \"~/lib/exec\"\n\nimport { createHash } from \"node:crypto\"\nimport { promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nexport interface StopGateLaunchInput {\n /** Workspace the checks run in (the session cwd). */\n workspace: string\n /** Which SEALED gate to run (the registry owns the commands). */\n gateId: string\n /** Injected process exec. */\n exec: ExecFn\n /** The working-tree diff to scan for gate-weakening (e.g. `git diff HEAD`). */\n diff: string\n}\n\nexport async function runStopGateForLaunch(input: StopGateLaunchInput): Promise<StopGateResult> {\n const gate = resolveSealedGate(input.gateId)\n if (!gate) {\n // Fail OPEN: never wedge the session on a config error. Surface it instead.\n return {\n block: false,\n reason: `stop-gate: unknown gateId \"${input.gateId}\" (not blocking)`,\n failedChecks: [],\n weakening: [],\n }\n }\n return evaluateStopGate({ checks: gate.checks, cwd: input.workspace, exec: input.exec, diff: input.diff })\n}\n\n/**\n * The launch-stable baseline key for the dynamic gate path: keyed by the\n * workdir + the descriptor's check set + a per-launch token, NOT the session id\n * and NOT HEAD. The LAUNCHER generates the token (passed to the child via env),\n * computes this key, and seeds the baseline (the set of checks already failing at\n * launch, BEFORE the agent mutates the tree); the runtime hook recomputes the\n * same key (reading the token from its env) and reads that baseline, so an\n * agent-introduced failure is a REGRESSION rather than being silently adopted as\n * the baseline at first stop. The per-launch token isolates concurrent sessions\n * on the same repo (they don't clobber each other's baseline). HEAD is\n * deliberately NOT in the key: an agent that COMMITS its work mid-session must\n * still be judged against the pre-session baseline (a HEAD-keyed baseline would\n * vanish on the commit — the exact moment it matters). A changed check set (the\n * agent edited the harness) yields a different key → the launch baseline no\n * longer matches and the hook degrades to first-stop baselining.\n */\nexport function launchBaselineKey(workdir: string, descriptorKey: string, token?: string): string {\n return JSON.stringify([\"launch\", nodePath.resolve(workdir), descriptorKey, token ?? \"\"])\n}\n\n/**\n * Run the resolved checks ONCE at launch (pre-mutation) and record the failing\n * set as the baseline under `launchBaselineKey`. Best-effort: any failure is\n * swallowed (the hook then degrades to first-stop baselining). The launcher MUST\n * pass the SAME check set the hook will run (same `includeTests`) so a\n * pre-existing test failure isn't later miscounted as a regression.\n *\n * NOTE: capture is best-effort pre-mutation — the launcher fires it before the\n * child can edit, and in practice the agent does not mutate until well after a\n * user prompt, so the checks see the clean tree. If the agent somehow mutates\n * before this completes, the worst case is the legacy first-stop baseline (no\n * regression, never a wedge).\n */\nexport async function captureLaunchBaseline(input: {\n checks: ReadonlyArray<CheckSpec>\n workdir: string\n exec: ExecFn\n descriptorKey: string\n token?: string\n baseline: BaselineStore\n}): Promise<void> {\n if (input.checks.length === 0) return\n try {\n const gate = await runGateChecks(input.checks, input.workdir, input.exec)\n const failed = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id))\n await input.baseline.set(launchBaselineKey(input.workdir, input.descriptorKey, input.token), failed)\n } catch {\n /* best-effort: a launch-baseline miss only downgrades to first-stop baselining. */\n }\n}\n\n/**\n * The structural-gate Stop hook is OPT-IN and default-OFF: it changes the spawned\n * session's stop behavior (a red gate refuses \"done\"), so a user enables it\n * explicitly via `GH_ROUTER_ENABLE_STOP_GATE` (the canonical `parseBoolEnv`\n * accepts `1`/`true`/`yes`/`on`).\n */\nexport function stopGateEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_ENABLE_STOP_GATE) === true\n}\n\n/**\n * The advisory background review (hook V2) is ON by default whenever the Stop\n * gate runs; it is the cross-lab accountability layer. Opt out with\n * `GH_ROUTER_DISABLE_STOP_REVIEW=1` to keep the deterministic gate but drop the\n * LLM review. (Disabling the whole gate with `GH_ROUTER_DISABLE_STOP_GATE=1`\n * also drops the review, since the review only ever fires from the gate's green\n * path.)\n */\nexport function stopReviewEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_REVIEW) !== true\n}\n\n/** The sealed gate the Stop hook runs, overridable via `GH_ROUTER_STOP_GATE_ID`\n * (must be a registered sealed id; the live wrapper falls open on an unknown\n * id). Defaults to `default-ci`. */\nexport function stopGateId(env: NodeJS.ProcessEnv = process.env): string {\n const v = (env.GH_ROUTER_STOP_GATE_ID ?? \"\").trim()\n return v.length > 0 ? v : \"default-ci\"\n}\n\n/**\n * Is a unified-diff file path confined to a `plans/` or `memory/` directory?\n * Windows-safe (normalizes `\\` → `/`). Matches only when a DIRECTORY segment of\n * the path is exactly `plans` or `memory`, so `plans/x.md`, `memory/y.md`, and\n * `a/plans/z` match while the filenames `docs/plans.md` / `myplans/x` do not.\n */\nfunction isPlanMemoryDiffPath(p: string): boolean {\n const segs = p.replace(/\\\\/g, \"/\").split(\"/\")\n // Only directory segments count — exclude the final (filename) segment.\n for (let i = 0; i < segs.length - 1; i++) {\n if (segs[i] === \"plans\" || segs[i] === \"memory\") return true\n }\n return false\n}\n\n/**\n * Drop the file sections of a unified `git diff` whose NEW path (the `b/…` side)\n * is confined to `plans/` or `memory/` — used for the gate-WEAKENING scan when\n * plan mode is active. Sections for other files (e.g. `src/…`) are preserved\n * verbatim, so a MIXED diff keeps its non-plan hunks and `src/` weakening is\n * still caught. The executable gate does not read the diff, so filtering it here\n * never affects which checks run. A diff with no `diff --git` sections (or an\n * empty one) is returned unchanged.\n */\nexport function stripPlanMemoryDiffHunks(diff: string): string {\n if (diff.length === 0) return diff\n const out: string[] = []\n let dropping = false\n for (const line of diff.split(\"\\n\")) {\n if (line.startsWith(\"diff --git \")) {\n const m = /^diff --git a\\/.+ b\\/(.+)$/.exec(line)\n const bPath = m?.[1]\n dropping = bPath !== undefined && isPlanMemoryDiffPath(bPath)\n if (!dropping) out.push(line)\n continue\n }\n if (!dropping) out.push(line)\n }\n return out.join(\"\\n\")\n}\n\n/**\n * C3: whether the structural Stop-gate is disabled for THIS launch. True when the\n * `GH_ROUTER_DISABLE_STOP_GATE` env is set OR the `--no-stop-gate` launcher flag was\n * passed (`args[\"no-stop-gate\"] === true`). A DRIVEN session sets the flag so a\n * blocking Stop hook never hangs its turn-end (~10 min) waiting on the fleet driver.\n * Pure so it is unit-testable without the live launch path.\n */\nexport function stopGateDisabled(\n args: Record<string, unknown>,\n env: NodeJS.ProcessEnv = process.env,\n): boolean {\n return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true || args[\"no-stop-gate\"] === true\n}\n\n/**\n * Build the Claude Code `settings.json` fragment that registers `command` as a\n * Stop hook. Returns just the `hooks` object so the caller merges it into the\n * mirrored settings (never clobbering existing hooks). The Stop event takes no\n * matcher; the command runs on every stop and an exit code of 2 blocks it.\n */\nexport function buildStopHookSettings(command: string): {\n hooks: { Stop: Array<{ hooks: Array<{ type: \"command\"; command: string }> }> }\n} {\n return { hooks: { Stop: [{ hooks: [{ type: \"command\", command }] }] } }\n}\n\n/** True when a settings `Stop` entry already registers `command` (so the merge\n * is idempotent across re-launches). */\nfunction entryHasCommand(entry: unknown, command: string): boolean {\n if (!entry || typeof entry !== \"object\") return false\n const hooks = (entry as { hooks?: unknown }).hooks\n if (!Array.isArray(hooks)) return false\n return hooks.some((h) => h && typeof h === \"object\" && (h as { command?: unknown }).command === command)\n}\n\n/**\n * Idempotently merge a hook running `command` for `event` (default `Stop`) into\n * an existing Claude Code settings object WITHOUT clobbering other hook events or\n * other entries. Returns a new object (never mutates the input). Re-running the\n * launcher with the same command+event does not duplicate the hook.\n */\nexport function mergeStopHookIntoSettings(\n existing: Record<string, unknown> | undefined,\n command: string,\n event: string = \"Stop\",\n timeoutSec?: number,\n matcher?: string,\n): Record<string, unknown> {\n const base: Record<string, unknown> = existing && typeof existing === \"object\" ? { ...existing } : {}\n const hooks: Record<string, unknown> =\n base.hooks && typeof base.hooks === \"object\" ? { ...(base.hooks as Record<string, unknown>) } : {}\n const arr: unknown[] = Array.isArray(hooks[event]) ? [...(hooks[event] as unknown[])] : []\n if (!arr.some((e) => entryHasCommand(e, command))) {\n const hook: { type: \"command\"; command: string; timeout?: number } = { type: \"command\", command }\n if (typeof timeoutSec === \"number\" && Number.isFinite(timeoutSec) && timeoutSec > 0) {\n hook.timeout = timeoutSec\n }\n arr.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] })\n }\n hooks[event] = arr\n base.hooks = hooks\n return base\n}\n\n/**\n * The Stop-hook subcommand decision, factored out so it is pure + unit-testable.\n * Given the raw stdin JSON Claude Code sends, the sealed gate id, an exec, a\n * diff-capture, and a persistent per-session block budget, it returns the exit\n * code (2 blocks, 0 allows) and the stderr Claude reads on a block.\n *\n * TERMINATION GUARANTEE (the one property that must hold: never wedge the\n * session). Three independent stand-down paths, any of which yields exit 0:\n * 1. unparseable stdin or no session_id -> can't budget-track safely, allow;\n * 2. Claude Code's own `stop_hook_active` re-entry signal;\n * 3. a HARD per-session block budget (`maxBlocks`, default 3): after that many\n * blocks for one session_id the gate ALWAYS allows the stop. This is the\n * load-bearing guard, because `stop_hook_active` can reset when the model\n * does intervening tool calls between stop attempts (so it alone does NOT\n * bound the loop). Any budget IO failure also stands down (can't guarantee\n * termination -> don't block).\n * The gate blocks only when ALL of these allow it AND the executable gate is red\n * or the diff weakens a gate.\n */\nexport interface StopHookDecision {\n exitCode: 0 | 2\n /** stderr text shown to Claude on a block (exit 2). */\n stderr?: string\n}\n\n/** Inputs the detached background reviewer needs (assembled on the green path). */\nexport interface StopReviewContext {\n sessionId: string\n cwd: string\n diff: string\n diffHash: string\n}\n\n/** A persistent per-session count of how many times the gate has blocked, so the\n * budget survives across separate hook-process invocations within one session. */\nexport interface BlockBudget {\n count: (sessionId: string) => Promise<number>\n record: (sessionId: string) => Promise<void>\n /** Clear the count for a session (called by the per-prompt reset hook so the\n * budget is per-user-prompt, not per-session-lifetime). */\n reset: (sessionId: string) => Promise<void>\n}\n\nexport async function decideStopHook(input: {\n /** Raw stdin from Claude Code (a JSON payload; tolerated if malformed). */\n stdin: string\n gateId: string\n exec: ExecFn\n /** Capture the working-tree diff for the session cwd (e.g. `git diff HEAD`). */\n captureDiff: (cwd: string) => Promise<string>\n /** cwd to use when the payload omits one. */\n fallbackCwd: string\n /** Persistent per-session block budget (the hard termination guard). */\n budget: BlockBudget\n /** Per-session baseline of pre-existing failures (block only on regressions). */\n baseline: BaselineStore\n /**\n * Runtime trust re-check: act ONLY in a repo the user consented to (or\n * force-enabled). Injected for testability; the live wrapper passes\n * `stopGateEnabledForRepo`. This is the security gate — even though the\n * launcher only registers the hook for a trusted repo, re-checking at runtime\n * against `payload.cwd` defends against a mid-session cwd change.\n */\n isEnabledForRepo: (cwd: string) => Promise<boolean>\n /**\n * DYNAMIC gate source (language-agnostic path). When provided, the gate's\n * check set is resolved at decision time from this resolver instead of the\n * sealed `gateId` registry — re-derived from the live tree (parser path) or\n * read from the cached evidence-pinned record (discovered path). It returns:\n * - `checks` : the canonical `{id, command}` set to run;\n * - `workdir` : the dir the checks run in (the repo/package root where\n * the evidence was found — NOT the Stop payload cwd, so a\n * monorepo stop from a nested dir runs the root's checks);\n * - `descriptorKey` : a stable key over the check set for baseline isolation.\n * Returning `null` (no gate resolvable now — markers/tools vanished, empty set)\n * makes the hook FAIL OPEN (exit 0), never wedging the session. When absent the\n * hook falls through to the sealed `gateId` path (bun/TS, unchanged).\n */\n resolveChecks?: (\n cwd: string,\n ) => Promise<{ checks: CheckSpec[]; workdir: string; descriptorKey: string; baselineKey?: string } | null>\n /** Max blocks per prompt before the gate always allows (default 2). */\n maxBlocks?: number\n /** Absolute wall-clock cap on the diff+gate evaluation; on timeout the hook\n * FAILS OPEN (exit 0) and never claims the gate passed. Default 300s. */\n timeoutMs?: number\n /**\n * Advisory-review layer (hook V2), entirely OPTIONAL and side-effect-only.\n * When BOTH `reviewDebounce` and `spawnReview` are provided, a GREEN stop with\n * a substantive diff (and a not-yet-reviewed diff hash) spawns a detached,\n * background read-only review. This NEVER affects the exit code — the\n * deterministic gate above is the only blocker. Any failure in this block is\n * swallowed (the stop still ends exit 0). Omitted in tests / when\n * `GH_ROUTER_DISABLE_STOP_REVIEW` is set → the deterministic gate is unchanged.\n */\n reviewDebounce?: ReviewDebounce\n /** Fire-and-forget spawn of the detached background reviewer. Must return\n * immediately (detached + unref'd); decideStopHook never awaits the review. */\n spawnReview?: (ctx: StopReviewContext) => void\n}): Promise<StopHookDecision> {\n const maxBlocks = input.maxBlocks ?? 2\n let payload: { cwd?: unknown; session_id?: unknown; agent_id?: unknown; agent_type?: unknown; plan_mode?: unknown } = {}\n let parsed = false\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") {\n payload = p as typeof payload\n parsed = true\n }\n } catch {\n // tolerate a non-JSON stdin.\n }\n // (1) abnormal payload -> fail OPEN (can't budget-track safely).\n if (!parsed) return { exitCode: 0 }\n // (2) SCOPING: stand down in any subagent / teammate context. A Stop hook is\n // converted to SubagentStop for subagents, so this command CAN run there;\n // the payload's agent_type/agent_id mark it. The gate is top-level-only.\n if (isSubagentContext(payload)) return { exitCode: 0 }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n // (3) without a session id we cannot enforce the termination budget -> allow.\n if (!sessionId) return { exitCode: 0 }\n const cwdRaw = typeof payload.cwd === \"string\" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd\n // Resolve symlinks ONCE so the consent check and the gate's exec target are\n // the same immutable path — defends against a symlink/rename swap between the\n // trust check and the exec (codex review #3). Keep the raw path if realpath\n // fails (e.g. the dir was removed) — the consent check then simply denies.\n let cwd = cwdRaw\n try {\n cwd = await fs.realpath(cwdRaw)\n } catch {\n /* keep raw */\n }\n // (4) CONSENT: only run the repo's scripts if the user trusted it (or forced).\n // Any failure -> allow (never wedge, never run untrusted code).\n let enabled: boolean\n try {\n enabled = await input.isEnabledForRepo(cwd)\n } catch {\n return { exitCode: 0 }\n }\n if (!enabled) return { exitCode: 0 }\n // (5) hard per-prompt budget: never block more than maxBlocks times per\n // prompt. This — NOT the (now-absent) stop_hook_active flag — is the\n // termination guard; relying on stop_hook_active would defeat block-twice.\n let priorBlocks: number\n try {\n priorBlocks = await input.budget.count(sessionId)\n } catch {\n return { exitCode: 0 } // can't read the budget -> can't guarantee termination -> allow.\n }\n if (priorBlocks >= maxBlocks) {\n // Termination guard: never block more than maxBlocks times per prompt. This\n // is a deliberate trade-off (a stubborn agent gets through), so make the\n // stand-down LOUD rather than a silent exit 0 — the user must know broken\n // work may be shipping unverified.\n return {\n exitCode: 0,\n stderr:\n `structural gate: reached the ${maxBlocks}-block limit for this prompt; allowing the stop `\n + `WITHOUT re-running the checks. If the failures from the last block are unfixed, they are `\n + `shipping — run the checks manually to verify.`,\n }\n }\n\n // (6) Run the gate under an absolute timeout. The gate result is computed\n // inside the raced promise; the BASELINE read/write happens AFTER the race\n // resolves to a real result, so a timed-out (abandoned) eval can never\n // seed or poison the baseline (codex review #5). The gate's child process\n // is bounded + tree-killed by liveExec's per-command timeout, and the\n // wrapper process.exit()s, so a timeout never wedges the session.\n //\n // The working-tree diff is captured INSIDE the raced promise (so a stalled\n // `git diff` is covered by the same absolute timeout, not run unbounded on\n // the critical path) and RETURNED so the advisory review below judges the\n // exact same tree without a second capture. captureDiff swallows its own\n // failures to \"\".\n // The check set comes from the dynamic resolver when injected (parser /\n // discovered path) or the sealed `gateId` registry otherwise. `resolvedKey`\n // (read AFTER the race) keys the baseline so a changed command set gets a\n // fresh baseline. `resolvedWorkdir` is where the checks run (the repo root the\n // evidence was found in, not necessarily the payload cwd — monorepo fix).\n let resolvedKey = input.gateId\n // When the dynamic resolver supplies a launch-stable baseline key (keyed by\n // workdir + HEAD + descriptor, NOT the session), the gate reads the baseline\n // the LAUNCHER captured BEFORE the agent mutated the tree — so an\n // agent-introduced failure is a regression, not silently adopted as baseline.\n let dynamicBaselineKey: string | undefined\n // Forward-compatible payload signal. Current Claude Code does not emit\n // `plan_mode`, but preserving this path keeps the prior diff-scoping behavior\n // if it does. The dead env fallback is deliberately gone.\n const scanDiff = (diff: string): string =>\n payload.plan_mode === true ? stripPlanMemoryDiffHunks(diff) : diff\n type GateRunResult =\n | { kind: \"evaluated\"; failedChecks: string[]; weakeningPatterns: string[]; diff: string }\n | { kind: \"no-diff\"; diff: string }\n const capturedDiff = async (workdir: string): Promise<{ diff: string; captured: boolean }> => {\n try {\n return { diff: await input.captureDiff(workdir), captured: true }\n } catch {\n // A failed diff capture is NOT an empty diff. Preserve the old safe behavior:\n // run the executable checks with an empty weakening scan rather than skipping.\n return { diff: \"\", captured: false }\n }\n }\n const runGate = async (): Promise<GateRunResult | null> => {\n if (input.resolveChecks) {\n const resolved = await input.resolveChecks(cwd).catch(() => null)\n // No gate resolvable now (markers/tools vanished, empty set) → fail OPEN.\n if (!resolved || resolved.checks.length === 0) return null\n resolvedKey = resolved.descriptorKey\n dynamicBaselineKey = resolved.baselineKey\n const workdir = resolved.workdir || cwd\n const { diff, captured } = await capturedDiff(workdir)\n if (captured && diff.length === 0) return { kind: \"no-diff\", diff }\n const result = await evaluateStopGate({ checks: resolved.checks, cwd: workdir, exec: input.exec, diff: captured ? scanDiff(diff) : \"\" })\n return {\n kind: \"evaluated\",\n failedChecks: [...result.failedChecks],\n weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],\n diff,\n }\n }\n const { diff, captured } = await capturedDiff(cwd)\n if (captured && diff.length === 0) return { kind: \"no-diff\", diff }\n const result = await runStopGateForLaunch({ workspace: cwd, gateId: input.gateId, exec: input.exec, diff: captured ? scanDiff(diff) : \"\" })\n return {\n kind: \"evaluated\",\n failedChecks: [...result.failedChecks],\n weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],\n diff,\n }\n }\n const timeoutMs = input.timeoutMs ?? 300_000\n let timer: ReturnType<typeof setTimeout> | undefined\n let raced: GateRunResult | null | \"timeout\"\n try {\n raced = await Promise.race<GateRunResult | null | \"timeout\">([\n runGate(),\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeoutMs)\n // Never keep this short-lived hook process alive on the timer alone.\n timer.unref?.()\n }),\n ])\n } finally {\n // Clear in `finally`: a `runGate()` REJECTION previously skipped the\n // clearTimeout below it, leaving a 5-minute timer pinning the hook process\n // open long after it had nothing left to do.\n if (timer) clearTimeout(timer)\n }\n if (raced === \"timeout\") return { exitCode: 0 } // gate did not complete -> allow; never a claimed pass.\n if (raced === null) return { exitCode: 0 } // no gate resolvable now -> allow (fail open).\n if (raced.kind === \"no-diff\") return { exitCode: 0 } // no working-tree diff -> no checks to run.\n\n // Baseline isolation, keyed by (session, workdir, descriptor) so a mid-session\n // repo/gate change can't mask or invent regressions (codex review #7). The\n // dynamic path prefers a launch-stable key (pre-mutation baseline). Only a\n // COMPLETED eval reads/writes it.\n const baselineKey = dynamicBaselineKey ?? JSON.stringify([sessionId, cwd, resolvedKey])\n const recorded = await input.baseline.get(baselineKey).catch(() => null)\n if (recorded === null) {\n await input.baseline.set(baselineKey, raced.failedChecks).catch(() => {})\n }\n const regressed = regressions(raced.failedChecks, recorded)\n const weakened = raced.weakeningPatterns.length > 0\n if (regressed.length === 0 && !weakened) {\n // GREEN stop. Fire the advisory background review (never blocks, never\n // changes the exit code). Fully isolated + time-bounded so a store or spawn\n // failure (or a stalled debounce read/write) can't turn a clean stop into\n // anything but a prompt exit 0.\n await maybeSpawnReview(input, sessionId, cwd, raced.diff)\n return { exitCode: 0 }\n }\n try {\n await input.budget.record(sessionId)\n } catch {\n return { exitCode: 0 } // can't persist the block -> can't bound the loop -> allow.\n }\n const parts: string[] = []\n if (regressed.length > 0) parts.push(`regressed gates: ${regressed.join(\", \")}`)\n if (weakened) parts.push(`gate-weakening in the diff: ${raced.weakeningPatterns.join(\", \")}`)\n return {\n exitCode: 2,\n stderr:\n `structural gate failed (block ${priorBlocks + 1}/${maxBlocks}): ${parts.join(\"; \")}. `\n + `Fix the failing checks and revert any gate-weakening (no new .skip / as any / `\n + `lint-disable) before finishing.`,\n }\n}\n\n/**\n * The advisory-review side-effect on a GREEN stop: debounce by diff hash, then\n * fire the detached background reviewer. ADVISORY-ONLY — it returns void, never\n * throws (every step is swallowed), and the caller does not await its result for\n * the exit decision. A no-op when the review layer isn't wired (no debounce /\n * spawn injected, e.g. GH_ROUTER_DISABLE_STOP_REVIEW) or the diff is empty.\n *\n * `markReviewed` runs BEFORE the spawn so a crashing spawn still records the\n * debounce (an identical tree won't re-trigger on the next stop). The review is\n * gated on the diff CHANGING since the last review — without it, every stop of\n * an unchanged tree would re-spend a background gpt-5.6-sol review.\n *\n * The whole body is bounded by a short timeout (the stores are local temp files\n * that complete in well under a millisecond in practice, so the timeout never\n * fires normally — but if the debounce read/write ever stalled, the stop must\n * still proceed promptly; the advisory layer never delays a clean stop).\n */\nconst REVIEW_SIDE_EFFECT_BUDGET_MS = 2_000\n\nasync function maybeSpawnReview(\n input: { reviewDebounce?: ReviewDebounce; spawnReview?: (ctx: StopReviewContext) => void },\n sessionId: string,\n cwd: string,\n diff: string,\n): Promise<void> {\n if (!input.reviewDebounce || !input.spawnReview) return\n if (diff.trim().length === 0) return // no substantive diff -> nothing to review.\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const work = (async (): Promise<void> => {\n const diffHash = createHash(\"sha256\").update(diff).digest(\"hex\")\n if (!(await input.reviewDebounce!.shouldReview(sessionId, diffHash))) return\n await input.reviewDebounce!.markReviewed(sessionId, diffHash)\n input.spawnReview!({ sessionId, cwd, diff, diffHash })\n })()\n await Promise.race([\n work,\n new Promise<void>((resolve) => {\n timer = setTimeout(resolve, REVIEW_SIDE_EFFECT_BUDGET_MS)\n }),\n ])\n } catch {\n // Advisory layer must never affect the stop. Swallow everything.\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/**\n * A file-backed `BlockBudget` under `stateDir`, keyed by a hash of the session id\n * (so a session id is never written verbatim to a predictable path). Best-effort:\n * a read miss counts as 0; `record` increments. A write/read error propagates so\n * `decideStopHook` stands down (it can't guarantee termination without the\n * budget).\n */\nexport function fileBlockBudget(stateDir: string): BlockBudget {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `block-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n const readCount = async (sid: string): Promise<number> => {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) && n > 0 ? n : 0\n } catch {\n return 0 // a miss is 0 blocks so far.\n }\n }\n return {\n count: readCount,\n async record(sid) {\n const next = (await readCount(sid)) + 1\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), String(next), { mode: 0o600 })\n },\n async reset(sid) {\n await fs.unlink(fileFor(sid)).catch(() => {})\n },\n }\n}\n\n/** Build the shell command Claude Code runs for the Stop hook. */\nexport function buildStopHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-stop-hook\")\n}\n\n/**\n * Build the shell command Claude Code runs for the SessionStart/SessionEnd hooks\n * (registered only when github-router runs inside an ai-or-die Terminal tab). The\n * sidecar path is baked in as a literal `--out` arg — NOT passed via env — so it\n * survives `AIORDIE_CLAUDE_BIND` being stripped from the child's environment and a\n * nested `github-router claude` can't inherit it. Pure (binary + script + out\n * paths) for unit-testable quoting; the live firing is verified end-to-end.\n */\nexport function buildSessionBindHookCommand(\n invocation: SelfInvocation,\n outPath: string,\n): string {\n return buildSelfCommand(invocation, `internal-session-bind --out \"${outPath}\"`)\n}\n\n/** Command for the `internal-artifact-open` hook (no args — token comes from the\n * mirror creds file, plan from the plans dir; nothing secret in argv). */\nexport function buildArtifactOpenHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-artifact-open\")\n}\n\n/**\n * Read-merge-atomic-write the Stop hook into a Claude Code `settings.json` file\n * (the mirrored one). A MISSING file (ENOENT) starts from `{}`; any OTHER read or\n * parse error THROWS (the caller's try/catch warns and continues) rather than\n * overwriting a file we couldn't understand with our defaults. Preserves every\n * other setting, is idempotent, and uses temp+rename so Claude Code's mtime\n * watcher never sees a half-written file. Returns the merged object.\n */\nexport async function injectStopHookIntoSettingsFile(\n settingsPath: string,\n command: string,\n event: string = \"Stop\",\n timeoutSec?: number,\n matcher?: string,\n): Promise<Record<string, unknown>> {\n let existing: Record<string, unknown> = {}\n let raw: string | undefined\n try {\n raw = await fs.readFile(settingsPath, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err // never clobber on a transient read error.\n raw = undefined // missing file -> start clean.\n }\n if (raw !== undefined) {\n // A parse failure means a real file we don't understand: do NOT replace it.\n const parsed: unknown = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n existing = parsed as Record<string, unknown>\n } else {\n throw new Error(`settings.json at ${settingsPath} is not a JSON object; refusing to overwrite`)\n }\n }\n const merged = mergeStopHookIntoSettings(existing, command, event, timeoutSec, matcher)\n const tmp = `${settingsPath}.${process.pid}.tmp`\n await fs.writeFile(tmp, `${JSON.stringify(merged, null, 2)}\\n`, { mode: 0o600 })\n await fs.rename(tmp, settingsPath)\n return merged\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stop-gate-hook-DQnh_KfI.js","names":["nodePath","fs"],"sources":["../src/lib/orchestration/gate-runner.ts","../src/lib/orchestration/gate-registry.ts","../src/lib/orchestration/gate-immutability.ts","../src/lib/orchestration/stop-gate.ts","../src/lib/orchestration/stop-gate-hook.ts"],"sourcesContent":["/**\n * The executable-gate runner — runs the kernel's SEALED check commands and\n * reports which passed, as a `GateOutcome` the selector compares. A \"check\" is a\n * category (e.g. `tests` → `bun test`, `types` → `tsc`, `lint` → eslint); the\n * commands are owned by the kernel/registry, never authored by a producer\n * (gate-immutability, invariant 5), so a candidate can only PASS them, not\n * rewrite them.\n *\n * The process exec is INJECTED (`ExecFn`) so this is pure + unit-testable; the\n * live adapter (Bun.spawn / the managed-exe helper in a sandboxed cwd) is a thin\n * wrapper. A check that exits non-zero OR fails to run counts as not-passed (but\n * still `ran`) — never a thrown error that crashes the kernel.\n */\n\nimport { type GateOutcome } from \"./select\"\n\nexport interface CheckSpec {\n /** The canonical check id the selector compares (e.g. \"tests\"). */\n id: string\n /** The sealed command for this check (e.g. \"bun test\"). */\n command: string\n}\n\nexport interface ExecResult {\n exitCode: number\n}\n\nexport type ExecFn = (input: { command: string; cwd: string }) => Promise<ExecResult>\n\nexport async function runGateChecks(\n checks: ReadonlyArray<CheckSpec>,\n cwd: string,\n exec: ExecFn,\n): Promise<GateOutcome> {\n // Independent checks run concurrently; a throw or a non-zero exit is a fail,\n // never a kernel crash.\n const results = await Promise.all(\n checks.map(async (c) => {\n try {\n const r = await exec({ command: c.command, cwd })\n return { id: c.id, passed: r.exitCode === 0 }\n } catch {\n return { id: c.id, passed: false }\n }\n }),\n )\n const passed = new Set<string>()\n const ran = new Set<string>()\n for (const r of results) {\n ran.add(r.id)\n if (r.passed) passed.add(r.id)\n }\n return { passed, ran }\n}\n","/**\n * The SEALED executable-gate registry. The kernel owns the gate commands; the\n * workflow IR (and the model that drafted it) references a gate by its `id` ONLY\n * and can never author or alter the command that runs (invariant 5,\n * gate-immutability at the command level). `run_workflow` resolves the caller's\n * `gateId` against this map; an unknown id is rejected before the kernel runs, so\n * a model cannot smuggle an arbitrary shell command in disguised as a \"gate\".\n *\n * The check `id`s (not the commands) are what champion-retention compares, so the\n * selector reasons over stable category names (\"typecheck\"/\"test\"/\"lint\"), not\n * over shell strings that vary per repo.\n */\n\nimport { type CheckSpec } from \"./gate-runner\"\n\nexport interface SealedGate {\n id: string\n /** The sealed checks (id + command). Run by the kernel, never by the model. */\n checks: CheckSpec[]\n}\n\n/**\n * Built-in sealed gates. Commands follow this repo's TS/Bun conventions (the\n * `bun run <script>` indirection means a repo without that script simply fails\n * the check, which the selector treats as not-passed rather than a crash). New\n * ecosystems get a new sealed id here, never a caller-supplied command.\n */\nconst SEALED_GATES: Readonly<Record<string, ReadonlyArray<CheckSpec>>> = {\n \"default-ci\": [\n { id: \"typecheck\", command: \"bun run typecheck\" },\n { id: \"test\", command: \"bun test\" },\n { id: \"lint\", command: \"bun run lint\" },\n ],\n \"typecheck-test\": [\n { id: \"typecheck\", command: \"bun run typecheck\" },\n { id: \"test\", command: \"bun test\" },\n ],\n \"typecheck-only\": [{ id: \"typecheck\", command: \"bun run typecheck\" }],\n}\n\n/** The set of sealed gate ids, used as the kernel's `knownGateIds` so the IR\n * verifier rejects an executable gate that references an unregistered id. */\nexport function sealedGateIds(): ReadonlySet<string> {\n return new Set(Object.keys(SEALED_GATES))\n}\n\n/**\n * Resolve a sealed gate by id. Returns a DEFENSIVE CLONE (fresh objects) so a\n * caller can never mutate the registry's command set. `undefined` for an\n * unknown id, which `run_workflow` rejects before executing anything.\n */\nexport function resolveSealedGate(gateId: string): SealedGate | undefined {\n const checks = SEALED_GATES[gateId]\n if (!checks) return undefined\n return { id: gateId, checks: checks.map((c) => ({ id: c.id, command: c.command })) }\n}\n","/**\n * Gate-immutability detection (floor invariant 5). A producer must not weaken\n * the gates it is judged by — adding `.skip`, `@ts-ignore`, `as any`, or\n * `eslint-disable` turns a failing gate green without fixing anything, which is\n * the cheapest way to defeat the whole floor guarantee.\n *\n * `detectGateWeakening` scans the ADDED lines of a unified git diff for these\n * patterns. It is deliberately a syntactic heuristic over added lines only (a\n * removed `.skip` is a strengthening, not a weakening) — pure, dependency-free,\n * and used by BOTH the Phase-0 structural-gate Stop-hook (reject the diff) and\n * the kernel's runner (gate-immutability check before accepting an artifact).\n *\n * Patterns are LANGUAGE-SCOPED: each added line is tested only against a shared\n * COMMON set plus the patterns for the file's language (by extension, derived\n * from the diff header). An unknown extension is tested against COMMON only —\n * failing OPEN (no false block) is the safe direction for a Stop hook. This both\n * generalizes beyond TS/JS and removes cross-language false positives (e.g. a Go\n * `.only(` substring is no longer flagged as a skipped JS test).\n */\n\nexport interface WeakeningFinding {\n /** Stable category (e.g. \"skipped-test\", \"any-cast\"). */\n pattern: string\n /** The offending added line (trimmed) for the report. */\n line: string\n /** The file the line was added to, when derivable from the diff header. */\n file?: string\n}\n\nexport interface GateImmutabilityResult {\n weakened: boolean\n findings: WeakeningFinding[]\n}\n\ninterface WeakeningPattern {\n name: string\n re: RegExp\n /** Where the syntax is meaningful: executable code or a comment directive. */\n scope: \"code\" | \"slash-comment\" | \"hash-comment\"\n}\n\n/** Patterns that weaken a gate regardless of language (test exclusivity / focus\n * idioms shared across JS test runners; kept in COMMON since several languages\n * reuse them). */\nconst COMMON_PATTERNS: ReadonlyArray<WeakeningPattern> = [\n // Disabling tests: jest/bun/mocha skip + exclusive-focus (`.only` narrows the\n // suite so other failures stop running — also a weakening).\n { name: \"skipped-test\", re: /(\\.\\s*skip\\s*\\(|\\bxit\\s*\\(|\\bxdescribe\\s*\\(|\\.\\s*only\\s*\\()/, scope: \"code\" },\n]\n\n/** Language-specific weakening patterns, selected by the added line's file type. */\nconst LANG_PATTERNS: Readonly<Record<string, ReadonlyArray<WeakeningPattern>>> = {\n ts: [\n // Silencing the type-checker. Scoped to comments because the directive lives there.\n { name: \"ts-suppression\", re: /@ts-(ignore|nocheck|expect-error)\\b/, scope: \"slash-comment\" },\n // Casting away type errors.\n { name: \"any-cast\", re: /\\bas\\s+any\\b|:\\s*any\\b/, scope: \"code\" },\n // Silencing the linter. Scoped to comments because the directive lives there.\n { name: \"eslint-disable\", re: /eslint-disable\\b/, scope: \"slash-comment\" },\n ],\n py: [\n { name: \"py-type-ignore\", re: /#\\s*type:\\s*ignore\\b/, scope: \"hash-comment\" },\n { name: \"py-noqa\", re: /#\\s*noqa\\b/, scope: \"hash-comment\" },\n { name: \"py-skip\", re: /@(pytest\\.mark\\.skip|unittest\\.skip)\\b/, scope: \"code\" },\n ],\n go: [\n { name: \"go-skip\", re: /\\bt\\.Skip\\s*\\(/, scope: \"code\" },\n { name: \"go-nolint\", re: /\\/\\/\\s*nolint\\b/, scope: \"slash-comment\" },\n ],\n rust: [\n { name: \"rust-ignore\", re: /#\\[\\s*ignore\\b/, scope: \"code\" },\n { name: \"rust-allow\", re: /#\\[\\s*allow\\s*\\(/, scope: \"code\" },\n ],\n}\n\n/** Map a file path to a language key for `LANG_PATTERNS` (null → COMMON only). */\nfunction langForFile(file: string | undefined): keyof typeof LANG_PATTERNS | null {\n if (!file) return null\n const ext = file.slice(file.lastIndexOf(\".\")).toLowerCase()\n switch (ext) {\n case \".ts\":\n case \".tsx\":\n case \".mts\":\n case \".cts\":\n case \".js\":\n case \".jsx\":\n case \".mjs\":\n case \".cjs\":\n return \"ts\"\n case \".py\":\n case \".pyi\":\n return \"py\"\n case \".go\":\n return \"go\"\n case \".rs\":\n return \"rust\"\n default:\n return null\n }\n}\n\n/** Union of COMMON + every language — the pre-header default (no file known yet),\n * so a header-less diff is matched permissively, exactly as before this became\n * language-scoped. Once a real file header appears, the set narrows to that\n * file's language (or COMMON only for an unknown extension → fail open). */\nconst ALL_PATTERNS: ReadonlyArray<WeakeningPattern> = [\n ...COMMON_PATTERNS,\n ...Object.values(LANG_PATTERNS).flat(),\n]\n\n/** Lexical views of one added line. Quoted text is omitted from every view;\n * code excludes comments, while directive views retain only their comment text.\n * This prevents fixture/documentation strings from self-matching without hiding\n * real comment-based suppressions. It is intentionally conservative: an\n * unterminated quote returns the raw line as code and no comment views. */\nfunction lexicalViews(line: string): { code: string; slashComment: string; hashComment: string } {\n let code = \"\"\n let slashComment = \"\"\n let hashComment = \"\"\n let quote: \"'\" | '\"' | \"`\" | null = null\n let escaped = false\n for (let i = 0; i < line.length; i++) {\n const ch = line[i]!\n if (quote !== null) {\n if (escaped) {\n escaped = false\n continue\n }\n if (ch === \"\\\\\") {\n escaped = true\n continue\n }\n if (ch === quote) quote = null\n continue\n }\n if (ch === \"'\" || ch === '\"' || ch === \"`\") {\n quote = ch\n code += \" \"\n continue\n }\n if (ch === \"/\" && line[i + 1] === \"/\") {\n slashComment = line.slice(i)\n break\n }\n if (ch === \"/\" && line[i + 1] === \"*\") {\n slashComment = line.slice(i)\n break\n }\n // `#` starts a comment for Python, but Rust attributes begin `#[` and are\n // executable syntax that must remain in the code view.\n if (ch === \"#\" && line[i + 1] !== \"[\") {\n hashComment = line.slice(i)\n break\n }\n code += ch\n }\n return quote === null\n ? { code, slashComment, hashComment }\n : { code: line, slashComment: \"\", hashComment: \"\" }\n}\n\n/** A `diff --git a/x b/x` or `+++ b/x` header → the current file path. */\nfunction fileFromHeader(line: string): string | undefined {\n const git = /^diff --git a\\/.+ b\\/(.+)$/.exec(line)\n if (git) return git[1]\n const plus = /^\\+\\+\\+ b\\/(.+)$/.exec(line)\n if (plus) return plus[1]\n return undefined\n}\n\nexport function detectGateWeakening(diff: string): GateImmutabilityResult {\n const findings: WeakeningFinding[] = []\n let file: string | undefined\n let patterns: ReadonlyArray<WeakeningPattern> = ALL_PATTERNS\n for (const raw of diff.split(\"\\n\")) {\n const headerFile = fileFromHeader(raw)\n if (headerFile !== undefined) {\n file = headerFile\n const lang = langForFile(file)\n patterns = lang ? [...COMMON_PATTERNS, ...LANG_PATTERNS[lang]] : COMMON_PATTERNS\n continue\n }\n // Only ADDED content lines (skip the `+++` file header and context/removed).\n if (!raw.startsWith(\"+\") || raw.startsWith(\"+++\")) continue\n const added = raw.slice(1)\n const views = lexicalViews(added)\n for (const p of patterns) {\n const searchable = p.scope === \"code\"\n ? views.code\n : p.scope === \"slash-comment\"\n ? views.slashComment\n : views.hashComment\n if (p.re.test(searchable)) {\n findings.push(file === undefined ? { pattern: p.name, line: added.trim() } : { pattern: p.name, line: added.trim(), file })\n }\n }\n }\n return { weakened: findings.length > 0, findings }\n}\n","/**\n * The Phase-0 structural-gate decision — the cheap, CERTAIN floor win. It blocks\n * marking work \"done\" when any canonical gate fails OR the diff weakens a gate.\n * A non-model check that fires every run and never ships broken or gate-gamed\n * code; per the floor analysis it is the only component that raises the\n * worst-case floor BY CONSTRUCTION (the orchestration on top is a conditional\n * bet).\n *\n * This is the logic behind the spawned-session Stop-hook AND the kernel runner's\n * pre-accept check. The process exec and the diff are inputs, so the decision is\n * pure + unit-testable; the hook script that runs `git diff` + invokes this is a\n * thin wrapper.\n */\n\nimport { detectGateWeakening, type WeakeningFinding } from \"./gate-immutability\"\nimport { runGateChecks, type CheckSpec, type ExecFn } from \"./gate-runner\"\n\nexport interface StopGateInput {\n /** The canonical gate commands (tests/types/lint/build). */\n checks: ReadonlyArray<CheckSpec>\n /** Workspace the checks run in. */\n cwd: string\n /** Injected process exec (the live wrapper spawns `command` in `cwd`). */\n exec: ExecFn\n /** The working-tree diff to scan for gate-weakening (e.g. `git diff HEAD`). */\n diff: string\n}\n\nexport interface StopGateResult {\n /** True ⇒ refuse \"done\": a gate is red or the diff weakens a gate. */\n block: boolean\n /** Human-readable summary for the hook's stderr / the kernel's reason. */\n reason: string\n /** Canonical check ids that did not pass. */\n failedChecks: string[]\n /** Gate-weakening findings in the diff (added `.skip` / `as any` / …). */\n weakening: WeakeningFinding[]\n}\n\nexport async function evaluateStopGate(input: StopGateInput): Promise<StopGateResult> {\n const gate = await runGateChecks(input.checks, input.cwd, input.exec)\n const weak = detectGateWeakening(input.diff)\n\n const failedChecks = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id))\n const block = failedChecks.length > 0 || weak.weakened\n\n const parts: string[] = []\n if (failedChecks.length > 0) parts.push(`failing gates: ${failedChecks.join(\", \")}`)\n if (weak.weakened) {\n const pats = [...new Set(weak.findings.map((f) => f.pattern))].join(\", \")\n parts.push(`gate-weakening in the diff: ${pats}`)\n }\n\n return {\n block,\n reason: block ? parts.join(\"; \") : \"all canonical gates pass; no gate-weakening in the diff\",\n failedChecks,\n weakening: weak.findings,\n }\n}\n","/**\n * The Phase-0 structural-gate Stop-hook glue. `runStopGateForLaunch` resolves a\n * SEALED gate by id and evaluates it against the working-tree diff, returning the\n * block decision a spawned-session Stop hook (or a manual check) acts on.\n *\n * Safety posture (deliberately different from the kernel): a Stop hook that\n * blocks on a MISCONFIGURATION would wedge the user's session (they could not\n * stop). So this fails OPEN on config problems (unknown gate id) and blocks ONLY\n * on a genuine red gate or a gate-weakening diff. The kernel, by contrast, fails\n * CLOSED to the baseline. Both are correct for their context.\n *\n * The sealed gate + diff + exec are inputs, so the decision is pure and\n * unit-testable; the thin live wrapper (capture `git diff`, spawn the checks,\n * map the result to an exit code) is the only part that needs a real process.\n */\n\nimport { buildSelfCommand, type SelfInvocation } from \"../hook-launcher/self-invocation\"\nimport { resolveSealedGate } from \"./gate-registry\"\nimport { runGateChecks, type CheckSpec, type ExecFn } from \"./gate-runner\"\nimport { evaluateStopGate, type StopGateResult } from \"./stop-gate\"\nimport { isSubagentContext, regressions, type BaselineStore, type ReviewDebounce } from \"./stop-gate-policy\"\nimport { parseBoolEnv } from \"~/lib/exec\"\n\nimport { createHash } from \"node:crypto\"\nimport { promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nexport interface StopGateLaunchInput {\n /** Workspace the checks run in (the session cwd). */\n workspace: string\n /** Which SEALED gate to run (the registry owns the commands). */\n gateId: string\n /** Injected process exec. */\n exec: ExecFn\n /** The working-tree diff to scan for gate-weakening (e.g. `git diff HEAD`). */\n diff: string\n}\n\nexport async function runStopGateForLaunch(input: StopGateLaunchInput): Promise<StopGateResult> {\n const gate = resolveSealedGate(input.gateId)\n if (!gate) {\n // Fail OPEN: never wedge the session on a config error. Surface it instead.\n return {\n block: false,\n reason: `stop-gate: unknown gateId \"${input.gateId}\" (not blocking)`,\n failedChecks: [],\n weakening: [],\n }\n }\n return evaluateStopGate({ checks: gate.checks, cwd: input.workspace, exec: input.exec, diff: input.diff })\n}\n\n/**\n * The launch-stable baseline key for the dynamic gate path: keyed by the\n * workdir + the descriptor's check set + a per-launch token, NOT the session id\n * and NOT HEAD. The LAUNCHER generates the token (passed to the child via env),\n * computes this key, and seeds the baseline (the set of checks already failing at\n * launch, BEFORE the agent mutates the tree); the runtime hook recomputes the\n * same key (reading the token from its env) and reads that baseline, so an\n * agent-introduced failure is a REGRESSION rather than being silently adopted as\n * the baseline at first stop. The per-launch token isolates concurrent sessions\n * on the same repo (they don't clobber each other's baseline). HEAD is\n * deliberately NOT in the key: an agent that COMMITS its work mid-session must\n * still be judged against the pre-session baseline (a HEAD-keyed baseline would\n * vanish on the commit — the exact moment it matters). A changed check set (the\n * agent edited the harness) yields a different key → the launch baseline no\n * longer matches and the hook degrades to first-stop baselining.\n */\nexport function launchBaselineKey(workdir: string, descriptorKey: string, token?: string): string {\n return JSON.stringify([\"launch\", nodePath.resolve(workdir), descriptorKey, token ?? \"\"])\n}\n\n/**\n * Run the resolved checks ONCE at launch (pre-mutation) and record the failing\n * set as the baseline under `launchBaselineKey`. Best-effort: any failure is\n * swallowed (the hook then degrades to first-stop baselining). The launcher MUST\n * pass the SAME check set the hook will run (same `includeTests`) so a\n * pre-existing test failure isn't later miscounted as a regression.\n *\n * NOTE: capture is best-effort pre-mutation — the launcher fires it before the\n * child can edit, and in practice the agent does not mutate until well after a\n * user prompt, so the checks see the clean tree. If the agent somehow mutates\n * before this completes, the worst case is the legacy first-stop baseline (no\n * regression, never a wedge).\n */\nexport async function captureLaunchBaseline(input: {\n checks: ReadonlyArray<CheckSpec>\n workdir: string\n exec: ExecFn\n descriptorKey: string\n token?: string\n baseline: BaselineStore\n}): Promise<void> {\n if (input.checks.length === 0) return\n try {\n const gate = await runGateChecks(input.checks, input.workdir, input.exec)\n const failed = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id))\n await input.baseline.set(launchBaselineKey(input.workdir, input.descriptorKey, input.token), failed)\n } catch {\n /* best-effort: a launch-baseline miss only downgrades to first-stop baselining. */\n }\n}\n\n/**\n * The structural-gate Stop hook is OPT-IN and default-OFF: it changes the spawned\n * session's stop behavior (a red gate refuses \"done\"), so a user enables it\n * explicitly via `GH_ROUTER_ENABLE_STOP_GATE` (the canonical `parseBoolEnv`\n * accepts `1`/`true`/`yes`/`on`).\n */\nexport function stopGateEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_ENABLE_STOP_GATE) === true\n}\n\n/**\n * The advisory background review (hook V2) is ON by default whenever the Stop\n * gate runs; it is the cross-lab accountability layer. Opt out with\n * `GH_ROUTER_DISABLE_STOP_REVIEW=1` to keep the deterministic gate but drop the\n * LLM review. (Disabling the whole gate with `GH_ROUTER_DISABLE_STOP_GATE=1`\n * also drops the review, since the review only ever fires from the gate's green\n * path.)\n */\nexport function stopReviewEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_REVIEW) !== true\n}\n\n/** The sealed gate the Stop hook runs, overridable via `GH_ROUTER_STOP_GATE_ID`\n * (must be a registered sealed id; the live wrapper falls open on an unknown\n * id). Defaults to `default-ci`. */\nexport function stopGateId(env: NodeJS.ProcessEnv = process.env): string {\n const v = (env.GH_ROUTER_STOP_GATE_ID ?? \"\").trim()\n return v.length > 0 ? v : \"default-ci\"\n}\n\n/**\n * Is a unified-diff file path confined to a `plans/` or `memory/` directory?\n * Windows-safe (normalizes `\\` → `/`). Matches only when a DIRECTORY segment of\n * the path is exactly `plans` or `memory`, so `plans/x.md`, `memory/y.md`, and\n * `a/plans/z` match while the filenames `docs/plans.md` / `myplans/x` do not.\n */\nfunction isPlanMemoryDiffPath(p: string): boolean {\n const segs = p.replace(/\\\\/g, \"/\").split(\"/\")\n // Only directory segments count — exclude the final (filename) segment.\n for (let i = 0; i < segs.length - 1; i++) {\n if (segs[i] === \"plans\" || segs[i] === \"memory\") return true\n }\n return false\n}\n\n/**\n * Drop the file sections of a unified `git diff` whose NEW path (the `b/…` side)\n * is confined to `plans/` or `memory/` — used for the gate-WEAKENING scan when\n * plan mode is active. Sections for other files (e.g. `src/…`) are preserved\n * verbatim, so a MIXED diff keeps its non-plan hunks and `src/` weakening is\n * still caught. The executable gate does not read the diff, so filtering it here\n * never affects which checks run. A diff with no `diff --git` sections (or an\n * empty one) is returned unchanged.\n */\nexport function stripPlanMemoryDiffHunks(diff: string): string {\n if (diff.length === 0) return diff\n const out: string[] = []\n let dropping = false\n for (const line of diff.split(\"\\n\")) {\n if (line.startsWith(\"diff --git \")) {\n const m = /^diff --git a\\/.+ b\\/(.+)$/.exec(line)\n const bPath = m?.[1]\n dropping = bPath !== undefined && isPlanMemoryDiffPath(bPath)\n if (!dropping) out.push(line)\n continue\n }\n if (!dropping) out.push(line)\n }\n return out.join(\"\\n\")\n}\n\n/**\n * C3: whether the structural Stop-gate is disabled for THIS launch. True when the\n * `GH_ROUTER_DISABLE_STOP_GATE` env is set OR the `--no-stop-gate` launcher flag was\n * passed (`args[\"no-stop-gate\"] === true`). A DRIVEN session sets the flag so a\n * blocking Stop hook never hangs its turn-end (~10 min) waiting on the fleet driver.\n * Pure so it is unit-testable without the live launch path.\n */\nexport function stopGateDisabled(\n args: Record<string, unknown>,\n env: NodeJS.ProcessEnv = process.env,\n): boolean {\n return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true || args[\"no-stop-gate\"] === true\n}\n\n/**\n * Build the Claude Code `settings.json` fragment that registers `command` as a\n * Stop hook. Returns just the `hooks` object so the caller merges it into the\n * mirrored settings (never clobbering existing hooks). The Stop event takes no\n * matcher; the command runs on every stop and an exit code of 2 blocks it.\n */\nexport function buildStopHookSettings(command: string): {\n hooks: { Stop: Array<{ hooks: Array<{ type: \"command\"; command: string }> }> }\n} {\n return { hooks: { Stop: [{ hooks: [{ type: \"command\", command }] }] } }\n}\n\n/** True when a settings `Stop` entry already registers `command` (so the merge\n * is idempotent across re-launches). */\nfunction entryHasCommand(entry: unknown, command: string): boolean {\n if (!entry || typeof entry !== \"object\") return false\n const hooks = (entry as { hooks?: unknown }).hooks\n if (!Array.isArray(hooks)) return false\n return hooks.some((h) => h && typeof h === \"object\" && (h as { command?: unknown }).command === command)\n}\n\n/**\n * Idempotently merge a hook running `command` for `event` (default `Stop`) into\n * an existing Claude Code settings object WITHOUT clobbering other hook events or\n * other entries. Returns a new object (never mutates the input). Re-running the\n * launcher with the same command+event does not duplicate the hook.\n */\nexport function mergeStopHookIntoSettings(\n existing: Record<string, unknown> | undefined,\n command: string,\n event: string = \"Stop\",\n timeoutSec?: number,\n matcher?: string,\n): Record<string, unknown> {\n const base: Record<string, unknown> = existing && typeof existing === \"object\" ? { ...existing } : {}\n const hooks: Record<string, unknown> =\n base.hooks && typeof base.hooks === \"object\" ? { ...(base.hooks as Record<string, unknown>) } : {}\n const arr: unknown[] = Array.isArray(hooks[event]) ? [...(hooks[event] as unknown[])] : []\n if (!arr.some((e) => entryHasCommand(e, command))) {\n const hook: { type: \"command\"; command: string; timeout?: number } = { type: \"command\", command }\n if (typeof timeoutSec === \"number\" && Number.isFinite(timeoutSec) && timeoutSec > 0) {\n hook.timeout = timeoutSec\n }\n arr.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] })\n }\n hooks[event] = arr\n base.hooks = hooks\n return base\n}\n\n/**\n * The Stop-hook subcommand decision, factored out so it is pure + unit-testable.\n * Given the raw stdin JSON Claude Code sends, the sealed gate id, an exec, a\n * diff-capture, and a persistent per-session block budget, it returns the exit\n * code (2 blocks, 0 allows) and the stderr Claude reads on a block.\n *\n * TERMINATION GUARANTEE (the one property that must hold: never wedge the\n * session). Three independent stand-down paths, any of which yields exit 0:\n * 1. unparseable stdin or no session_id -> can't budget-track safely, allow;\n * 2. Claude Code's own `stop_hook_active` re-entry signal;\n * 3. a HARD per-session block budget (`maxBlocks`, default 3): after that many\n * blocks for one session_id the gate ALWAYS allows the stop. This is the\n * load-bearing guard, because `stop_hook_active` can reset when the model\n * does intervening tool calls between stop attempts (so it alone does NOT\n * bound the loop). Any budget IO failure also stands down (can't guarantee\n * termination -> don't block).\n * The gate blocks only when ALL of these allow it AND the executable gate is red\n * or the diff weakens a gate.\n */\nexport interface StopHookDecision {\n exitCode: 0 | 2\n /** stderr text shown to Claude on a block (exit 2). */\n stderr?: string\n}\n\n/** Inputs the detached background reviewer needs (assembled on the green path). */\nexport interface StopReviewContext {\n sessionId: string\n cwd: string\n diff: string\n diffHash: string\n}\n\n/** A persistent per-session count of how many times the gate has blocked, so the\n * budget survives across separate hook-process invocations within one session. */\nexport interface BlockBudget {\n count: (sessionId: string) => Promise<number>\n record: (sessionId: string) => Promise<void>\n /** Clear the count for a session (called by the per-prompt reset hook so the\n * budget is per-user-prompt, not per-session-lifetime). */\n reset: (sessionId: string) => Promise<void>\n}\n\nexport async function decideStopHook(input: {\n /** Raw stdin from Claude Code (a JSON payload; tolerated if malformed). */\n stdin: string\n gateId: string\n exec: ExecFn\n /** Capture the working-tree diff for the session cwd (e.g. `git diff HEAD`). */\n captureDiff: (cwd: string) => Promise<string>\n /** cwd to use when the payload omits one. */\n fallbackCwd: string\n /** Persistent per-session block budget (the hard termination guard). */\n budget: BlockBudget\n /** Per-session baseline of pre-existing failures (block only on regressions). */\n baseline: BaselineStore\n /**\n * Runtime trust re-check: act ONLY in a repo the user consented to (or\n * force-enabled). Injected for testability; the live wrapper passes\n * `stopGateEnabledForRepo`. This is the security gate — even though the\n * launcher only registers the hook for a trusted repo, re-checking at runtime\n * against `payload.cwd` defends against a mid-session cwd change.\n */\n isEnabledForRepo: (cwd: string) => Promise<boolean>\n /**\n * DYNAMIC gate source (language-agnostic path). When provided, the gate's\n * check set is resolved at decision time from this resolver instead of the\n * sealed `gateId` registry — re-derived from the live tree (parser path) or\n * read from the cached evidence-pinned record (discovered path). It returns:\n * - `checks` : the canonical `{id, command}` set to run;\n * - `workdir` : the dir the checks run in (the repo/package root where\n * the evidence was found — NOT the Stop payload cwd, so a\n * monorepo stop from a nested dir runs the root's checks);\n * - `descriptorKey` : a stable key over the check set for baseline isolation.\n * Returning `null` (no gate resolvable now — markers/tools vanished, empty set)\n * makes the hook FAIL OPEN (exit 0), never wedging the session. When absent the\n * hook falls through to the sealed `gateId` path (bun/TS, unchanged).\n */\n resolveChecks?: (\n cwd: string,\n ) => Promise<{ checks: CheckSpec[]; workdir: string; descriptorKey: string; baselineKey?: string } | null>\n /** Max blocks per prompt before the gate always allows (default 2). */\n maxBlocks?: number\n /** Absolute wall-clock cap on the diff+gate evaluation; on timeout the hook\n * FAILS OPEN (exit 0) and never claims the gate passed. Default 300s. */\n timeoutMs?: number\n /**\n * Advisory-review layer (hook V2), entirely OPTIONAL and side-effect-only.\n * When BOTH `reviewDebounce` and `spawnReview` are provided, a GREEN stop with\n * a substantive diff (and a not-yet-reviewed diff hash) spawns a detached,\n * background read-only review. This NEVER affects the exit code — the\n * deterministic gate above is the only blocker. Any failure in this block is\n * swallowed (the stop still ends exit 0). Omitted in tests / when\n * `GH_ROUTER_DISABLE_STOP_REVIEW` is set → the deterministic gate is unchanged.\n */\n reviewDebounce?: ReviewDebounce\n /** Fire-and-forget spawn of the detached background reviewer. Must return\n * immediately (detached + unref'd); decideStopHook never awaits the review. */\n spawnReview?: (ctx: StopReviewContext) => void\n}): Promise<StopHookDecision> {\n const maxBlocks = input.maxBlocks ?? 2\n let payload: { cwd?: unknown; session_id?: unknown; agent_id?: unknown; agent_type?: unknown; plan_mode?: unknown } = {}\n let parsed = false\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") {\n payload = p as typeof payload\n parsed = true\n }\n } catch {\n // tolerate a non-JSON stdin.\n }\n // (1) abnormal payload -> fail OPEN (can't budget-track safely).\n if (!parsed) return { exitCode: 0 }\n // (2) SCOPING: stand down in any subagent / teammate context. A Stop hook is\n // converted to SubagentStop for subagents, so this command CAN run there;\n // the payload's agent_type/agent_id mark it. The gate is top-level-only.\n if (isSubagentContext(payload)) return { exitCode: 0 }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n // (3) without a session id we cannot enforce the termination budget -> allow.\n if (!sessionId) return { exitCode: 0 }\n const cwdRaw = typeof payload.cwd === \"string\" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd\n // Resolve symlinks ONCE so the consent check and the gate's exec target are\n // the same immutable path — defends against a symlink/rename swap between the\n // trust check and the exec (codex review #3). Keep the raw path if realpath\n // fails (e.g. the dir was removed) — the consent check then simply denies.\n let cwd = cwdRaw\n try {\n cwd = await fs.realpath(cwdRaw)\n } catch {\n /* keep raw */\n }\n // (4) CONSENT: only run the repo's scripts if the user trusted it (or forced).\n // Any failure -> allow (never wedge, never run untrusted code).\n let enabled: boolean\n try {\n enabled = await input.isEnabledForRepo(cwd)\n } catch {\n return { exitCode: 0 }\n }\n if (!enabled) return { exitCode: 0 }\n // (5) hard per-prompt budget: never block more than maxBlocks times per\n // prompt. This — NOT the (now-absent) stop_hook_active flag — is the\n // termination guard; relying on stop_hook_active would defeat block-twice.\n let priorBlocks: number\n try {\n priorBlocks = await input.budget.count(sessionId)\n } catch {\n return { exitCode: 0 } // can't read the budget -> can't guarantee termination -> allow.\n }\n if (priorBlocks >= maxBlocks) {\n // Termination guard: never block more than maxBlocks times per prompt. This\n // is a deliberate trade-off (a stubborn agent gets through), so make the\n // stand-down LOUD rather than a silent exit 0 — the user must know broken\n // work may be shipping unverified.\n return {\n exitCode: 0,\n stderr:\n `structural gate: reached the ${maxBlocks}-block limit for this prompt; allowing the stop `\n + `WITHOUT re-running the checks. If the failures from the last block are unfixed, they are `\n + `shipping — run the checks manually to verify.`,\n }\n }\n\n // (6) Run the gate under an absolute timeout. The gate result is computed\n // inside the raced promise; the BASELINE read/write happens AFTER the race\n // resolves to a real result, so a timed-out (abandoned) eval can never\n // seed or poison the baseline (codex review #5). The gate's child process\n // is bounded + tree-killed by liveExec's per-command timeout, and the\n // wrapper process.exit()s, so a timeout never wedges the session.\n //\n // The working-tree diff is captured INSIDE the raced promise (so a stalled\n // `git diff` is covered by the same absolute timeout, not run unbounded on\n // the critical path) and RETURNED so the advisory review below judges the\n // exact same tree without a second capture. captureDiff swallows its own\n // failures to \"\".\n // The check set comes from the dynamic resolver when injected (parser /\n // discovered path) or the sealed `gateId` registry otherwise. `resolvedKey`\n // (read AFTER the race) keys the baseline so a changed command set gets a\n // fresh baseline. `resolvedWorkdir` is where the checks run (the repo root the\n // evidence was found in, not necessarily the payload cwd — monorepo fix).\n let resolvedKey = input.gateId\n // When the dynamic resolver supplies a launch-stable baseline key (keyed by\n // workdir + HEAD + descriptor, NOT the session), the gate reads the baseline\n // the LAUNCHER captured BEFORE the agent mutated the tree — so an\n // agent-introduced failure is a regression, not silently adopted as baseline.\n let dynamicBaselineKey: string | undefined\n // Forward-compatible payload signal. Current Claude Code does not emit\n // `plan_mode`, but preserving this path keeps the prior diff-scoping behavior\n // if it does. The dead env fallback is deliberately gone.\n const scanDiff = (diff: string): string =>\n payload.plan_mode === true ? stripPlanMemoryDiffHunks(diff) : diff\n type GateRunResult =\n | { kind: \"evaluated\"; failedChecks: string[]; weakeningPatterns: string[]; diff: string }\n | { kind: \"no-diff\"; diff: string }\n const capturedDiff = async (workdir: string): Promise<{ diff: string; captured: boolean }> => {\n try {\n return { diff: await input.captureDiff(workdir), captured: true }\n } catch {\n // A failed diff capture is NOT an empty diff. Preserve the old safe behavior:\n // run the executable checks with an empty weakening scan rather than skipping.\n return { diff: \"\", captured: false }\n }\n }\n const runGate = async (): Promise<GateRunResult | null> => {\n if (input.resolveChecks) {\n const resolved = await input.resolveChecks(cwd).catch(() => null)\n // No gate resolvable now (markers/tools vanished, empty set) → fail OPEN.\n if (!resolved || resolved.checks.length === 0) return null\n resolvedKey = resolved.descriptorKey\n dynamicBaselineKey = resolved.baselineKey\n const workdir = resolved.workdir || cwd\n const { diff, captured } = await capturedDiff(workdir)\n if (captured && diff.length === 0) return { kind: \"no-diff\", diff }\n const result = await evaluateStopGate({ checks: resolved.checks, cwd: workdir, exec: input.exec, diff: captured ? scanDiff(diff) : \"\" })\n return {\n kind: \"evaluated\",\n failedChecks: [...result.failedChecks],\n weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],\n diff,\n }\n }\n const { diff, captured } = await capturedDiff(cwd)\n if (captured && diff.length === 0) return { kind: \"no-diff\", diff }\n const result = await runStopGateForLaunch({ workspace: cwd, gateId: input.gateId, exec: input.exec, diff: captured ? scanDiff(diff) : \"\" })\n return {\n kind: \"evaluated\",\n failedChecks: [...result.failedChecks],\n weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],\n diff,\n }\n }\n const timeoutMs = input.timeoutMs ?? 300_000\n let timer: ReturnType<typeof setTimeout> | undefined\n let raced: GateRunResult | null | \"timeout\"\n try {\n raced = await Promise.race<GateRunResult | null | \"timeout\">([\n runGate(),\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeoutMs)\n // Never keep this short-lived hook process alive on the timer alone.\n timer.unref?.()\n }),\n ])\n } finally {\n // Clear in `finally`: a `runGate()` REJECTION previously skipped the\n // clearTimeout below it, leaving a 5-minute timer pinning the hook process\n // open long after it had nothing left to do.\n if (timer) clearTimeout(timer)\n }\n if (raced === \"timeout\") return { exitCode: 0 } // gate did not complete -> allow; never a claimed pass.\n if (raced === null) return { exitCode: 0 } // no gate resolvable now -> allow (fail open).\n if (raced.kind === \"no-diff\") return { exitCode: 0 } // no working-tree diff -> no checks to run.\n\n // Baseline isolation, keyed by (session, workdir, descriptor) so a mid-session\n // repo/gate change can't mask or invent regressions (codex review #7). The\n // dynamic path prefers a launch-stable key (pre-mutation baseline). Only a\n // COMPLETED eval reads/writes it.\n const baselineKey = dynamicBaselineKey ?? JSON.stringify([sessionId, cwd, resolvedKey])\n const recorded = await input.baseline.get(baselineKey).catch(() => null)\n if (recorded === null) {\n await input.baseline.set(baselineKey, raced.failedChecks).catch(() => {})\n }\n const regressed = regressions(raced.failedChecks, recorded)\n const weakened = raced.weakeningPatterns.length > 0\n if (regressed.length === 0 && !weakened) {\n // GREEN stop. Fire the advisory background review (never blocks, never\n // changes the exit code). Fully isolated + time-bounded so a store or spawn\n // failure (or a stalled debounce read/write) can't turn a clean stop into\n // anything but a prompt exit 0.\n await maybeSpawnReview(input, sessionId, cwd, raced.diff)\n return { exitCode: 0 }\n }\n try {\n await input.budget.record(sessionId)\n } catch {\n return { exitCode: 0 } // can't persist the block -> can't bound the loop -> allow.\n }\n const parts: string[] = []\n if (regressed.length > 0) parts.push(`regressed gates: ${regressed.join(\", \")}`)\n if (weakened) parts.push(`gate-weakening in the diff: ${raced.weakeningPatterns.join(\", \")}`)\n return {\n exitCode: 2,\n stderr:\n `structural gate failed (block ${priorBlocks + 1}/${maxBlocks}): ${parts.join(\"; \")}. `\n + `Fix the failing checks and revert any gate-weakening (no new .skip / as any / `\n + `lint-disable) before finishing.`,\n }\n}\n\n/**\n * The advisory-review side-effect on a GREEN stop: debounce by diff hash, then\n * fire the detached background reviewer. ADVISORY-ONLY — it returns void, never\n * throws (every step is swallowed), and the caller does not await its result for\n * the exit decision. A no-op when the review layer isn't wired (no debounce /\n * spawn injected, e.g. GH_ROUTER_DISABLE_STOP_REVIEW) or the diff is empty.\n *\n * `markReviewed` runs BEFORE the spawn so a crashing spawn still records the\n * debounce (an identical tree won't re-trigger on the next stop). The review is\n * gated on the diff CHANGING since the last review — without it, every stop of\n * an unchanged tree would re-spend a background gpt-5.6-sol review.\n *\n * The whole body is bounded by a short timeout (the stores are local temp files\n * that complete in well under a millisecond in practice, so the timeout never\n * fires normally — but if the debounce read/write ever stalled, the stop must\n * still proceed promptly; the advisory layer never delays a clean stop).\n */\nconst REVIEW_SIDE_EFFECT_BUDGET_MS = 2_000\n\nasync function maybeSpawnReview(\n input: { reviewDebounce?: ReviewDebounce; spawnReview?: (ctx: StopReviewContext) => void },\n sessionId: string,\n cwd: string,\n diff: string,\n): Promise<void> {\n if (!input.reviewDebounce || !input.spawnReview) return\n if (diff.trim().length === 0) return // no substantive diff -> nothing to review.\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const work = (async (): Promise<void> => {\n const diffHash = createHash(\"sha256\").update(diff).digest(\"hex\")\n if (!(await input.reviewDebounce!.shouldReview(sessionId, diffHash))) return\n await input.reviewDebounce!.markReviewed(sessionId, diffHash)\n input.spawnReview!({ sessionId, cwd, diff, diffHash })\n })()\n await Promise.race([\n work,\n new Promise<void>((resolve) => {\n timer = setTimeout(resolve, REVIEW_SIDE_EFFECT_BUDGET_MS)\n }),\n ])\n } catch {\n // Advisory layer must never affect the stop. Swallow everything.\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/**\n * A file-backed `BlockBudget` under `stateDir`, keyed by a hash of the session id\n * (so a session id is never written verbatim to a predictable path). Best-effort:\n * a read miss counts as 0; `record` increments. A write/read error propagates so\n * `decideStopHook` stands down (it can't guarantee termination without the\n * budget).\n */\nexport function fileBlockBudget(stateDir: string): BlockBudget {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `block-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n const readCount = async (sid: string): Promise<number> => {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) && n > 0 ? n : 0\n } catch {\n return 0 // a miss is 0 blocks so far.\n }\n }\n return {\n count: readCount,\n async record(sid) {\n const next = (await readCount(sid)) + 1\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), String(next), { mode: 0o600 })\n },\n async reset(sid) {\n await fs.unlink(fileFor(sid)).catch(() => {})\n },\n }\n}\n\n/** Build the shell command Claude Code runs for the Stop hook. */\nexport function buildStopHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-stop-hook\")\n}\n\n/**\n * Build the shell command Claude Code runs for the SessionStart/SessionEnd hooks\n * (registered only when github-router runs inside an ai-or-die Terminal tab). The\n * sidecar path is baked in as a literal `--out` arg — NOT passed via env — so it\n * survives `AIORDIE_CLAUDE_BIND` being stripped from the child's environment and a\n * nested `github-router claude` can't inherit it. Pure (binary + script + out\n * paths) for unit-testable quoting; the live firing is verified end-to-end.\n */\nexport function buildSessionBindHookCommand(\n invocation: SelfInvocation,\n outPath: string,\n): string {\n return buildSelfCommand(invocation, `internal-session-bind --out \"${outPath}\"`)\n}\n\n/** Command for the `internal-artifact-open` hook (no args — token comes from the\n * mirror creds file, plan from the plans dir; nothing secret in argv). */\nexport function buildArtifactOpenHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-artifact-open\")\n}\n\n/**\n * Read-merge-atomic-write the Stop hook into a Claude Code `settings.json` file\n * (the mirrored one). A MISSING file (ENOENT) starts from `{}`; any OTHER read or\n * parse error THROWS (the caller's try/catch warns and continues) rather than\n * overwriting a file we couldn't understand with our defaults. Preserves every\n * other setting, is idempotent, and uses temp+rename so Claude Code's mtime\n * watcher never sees a half-written file. Returns the merged object.\n */\nexport async function injectStopHookIntoSettingsFile(\n settingsPath: string,\n command: string,\n event: string = \"Stop\",\n timeoutSec?: number,\n matcher?: string,\n): Promise<Record<string, unknown>> {\n let existing: Record<string, unknown> = {}\n let raw: string | undefined\n try {\n raw = await fs.readFile(settingsPath, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err // never clobber on a transient read error.\n raw = undefined // missing file -> start clean.\n }\n if (raw !== undefined) {\n // A parse failure means a real file we don't understand: do NOT replace it.\n const parsed: unknown = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n existing = parsed as Record<string, unknown>\n } else {\n throw new Error(`settings.json at ${settingsPath} is not a JSON object; refusing to overwrite`)\n }\n }\n const merged = mergeStopHookIntoSettings(existing, command, event, timeoutSec, matcher)\n const tmp = `${settingsPath}.${process.pid}.tmp`\n await fs.writeFile(tmp, `${JSON.stringify(merged, null, 2)}\\n`, { mode: 0o600 })\n await fs.rename(tmp, settingsPath)\n return merged\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as PATHS } from "./paths-
|
|
1
|
+
import { t as PATHS } from "./paths-Ci485qSJ.js";
|
|
2
2
|
import { o as runCommandCapture, r as parseBoolEnv } from "./exec-y8C_MU8A.js";
|
|
3
3
|
import { promises } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -265,4 +265,4 @@ function fileLastPromptStore(stateDir) {
|
|
|
265
265
|
//#endregion
|
|
266
266
|
export { isSubagentContext as a, repoRoot as c, trustRepo as d, fileReviewDebounce as i, stopGateEnabledForRepo as l, fileFindingsStore as n, regressions as o, fileLastPromptStore as r, repoFingerprint as s, fileBaselineStore as t, stopReviewStateDir as u };
|
|
267
267
|
|
|
268
|
-
//# sourceMappingURL=stop-gate-policy-
|
|
268
|
+
//# sourceMappingURL=stop-gate-policy-DMr3KVmw.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stop-gate-policy-BGd6b5hR.js","names":["nodePath","fs"],"sources":["../src/lib/orchestration/stop-gate-policy.ts"],"sourcesContent":["/**\n * Pure + thin-IO policy helpers for the consent-gated, top-level-only structural\n * Stop-gate. Split out of `stop-gate-hook.ts` so the security-critical decisions\n * (is this a subagent? is this repo trusted? which checks regressed?) are small,\n * named, and unit-testable.\n *\n * Verified against the official Claude Code hooks reference\n * (https://code.claude.com/docs/en/hooks), 2026-06:\n * - `Stop` fires for the MAIN agent; subagents fire `SubagentStop`, and a\n * `Stop`-registered hook is converted to `SubagentStop` in a subagent\n * context — so the hook command CAN run for a subagent. The payload carries\n * `agent_id` / `agent_type` ONLY inside a subagent/teammate context, so that\n * is the reliable top-level-vs-sub discriminator (NOT an env var, which the\n * proxy cannot set on Claude-spawned subagents).\n * - `UserPromptSubmit` fires only for human prompts to the main agent.\n * - `stop_hook_active` is NOT in the current documented payload, so the\n * per-prompt block budget — not that flag — must be the termination guard.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { promises as fs } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport nodePath from \"node:path\"\n\nimport { parseBoolEnv, resolveExecutable, runCommandCapture } from \"~/lib/exec\"\nimport { PATHS } from \"~/lib/paths\"\n\n/** Minimal shape of the hook payload fields these helpers read. */\nexport interface HookPayloadCommon {\n cwd?: unknown\n session_id?: unknown\n agent_id?: unknown\n agent_type?: unknown\n}\n\n/**\n * True when the hook is firing inside a subagent / teammate context (NOT the\n * top-level user session). Claude Code adds `agent_id` + `agent_type` to the\n * payload only there, so their presence is the discriminator. The Stop-gate and\n * the prompt-steer hook both stand down when this is true, scoping them to the\n * top-level session.\n */\nexport function isSubagentContext(payload: HookPayloadCommon | null | undefined): boolean {\n // Fail CLOSED: ANY present, non-null agent marker means \"not the top-level\n // session\" -> stand down. Main-agent payloads omit these keys entirely\n // (undefined), so this never disables the gate for the top-level session; but\n // a numeric / empty-string / null marker (malformed or future shape) still\n // scopes us OUT, which is the safe direction for the top-level-only invariant.\n const present = (v: unknown): boolean => v !== undefined && v !== null\n return present(payload?.agent_type) || present(payload?.agent_id)\n}\n\n// ─── Per-repo trust (consent once) ───────────────────────────────────────────\n// The gate executes the repo's own `bun run typecheck` / `bun test` / `bun run\n// lint` scripts, which are arbitrary shell. It must NEVER run them in a repo the\n// user has not explicitly trusted. Trust is recorded once per repo in a STABLE\n// dir (survives across launches), keyed by the resolved git repo root.\n\n/** Stable trust dir (NOT the per-launch mirror — trust must persist). */\nfunction trustDir(): string {\n return nodePath.join(PATHS.APP_DIR, \"stop-gate\", \"trust\")\n}\n\n/**\n * Per-process memo for `repoRoot` ONLY.\n *\n * `repoRoot` re-spawned `git rev-parse --show-toplevel` on every call — three\n * times per `github-router claude` launch (via `repoRoot`, `trustRepo` and\n * `stopGateEnabledForRepo`), ~35ms per spawn on Windows. The repo root of a\n * fixed cwd does not change inside a launch, so memoizing a SUCCESSFUL lookup\n * is sound.\n *\n * A FALLBACK result is never memoized. `runCommandCapture` is `.catch`ed to\n * `undefined`, so a git failure or 5s timeout RESOLVES with `cwd` rather than\n * rejecting — which a naive memo would then pin for the life of the process.\n * That matters because the fallback feeds a trust decision: `trustFileFor`\n * keys on the returned root, so a pinned `cwd` would key trust on a\n * subdirectory instead of the repo root. Timeouts are routine on the Windows\n * target (cold disk, AV scanning git.exe, OneDrive-synced repos), so this is\n * not a hypothetical.\n *\n * `repoFingerprint` is not memoized at all — see the note on it below.\n *\n * The memo stores the PROMISE, so concurrent callers share one spawn.\n */\nconst repoRootMemo = new Map<string, Promise<string>>()\n\n/** Resolve the git repo root for `cwd`, falling back to `cwd` when not a repo. */\nexport async function repoRoot(cwd: string): Promise<string> {\n const key = nodePath.resolve(cwd)\n const hit = repoRootMemo.get(key)\n if (hit) return hit\n const pending = (async () => {\n const r = await runCommandCapture([\"git\", \"rev-parse\", \"--show-toplevel\"], {\n cwd,\n timeoutMs: 5_000,\n }).catch(() => undefined)\n const top = r?.stdout?.trim()\n if (top && top.length > 0) return top\n // Fallback: evict so the next call re-probes instead of inheriting a\n // transient failure for the rest of the session.\n repoRootMemo.delete(key)\n return cwd\n })().catch((err: unknown) => {\n repoRootMemo.delete(key)\n throw err\n })\n repoRootMemo.set(key, pending)\n return pending\n}\n\nfunction trustFileFor(root: string): string {\n const key = createHash(\"sha256\").update(nodePath.resolve(root)).digest(\"hex\").slice(0, 32)\n return nodePath.join(trustDir(), key)\n}\n\n/**\n * A stable identity for the repo at `root`: the first (root) commit SHA. It\n * survives normal history growth but differs across distinct repositories, so a\n * DIFFERENT repo later appearing at the same filesystem path is not silently\n * trusted (codex review #2). Empty string when unavailable (no git / no commits)\n * — trust then falls back to path-only, the best we can do.\n *\n * NOT memoized, deliberately. This is the freshness half of a SECURITY control:\n * `isRepoTrusted` compares the stored fingerprint against the LIVE one to\n * detect a repo swap at a trusted path. A per-process memo defeats exactly\n * that — verified by reproduction: init a repo, fingerprint it, then\n * `rm -rf .git && git init` with a new commit at the same path, and a memoized\n * lookup keeps returning the OLD root commit, so the comparison matches and\n * the swapped-in repo is auto-trusted. The proxy is long-lived, so that window\n * is a whole session. The ~42ms per spawn this would save is not worth\n * widening trust.\n */\nexport async function repoFingerprint(root: string): Promise<string> {\n const r = await runCommandCapture(\n [\"git\", \"rev-list\", \"--max-parents=0\", \"HEAD\"],\n { cwd: root, timeoutMs: 5_000 },\n ).catch(() => undefined)\n return (\n r?.stdout\n ?.split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter(Boolean)[0] ?? \"\"\n )\n}\n\n/**\n * True iff the user has consented to run the gate in this repo AND the repo's\n * identity still matches what was trusted. The trust file stores `root\\nfp\\n`;\n * a present fingerprint is verified against the live one (deny on mismatch, and\n * deny if we pinned one but can't recompute it — fail closed). A legacy file\n * with no fingerprint is path-only trust.\n */\nexport async function isRepoTrusted(cwd: string): Promise<boolean> {\n const root = await repoRoot(cwd)\n let stored: string\n try {\n stored = await fs.readFile(trustFileFor(root), \"utf8\")\n } catch {\n return false\n }\n const storedFp = (stored.split(/\\r?\\n/)[1] ?? \"\").trim()\n if (storedFp.length === 0) return true // legacy / no-fingerprint -> path-only.\n const currentFp = await repoFingerprint(root)\n if (currentFp.length === 0) return false // pinned a fp but can't verify -> deny.\n return currentFp === storedFp\n}\n\n/** Record consent for this repo (consent once → automatic thereafter), pinning\n * the repo's root-commit fingerprint so a later repo swap at the same path is\n * not auto-trusted. */\nexport async function trustRepo(cwd: string): Promise<string> {\n const root = await repoRoot(cwd)\n const fp = await repoFingerprint(root)\n await fs.mkdir(trustDir(), { recursive: true })\n await fs.writeFile(trustFileFor(root), `${root}\\n${fp}\\n`, { mode: 0o600 })\n return root\n}\n\n/**\n * Repo-aware gate enable: `GH_ROUTER_DISABLE_STOP_GATE` force-off wins;\n * `GH_ROUTER_ENABLE_STOP_GATE` force-on next; otherwise default to OFF unless the\n * repo is trusted. This is the load-bearing security gate — the default is OFF,\n * so an untrusted repo's scripts are never auto-run.\n */\nexport async function stopGateEnabledForRepo(\n cwd: string,\n env: NodeJS.ProcessEnv = process.env,\n): Promise<boolean> {\n if (parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true) return false\n if (parseBoolEnv(env.GH_ROUTER_ENABLE_STOP_GATE) === true) return true\n return isRepoTrusted(cwd)\n}\n\n// ─── Harness detection ───────────────────────────────────────────────────────\n// Pick a sealed gate whose commands are actually runnable: `bun` present AND the\n// package.json scripts the gate invokes exist. Every sealed gate runs `bun run\n// typecheck`, so a `typecheck` script is the floor; `bun test` is the bun\n// built-in (always runnable); `bun run lint` needs a `lint` script. Returns null\n// when nothing safe matches (→ the gate stays off rather than false-red).\n\nasync function readScripts(root: string): Promise<Record<string, string>> {\n try {\n const raw = await fs.readFile(nodePath.join(root, \"package.json\"), \"utf8\")\n const pkg: unknown = JSON.parse(raw)\n const scripts =\n pkg && typeof pkg === \"object\" ? (pkg as { scripts?: unknown }).scripts : undefined\n if (scripts && typeof scripts === \"object\") {\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(scripts as Record<string, unknown>)) {\n if (typeof v === \"string\") out[k] = v\n }\n return out\n }\n } catch {\n /* no package.json / unparseable → no scripts */\n }\n return {}\n}\n\n/** Returns the sealed gate id to run for `cwd`, or null when none is safe. */\nexport async function detectHarnessGateId(cwd: string): Promise<string | null> {\n if (!resolveExecutable(\"bun\", { env: process.env })) return null\n const root = await repoRoot(cwd)\n const scripts = await readScripts(root)\n const has = (k: string): boolean => typeof scripts[k] === \"string\"\n // Every sealed gate runs `bun run typecheck`; without that script it would\n // false-red, so it is the floor.\n if (!has(\"typecheck\")) return null\n if (has(\"lint\")) return \"default-ci\" // typecheck + test + lint\n return \"typecheck-test\" // typecheck + test\n}\n\n// ─── Baseline isolation ──────────────────────────────────────────────────────\n// Block only on checks the agent's diff REGRESSED, never on failures that\n// pre-date the session. v1: the first gate eval of a session records the then-\n// failing checks as the baseline and does not block on them (it still blocks on\n// gate-weakening); later evals block only on (currentFailed \\ baseline).\n\n/** Persistent per-session baseline: the set of check ids failing at first eval. */\nexport interface BaselineStore {\n /** Recorded baseline for a session, or null if none recorded yet. */\n get: (sessionId: string) => Promise<ReadonlySet<string> | null>\n /** Record the baseline failed-check set for a session (first eval only). */\n set: (sessionId: string, failed: ReadonlyArray<string>) => Promise<void>\n}\n\n/**\n * Given the current failed checks and the recorded baseline, return the checks\n * that REGRESSED (failing now, not failing at baseline). A null baseline (first\n * eval) yields an empty regression set — nothing is blamed on the agent yet.\n */\nexport function regressions(\n currentFailed: ReadonlyArray<string>,\n baseline: ReadonlySet<string> | null,\n): string[] {\n if (baseline === null) return []\n return currentFailed.filter((id) => !baseline.has(id))\n}\n\n/** File-backed `BaselineStore` under `stateDir`, keyed by sha256(session_id). */\nexport function fileBaselineStore(stateDir: string): BaselineStore {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `baseline-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n return {\n async get(sid) {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n const arr: unknown = JSON.parse(raw)\n if (Array.isArray(arr)) return new Set(arr.filter((x): x is string => typeof x === \"string\"))\n return new Set<string>()\n } catch {\n return null // no baseline recorded yet\n }\n },\n async set(sid, failed) {\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), JSON.stringify([...failed]), { mode: 0o600 })\n },\n }\n}\n\n// ─── Advisory review: debounce + findings stores (hook V2) ───────────────────\n// The Stop hook runs a background advisory review only when the diff CHANGED\n// since the last review (debounce), and the review writes its findings to a\n// per-session file that the next UserPromptSubmit reads + clears (so the Stop\n// review's notes reach Claude non-authoritatively on the next turn).\n\n/** Per-session \"last reviewed diff hash\" so an unchanged tree isn't re-reviewed. */\nexport interface ReviewDebounce {\n /** True iff `diffHash` differs from the last reviewed hash for this session. */\n shouldReview: (sessionId: string, diffHash: string) => Promise<boolean>\n /** Record `diffHash` as the last reviewed hash for this session. */\n markReviewed: (sessionId: string, diffHash: string) => Promise<void>\n}\n\nexport function fileReviewDebounce(stateDir: string): ReviewDebounce {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `review-hash-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n const readLast = async (sid: string): Promise<string> => {\n try {\n return (await fs.readFile(fileFor(sid), \"utf8\")).trim()\n } catch {\n return \"\"\n }\n }\n return {\n async shouldReview(sid, diffHash) {\n // Never re-review the identical tree; an empty diffHash (no diff) is a no-op.\n if (diffHash.length === 0) return false\n return (await readLast(sid)) !== diffHash\n },\n async markReviewed(sid, diffHash) {\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), diffHash, { mode: 0o600 })\n },\n }\n}\n\n/** Per-session pending advisory findings: the review WRITES, the next\n * UserPromptSubmit READS then CLEARS (one-shot delivery, non-authoritative). */\nexport interface FindingsStore {\n read: (sessionId: string) => Promise<string | null>\n write: (sessionId: string, findings: string) => Promise<void>\n clear: (sessionId: string) => Promise<void>\n}\n\nexport function fileFindingsStore(stateDir: string): FindingsStore {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `findings-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n return {\n async read(sid) {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n return raw.length > 0 ? raw : null\n } catch {\n return null\n }\n },\n async write(sid, findings) {\n await fs.mkdir(stateDir, { recursive: true })\n // Atomic temp+rename so a concurrent reader never sees a half-written file.\n const tmp = `${fileFor(sid)}.${process.pid}.tmp`\n await fs.writeFile(tmp, findings, { mode: 0o600 })\n await fs.rename(tmp, fileFor(sid))\n },\n async clear(sid) {\n await fs.unlink(fileFor(sid)).catch(() => {})\n },\n }\n}\n\n/**\n * The single canonical state dir for the advisory-review layer (hook V2): the\n * Stop hook's review debounce, the background review's findings file, and the\n * UserPromptSubmit hook's last-user-prompt store all live here, keyed by\n * sha256(session_id). One dir so the three independent subcommand processes\n * (`internal-stop-hook`, `internal-stop-review`, `internal-prompt-submit`)\n * agree on where to read/write without threading a path through env. Distinct\n * from the deterministic gate's `gh-router-stopgate*` dirs (block budget +\n * baseline) so the advisory layer can be wiped independently.\n */\nexport function stopReviewStateDir(): string {\n return nodePath.join(tmpdir(), \"gh-router-stop-review\")\n}\n\n/**\n * Per-session \"last user prompt\": the UserPromptSubmit hook WRITES the current\n * prompt; the Stop hook READS it to give the background reviewer the user's\n * actual ask (the Stop payload itself carries no prompt). Plain overwrite — only\n * the latest prompt matters, and the reviewer judges the diff against it.\n */\nexport interface LastPromptStore {\n read: (sessionId: string) => Promise<string | null>\n write: (sessionId: string, prompt: string) => Promise<void>\n}\n\nexport function fileLastPromptStore(stateDir: string): LastPromptStore {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `last-prompt-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n return {\n async read(sid) {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n return raw.length > 0 ? raw : null\n } catch {\n return null\n }\n },\n async write(sid, prompt) {\n await fs.mkdir(stateDir, { recursive: true })\n const tmp = `${fileFor(sid)}.${process.pid}.tmp`\n await fs.writeFile(tmp, prompt, { mode: 0o600 })\n await fs.rename(tmp, fileFor(sid))\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stop-gate-policy-DMr3KVmw.js","names":["nodePath","fs"],"sources":["../src/lib/orchestration/stop-gate-policy.ts"],"sourcesContent":["/**\n * Pure + thin-IO policy helpers for the consent-gated, top-level-only structural\n * Stop-gate. Split out of `stop-gate-hook.ts` so the security-critical decisions\n * (is this a subagent? is this repo trusted? which checks regressed?) are small,\n * named, and unit-testable.\n *\n * Verified against the official Claude Code hooks reference\n * (https://code.claude.com/docs/en/hooks), 2026-06:\n * - `Stop` fires for the MAIN agent; subagents fire `SubagentStop`, and a\n * `Stop`-registered hook is converted to `SubagentStop` in a subagent\n * context — so the hook command CAN run for a subagent. The payload carries\n * `agent_id` / `agent_type` ONLY inside a subagent/teammate context, so that\n * is the reliable top-level-vs-sub discriminator (NOT an env var, which the\n * proxy cannot set on Claude-spawned subagents).\n * - `UserPromptSubmit` fires only for human prompts to the main agent.\n * - `stop_hook_active` is NOT in the current documented payload, so the\n * per-prompt block budget — not that flag — must be the termination guard.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { promises as fs } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport nodePath from \"node:path\"\n\nimport { parseBoolEnv, resolveExecutable, runCommandCapture } from \"~/lib/exec\"\nimport { PATHS } from \"~/lib/paths\"\n\n/** Minimal shape of the hook payload fields these helpers read. */\nexport interface HookPayloadCommon {\n cwd?: unknown\n session_id?: unknown\n agent_id?: unknown\n agent_type?: unknown\n}\n\n/**\n * True when the hook is firing inside a subagent / teammate context (NOT the\n * top-level user session). Claude Code adds `agent_id` + `agent_type` to the\n * payload only there, so their presence is the discriminator. The Stop-gate and\n * the prompt-steer hook both stand down when this is true, scoping them to the\n * top-level session.\n */\nexport function isSubagentContext(payload: HookPayloadCommon | null | undefined): boolean {\n // Fail CLOSED: ANY present, non-null agent marker means \"not the top-level\n // session\" -> stand down. Main-agent payloads omit these keys entirely\n // (undefined), so this never disables the gate for the top-level session; but\n // a numeric / empty-string / null marker (malformed or future shape) still\n // scopes us OUT, which is the safe direction for the top-level-only invariant.\n const present = (v: unknown): boolean => v !== undefined && v !== null\n return present(payload?.agent_type) || present(payload?.agent_id)\n}\n\n// ─── Per-repo trust (consent once) ───────────────────────────────────────────\n// The gate executes the repo's own `bun run typecheck` / `bun test` / `bun run\n// lint` scripts, which are arbitrary shell. It must NEVER run them in a repo the\n// user has not explicitly trusted. Trust is recorded once per repo in a STABLE\n// dir (survives across launches), keyed by the resolved git repo root.\n\n/** Stable trust dir (NOT the per-launch mirror — trust must persist). */\nfunction trustDir(): string {\n return nodePath.join(PATHS.APP_DIR, \"stop-gate\", \"trust\")\n}\n\n/**\n * Per-process memo for `repoRoot` ONLY.\n *\n * `repoRoot` re-spawned `git rev-parse --show-toplevel` on every call — three\n * times per `github-router claude` launch (via `repoRoot`, `trustRepo` and\n * `stopGateEnabledForRepo`), ~35ms per spawn on Windows. The repo root of a\n * fixed cwd does not change inside a launch, so memoizing a SUCCESSFUL lookup\n * is sound.\n *\n * A FALLBACK result is never memoized. `runCommandCapture` is `.catch`ed to\n * `undefined`, so a git failure or 5s timeout RESOLVES with `cwd` rather than\n * rejecting — which a naive memo would then pin for the life of the process.\n * That matters because the fallback feeds a trust decision: `trustFileFor`\n * keys on the returned root, so a pinned `cwd` would key trust on a\n * subdirectory instead of the repo root. Timeouts are routine on the Windows\n * target (cold disk, AV scanning git.exe, OneDrive-synced repos), so this is\n * not a hypothetical.\n *\n * `repoFingerprint` is not memoized at all — see the note on it below.\n *\n * The memo stores the PROMISE, so concurrent callers share one spawn.\n */\nconst repoRootMemo = new Map<string, Promise<string>>()\n\n/** Resolve the git repo root for `cwd`, falling back to `cwd` when not a repo. */\nexport async function repoRoot(cwd: string): Promise<string> {\n const key = nodePath.resolve(cwd)\n const hit = repoRootMemo.get(key)\n if (hit) return hit\n const pending = (async () => {\n const r = await runCommandCapture([\"git\", \"rev-parse\", \"--show-toplevel\"], {\n cwd,\n timeoutMs: 5_000,\n }).catch(() => undefined)\n const top = r?.stdout?.trim()\n if (top && top.length > 0) return top\n // Fallback: evict so the next call re-probes instead of inheriting a\n // transient failure for the rest of the session.\n repoRootMemo.delete(key)\n return cwd\n })().catch((err: unknown) => {\n repoRootMemo.delete(key)\n throw err\n })\n repoRootMemo.set(key, pending)\n return pending\n}\n\nfunction trustFileFor(root: string): string {\n const key = createHash(\"sha256\").update(nodePath.resolve(root)).digest(\"hex\").slice(0, 32)\n return nodePath.join(trustDir(), key)\n}\n\n/**\n * A stable identity for the repo at `root`: the first (root) commit SHA. It\n * survives normal history growth but differs across distinct repositories, so a\n * DIFFERENT repo later appearing at the same filesystem path is not silently\n * trusted (codex review #2). Empty string when unavailable (no git / no commits)\n * — trust then falls back to path-only, the best we can do.\n *\n * NOT memoized, deliberately. This is the freshness half of a SECURITY control:\n * `isRepoTrusted` compares the stored fingerprint against the LIVE one to\n * detect a repo swap at a trusted path. A per-process memo defeats exactly\n * that — verified by reproduction: init a repo, fingerprint it, then\n * `rm -rf .git && git init` with a new commit at the same path, and a memoized\n * lookup keeps returning the OLD root commit, so the comparison matches and\n * the swapped-in repo is auto-trusted. The proxy is long-lived, so that window\n * is a whole session. The ~42ms per spawn this would save is not worth\n * widening trust.\n */\nexport async function repoFingerprint(root: string): Promise<string> {\n const r = await runCommandCapture(\n [\"git\", \"rev-list\", \"--max-parents=0\", \"HEAD\"],\n { cwd: root, timeoutMs: 5_000 },\n ).catch(() => undefined)\n return (\n r?.stdout\n ?.split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter(Boolean)[0] ?? \"\"\n )\n}\n\n/**\n * True iff the user has consented to run the gate in this repo AND the repo's\n * identity still matches what was trusted. The trust file stores `root\\nfp\\n`;\n * a present fingerprint is verified against the live one (deny on mismatch, and\n * deny if we pinned one but can't recompute it — fail closed). A legacy file\n * with no fingerprint is path-only trust.\n */\nexport async function isRepoTrusted(cwd: string): Promise<boolean> {\n const root = await repoRoot(cwd)\n let stored: string\n try {\n stored = await fs.readFile(trustFileFor(root), \"utf8\")\n } catch {\n return false\n }\n const storedFp = (stored.split(/\\r?\\n/)[1] ?? \"\").trim()\n if (storedFp.length === 0) return true // legacy / no-fingerprint -> path-only.\n const currentFp = await repoFingerprint(root)\n if (currentFp.length === 0) return false // pinned a fp but can't verify -> deny.\n return currentFp === storedFp\n}\n\n/** Record consent for this repo (consent once → automatic thereafter), pinning\n * the repo's root-commit fingerprint so a later repo swap at the same path is\n * not auto-trusted. */\nexport async function trustRepo(cwd: string): Promise<string> {\n const root = await repoRoot(cwd)\n const fp = await repoFingerprint(root)\n await fs.mkdir(trustDir(), { recursive: true })\n await fs.writeFile(trustFileFor(root), `${root}\\n${fp}\\n`, { mode: 0o600 })\n return root\n}\n\n/**\n * Repo-aware gate enable: `GH_ROUTER_DISABLE_STOP_GATE` force-off wins;\n * `GH_ROUTER_ENABLE_STOP_GATE` force-on next; otherwise default to OFF unless the\n * repo is trusted. This is the load-bearing security gate — the default is OFF,\n * so an untrusted repo's scripts are never auto-run.\n */\nexport async function stopGateEnabledForRepo(\n cwd: string,\n env: NodeJS.ProcessEnv = process.env,\n): Promise<boolean> {\n if (parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true) return false\n if (parseBoolEnv(env.GH_ROUTER_ENABLE_STOP_GATE) === true) return true\n return isRepoTrusted(cwd)\n}\n\n// ─── Harness detection ───────────────────────────────────────────────────────\n// Pick a sealed gate whose commands are actually runnable: `bun` present AND the\n// package.json scripts the gate invokes exist. Every sealed gate runs `bun run\n// typecheck`, so a `typecheck` script is the floor; `bun test` is the bun\n// built-in (always runnable); `bun run lint` needs a `lint` script. Returns null\n// when nothing safe matches (→ the gate stays off rather than false-red).\n\nasync function readScripts(root: string): Promise<Record<string, string>> {\n try {\n const raw = await fs.readFile(nodePath.join(root, \"package.json\"), \"utf8\")\n const pkg: unknown = JSON.parse(raw)\n const scripts =\n pkg && typeof pkg === \"object\" ? (pkg as { scripts?: unknown }).scripts : undefined\n if (scripts && typeof scripts === \"object\") {\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(scripts as Record<string, unknown>)) {\n if (typeof v === \"string\") out[k] = v\n }\n return out\n }\n } catch {\n /* no package.json / unparseable → no scripts */\n }\n return {}\n}\n\n/** Returns the sealed gate id to run for `cwd`, or null when none is safe. */\nexport async function detectHarnessGateId(cwd: string): Promise<string | null> {\n if (!resolveExecutable(\"bun\", { env: process.env })) return null\n const root = await repoRoot(cwd)\n const scripts = await readScripts(root)\n const has = (k: string): boolean => typeof scripts[k] === \"string\"\n // Every sealed gate runs `bun run typecheck`; without that script it would\n // false-red, so it is the floor.\n if (!has(\"typecheck\")) return null\n if (has(\"lint\")) return \"default-ci\" // typecheck + test + lint\n return \"typecheck-test\" // typecheck + test\n}\n\n// ─── Baseline isolation ──────────────────────────────────────────────────────\n// Block only on checks the agent's diff REGRESSED, never on failures that\n// pre-date the session. v1: the first gate eval of a session records the then-\n// failing checks as the baseline and does not block on them (it still blocks on\n// gate-weakening); later evals block only on (currentFailed \\ baseline).\n\n/** Persistent per-session baseline: the set of check ids failing at first eval. */\nexport interface BaselineStore {\n /** Recorded baseline for a session, or null if none recorded yet. */\n get: (sessionId: string) => Promise<ReadonlySet<string> | null>\n /** Record the baseline failed-check set for a session (first eval only). */\n set: (sessionId: string, failed: ReadonlyArray<string>) => Promise<void>\n}\n\n/**\n * Given the current failed checks and the recorded baseline, return the checks\n * that REGRESSED (failing now, not failing at baseline). A null baseline (first\n * eval) yields an empty regression set — nothing is blamed on the agent yet.\n */\nexport function regressions(\n currentFailed: ReadonlyArray<string>,\n baseline: ReadonlySet<string> | null,\n): string[] {\n if (baseline === null) return []\n return currentFailed.filter((id) => !baseline.has(id))\n}\n\n/** File-backed `BaselineStore` under `stateDir`, keyed by sha256(session_id). */\nexport function fileBaselineStore(stateDir: string): BaselineStore {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `baseline-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n return {\n async get(sid) {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n const arr: unknown = JSON.parse(raw)\n if (Array.isArray(arr)) return new Set(arr.filter((x): x is string => typeof x === \"string\"))\n return new Set<string>()\n } catch {\n return null // no baseline recorded yet\n }\n },\n async set(sid, failed) {\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), JSON.stringify([...failed]), { mode: 0o600 })\n },\n }\n}\n\n// ─── Advisory review: debounce + findings stores (hook V2) ───────────────────\n// The Stop hook runs a background advisory review only when the diff CHANGED\n// since the last review (debounce), and the review writes its findings to a\n// per-session file that the next UserPromptSubmit reads + clears (so the Stop\n// review's notes reach Claude non-authoritatively on the next turn).\n\n/** Per-session \"last reviewed diff hash\" so an unchanged tree isn't re-reviewed. */\nexport interface ReviewDebounce {\n /** True iff `diffHash` differs from the last reviewed hash for this session. */\n shouldReview: (sessionId: string, diffHash: string) => Promise<boolean>\n /** Record `diffHash` as the last reviewed hash for this session. */\n markReviewed: (sessionId: string, diffHash: string) => Promise<void>\n}\n\nexport function fileReviewDebounce(stateDir: string): ReviewDebounce {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `review-hash-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n const readLast = async (sid: string): Promise<string> => {\n try {\n return (await fs.readFile(fileFor(sid), \"utf8\")).trim()\n } catch {\n return \"\"\n }\n }\n return {\n async shouldReview(sid, diffHash) {\n // Never re-review the identical tree; an empty diffHash (no diff) is a no-op.\n if (diffHash.length === 0) return false\n return (await readLast(sid)) !== diffHash\n },\n async markReviewed(sid, diffHash) {\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), diffHash, { mode: 0o600 })\n },\n }\n}\n\n/** Per-session pending advisory findings: the review WRITES, the next\n * UserPromptSubmit READS then CLEARS (one-shot delivery, non-authoritative). */\nexport interface FindingsStore {\n read: (sessionId: string) => Promise<string | null>\n write: (sessionId: string, findings: string) => Promise<void>\n clear: (sessionId: string) => Promise<void>\n}\n\nexport function fileFindingsStore(stateDir: string): FindingsStore {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `findings-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n return {\n async read(sid) {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n return raw.length > 0 ? raw : null\n } catch {\n return null\n }\n },\n async write(sid, findings) {\n await fs.mkdir(stateDir, { recursive: true })\n // Atomic temp+rename so a concurrent reader never sees a half-written file.\n const tmp = `${fileFor(sid)}.${process.pid}.tmp`\n await fs.writeFile(tmp, findings, { mode: 0o600 })\n await fs.rename(tmp, fileFor(sid))\n },\n async clear(sid) {\n await fs.unlink(fileFor(sid)).catch(() => {})\n },\n }\n}\n\n/**\n * The single canonical state dir for the advisory-review layer (hook V2): the\n * Stop hook's review debounce, the background review's findings file, and the\n * UserPromptSubmit hook's last-user-prompt store all live here, keyed by\n * sha256(session_id). One dir so the three independent subcommand processes\n * (`internal-stop-hook`, `internal-stop-review`, `internal-prompt-submit`)\n * agree on where to read/write without threading a path through env. Distinct\n * from the deterministic gate's `gh-router-stopgate*` dirs (block budget +\n * baseline) so the advisory layer can be wiped independently.\n */\nexport function stopReviewStateDir(): string {\n return nodePath.join(tmpdir(), \"gh-router-stop-review\")\n}\n\n/**\n * Per-session \"last user prompt\": the UserPromptSubmit hook WRITES the current\n * prompt; the Stop hook READS it to give the background reviewer the user's\n * actual ask (the Stop payload itself carries no prompt). Plain overwrite — only\n * the latest prompt matters, and the reviewer judges the diff against it.\n */\nexport interface LastPromptStore {\n read: (sessionId: string) => Promise<string | null>\n write: (sessionId: string, prompt: string) => Promise<void>\n}\n\nexport function fileLastPromptStore(stateDir: string): LastPromptStore {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `last-prompt-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n return {\n async read(sid) {\n try {\n const raw = await fs.readFile(fileFor(sid), \"utf8\")\n return raw.length > 0 ? raw : null\n } catch {\n return null\n }\n },\n async write(sid, prompt) {\n await fs.mkdir(stateDir, { recursive: true })\n const tmp = `${fileFor(sid)}.${process.pid}.tmp`\n await fs.writeFile(tmp, prompt, { mode: 0o600 })\n await fs.rename(tmp, fileFor(sid))\n },\n }\n}\n"],"mappings":"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"}
|