github-router 0.3.314 → 0.3.316

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.
Files changed (110) hide show
  1. package/dist/{aic-ledger-Cx_Ubon3.js → aic-ledger-BS47yb_C.js} +66 -6
  2. package/dist/aic-ledger-BS47yb_C.js.map +1 -0
  3. package/dist/{attribution-settings-Btc5EZTj.js → attribution-settings-jcsSeVur.js} +120 -21
  4. package/dist/attribution-settings-jcsSeVur.js.map +1 -0
  5. package/dist/{auth-O_UG1ABO.js → auth-JfxL9q7z.js} +4 -4
  6. package/dist/{auth-O_UG1ABO.js.map → auth-JfxL9q7z.js.map} +1 -1
  7. package/dist/browser-ext/manifest.json +1 -1
  8. package/dist/{check-usage-dicJOWML.js → check-usage-DXUEQzt_.js} +4 -4
  9. package/dist/{check-usage-dicJOWML.js.map → check-usage-DXUEQzt_.js.map} +1 -1
  10. package/dist/{claude-D0vuAqTV.js → claude-CJ4Nd2_3.js} +116 -45
  11. package/dist/claude-CJ4Nd2_3.js.map +1 -0
  12. package/dist/{codex-BNm9D0iJ.js → codex-CwuLqNZF.js} +6 -6
  13. package/dist/codex-CwuLqNZF.js.map +1 -0
  14. package/dist/copilot-discount-IbXR5A28.js +220 -0
  15. package/dist/copilot-discount-IbXR5A28.js.map +1 -0
  16. package/dist/{debug-2PgImRnr.js → debug-CA7pg8V9.js} +2 -2
  17. package/dist/{debug-2PgImRnr.js.map → debug-CA7pg8V9.js.map} +1 -1
  18. package/dist/engine-DwrxceOS.js +2 -0
  19. package/dist/{exec-y8C_MU8A.js → exec-COUT4QsS.js} +2 -2
  20. package/dist/{exec-y8C_MU8A.js.map → exec-COUT4QsS.js.map} +1 -1
  21. package/dist/{gate-discovery-CO2njGBB.js → gate-discovery-z04ywOzB.js} +6 -6
  22. package/dist/{gate-discovery-CO2njGBB.js.map → gate-discovery-z04ywOzB.js.map} +1 -1
  23. package/dist/{get-copilot-usage-Cjy8TiFf.js → get-copilot-usage-BMD_cJNT.js} +3 -3
  24. package/dist/{get-copilot-usage-Cjy8TiFf.js.map → get-copilot-usage-BMD_cJNT.js.map} +1 -1
  25. package/dist/hooks.mjs +490 -28
  26. package/dist/hooks.sha256 +1 -1
  27. package/dist/internal-aic-status-BN0xRLSF.js +432 -0
  28. package/dist/internal-aic-status-BN0xRLSF.js.map +1 -0
  29. package/dist/{internal-artifact-open-CxeIfsW_.js → internal-artifact-open-B3stUu74.js} +2 -2
  30. package/dist/{internal-artifact-open-CxeIfsW_.js.map → internal-artifact-open-B3stUu74.js.map} +1 -1
  31. package/dist/{internal-fast-dispatch-guard-D8YNlnuQ.js → internal-fast-dispatch-guard-2QjUlhqO.js} +1 -1
  32. package/dist/{internal-fast-dispatch-guard-19xQEouu.js → internal-fast-dispatch-guard-CAwYj_Yz.js} +2 -2
  33. package/dist/{internal-fast-dispatch-guard-19xQEouu.js.map → internal-fast-dispatch-guard-CAwYj_Yz.js.map} +1 -1
  34. package/dist/{internal-first-mate-guard-BuweUgjb.js → internal-first-mate-guard-B0rbLb0L.js} +3 -3
  35. package/dist/{internal-first-mate-guard-BuweUgjb.js.map → internal-first-mate-guard-B0rbLb0L.js.map} +1 -1
  36. package/dist/{internal-first-mate-guard-Nf1H0mCK.js → internal-first-mate-guard-VgnHBp0s.js} +1 -1
  37. package/dist/{internal-max-dispatch-guard-DZ7KLWxP.js → internal-max-dispatch-guard-BNqQ2wMc.js} +3 -3
  38. package/dist/{internal-max-dispatch-guard-DZ7KLWxP.js.map → internal-max-dispatch-guard-BNqQ2wMc.js.map} +1 -1
  39. package/dist/{internal-max-dispatch-guard-_Pp4LltM.js → internal-max-dispatch-guard-Bz7a2Ntf.js} +1 -1
  40. package/dist/{internal-plan-review-DLduDz41.js → internal-plan-review-QUvNZmMR.js} +3 -3
  41. package/dist/{internal-plan-review-DLduDz41.js.map → internal-plan-review-QUvNZmMR.js.map} +1 -1
  42. package/dist/{internal-prompt-submit-DG94f8E-.js → internal-prompt-submit-fj5ccft_.js} +5 -5
  43. package/dist/{internal-prompt-submit-DG94f8E-.js.map → internal-prompt-submit-fj5ccft_.js.map} +1 -1
  44. package/dist/{internal-session-bind-D6Hpl-84.js → internal-session-bind-0Hmak8PR.js} +2 -2
  45. package/dist/{internal-session-bind-D6Hpl-84.js.map → internal-session-bind-0Hmak8PR.js.map} +1 -1
  46. package/dist/{internal-stop-hook-D56b6iVj.js → internal-stop-hook-UN1JHtjj.js} +6 -6
  47. package/dist/{internal-stop-hook-D56b6iVj.js.map → internal-stop-hook-UN1JHtjj.js.map} +1 -1
  48. package/dist/{internal-stop-review-DlKzZgFm.js → internal-stop-review-8v2llYDB.js} +2 -2
  49. package/dist/{internal-stop-review-DlKzZgFm.js.map → internal-stop-review-8v2llYDB.js.map} +1 -1
  50. package/dist/{internal-worker-guard-D7sfByyo.js → internal-worker-guard-BI4Pl0Ew.js} +2 -2
  51. package/dist/{internal-worker-guard-D7sfByyo.js.map → internal-worker-guard-BI4Pl0Ew.js.map} +1 -1
  52. package/dist/{internal-workspace-header-CU5OjLCB.js → internal-workspace-header-kg9FoNXK.js} +2 -2
  53. package/dist/{internal-workspace-header-CU5OjLCB.js.map → internal-workspace-header-kg9FoNXK.js.map} +1 -1
  54. package/dist/lifecycle-BpqAyaAj.js +2 -0
  55. package/dist/lifecycle-CL2WiyeQ.js +2 -0
  56. package/dist/{lifecycle-CtRA3Y7p.js → lifecycle-Cx1wVrLb.js} +3 -3
  57. package/dist/{lifecycle-CtRA3Y7p.js.map → lifecycle-Cx1wVrLb.js.map} +1 -1
  58. package/dist/{lifecycle-BCnarXJV.js → lifecycle-IwotwedG.js} +2 -2
  59. package/dist/{lifecycle-BCnarXJV.js.map → lifecycle-IwotwedG.js.map} +1 -1
  60. package/dist/main.js +20 -20
  61. package/dist/{max-profile-contract-BcrMlrmo.js → max-profile-contract-Bl7I_EOC.js} +2 -2
  62. package/dist/{max-profile-contract-BcrMlrmo.js.map → max-profile-contract-Bl7I_EOC.js.map} +1 -1
  63. package/dist/{mcp-workspace-header-B60WSR1Y.js → mcp-workspace-header-BYgWioLH.js} +2 -2
  64. package/dist/{mcp-workspace-header-B60WSR1Y.js.map → mcp-workspace-header-BYgWioLH.js.map} +1 -1
  65. package/dist/{models-COuMOfM6.js → models-CVzyU5mJ.js} +3 -3
  66. package/dist/{models-COuMOfM6.js.map → models-CVzyU5mJ.js.map} +1 -1
  67. package/dist/{orchestration-DjLA-lxB.js → orchestration-Dc3mO0G3.js} +3 -3
  68. package/dist/{orchestration-DjLA-lxB.js.map → orchestration-Dc3mO0G3.js.map} +1 -1
  69. package/dist/paths-BXXCVFvA.js +2 -0
  70. package/dist/{paths-De1Qh9Q9.js → paths-CMxlQEfQ.js} +3 -3
  71. package/dist/{paths-De1Qh9Q9.js.map → paths-CMxlQEfQ.js.map} +1 -1
  72. package/dist/{peer-mcp-personas-0_XU6nm9.js → peer-mcp-personas-CQ-0Piyr.js} +175 -54
  73. package/dist/peer-mcp-personas-CQ-0Piyr.js.map +1 -0
  74. package/dist/{plan-review-hook-vL6BYmA8.js → plan-review-hook-BIMwWkh6.js} +4 -4
  75. package/dist/{plan-review-hook-vL6BYmA8.js.map → plan-review-hook-BIMwWkh6.js.map} +1 -1
  76. package/dist/{prompt-submit-hook-DfqufWC1.js → prompt-submit-hook-BcFpjHXg.js} +3 -3
  77. package/dist/{prompt-submit-hook-DfqufWC1.js.map → prompt-submit-hook-BcFpjHXg.js.map} +1 -1
  78. package/dist/{provision-DuCnZJj_.js → provision-_R7ZMygj.js} +5 -5
  79. package/dist/{provision-DuCnZJj_.js.map → provision-_R7ZMygj.js.map} +1 -1
  80. package/dist/{self-invocation-B84JCHps.js → self-invocation-cRl4RqfW.js} +2 -2
  81. package/dist/{self-invocation-B84JCHps.js.map → self-invocation-cRl4RqfW.js.map} +1 -1
  82. package/dist/{serve-DXFVR6A0.js → serve-DU66K5kv.js} +14 -14
  83. package/dist/{serve-DXFVR6A0.js.map → serve-DU66K5kv.js.map} +1 -1
  84. package/dist/{server-setup-CP8nGop3.js → server-setup-iLG8LrfL.js} +168 -30
  85. package/dist/server-setup-iLG8LrfL.js.map +1 -0
  86. package/dist/{start-BHEjESCB.js → start-DQTcm0o_.js} +3 -3
  87. package/dist/{start-BHEjESCB.js.map → start-DQTcm0o_.js.map} +1 -1
  88. package/dist/{state-B8adscCv.js → state-BlMpEFAt.js} +2 -1
  89. package/dist/{state-B8adscCv.js.map → state-BlMpEFAt.js.map} +1 -1
  90. package/dist/{stop-gate-hook-BEU8qQrC.js → stop-gate-hook-BUCxa5Kc.js} +4 -4
  91. package/dist/{stop-gate-hook-BEU8qQrC.js.map → stop-gate-hook-BUCxa5Kc.js.map} +1 -1
  92. package/dist/{stop-gate-policy-qzqXfRxE.js → stop-gate-policy--bpyxxXU.js} +3 -3
  93. package/dist/{stop-gate-policy-qzqXfRxE.js.map → stop-gate-policy--bpyxxXU.js.map} +1 -1
  94. package/dist/{token-CCg-Ov5q.js → token-C1gthnbV.js} +3 -3
  95. package/dist/{token-CCg-Ov5q.js.map → token-C1gthnbV.js.map} +1 -1
  96. package/dist/{worker-dispatch-CH-NN5FW.js → worker-dispatch-DMIdrlsF.js} +2 -2
  97. package/dist/{worker-dispatch-CH-NN5FW.js.map → worker-dispatch-DMIdrlsF.js.map} +1 -1
  98. package/package.json +1 -1
  99. package/dist/aic-ledger-Cx_Ubon3.js.map +0 -1
  100. package/dist/attribution-settings-Btc5EZTj.js.map +0 -1
  101. package/dist/claude-D0vuAqTV.js.map +0 -1
  102. package/dist/codex-BNm9D0iJ.js.map +0 -1
  103. package/dist/engine-DsVZuCrD.js +0 -2
  104. package/dist/internal-aic-status-C_swX3_7.js +0 -95
  105. package/dist/internal-aic-status-C_swX3_7.js.map +0 -1
  106. package/dist/lifecycle-B0USSJvd.js +0 -2
  107. package/dist/lifecycle-CQFjaQc0.js +0 -2
  108. package/dist/paths-DyX6SEnd.js +0 -2
  109. package/dist/peer-mcp-personas-0_XU6nm9.js.map +0 -1
  110. package/dist/server-setup-CP8nGop3.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"stop-gate-policy-qzqXfRxE.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--bpyxxXU.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,5 +1,5 @@
1
- import { t as PATHS } from "./paths-De1Qh9Q9.js";
2
- import { t as state } from "./state-B8adscCv.js";
1
+ import { t as PATHS } from "./paths-CMxlQEfQ.js";
2
+ import { t as state } from "./state-BlMpEFAt.js";
3
3
  import consola from "consola";
4
4
  import path from "node:path";
5
5
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -1760,4 +1760,4 @@ async function warnIfAgentScopesInsufficient() {
1760
1760
  //#endregion
1761
1761
  export { GITHUB_GRAPHQL_URL as C, githubAgentGraphQLHeaders as D, copilotVersion as E, githubAgentHeaders as O, GITHUB_API_BASE_URL as S, copilotHeaders as T, forwardError as _, cacheCopilotVersion as a, sanitizeTransportText as b, filterBetaHeader as c, resolveModel as d, sleep as f, HTTPError as g, isAllowedCopilotHost as h, tryRefreshAndRetry as i, githubHeaders as k, isNullish as l, getGitHubUser as m, setupGitHubAgentToken as n, cacheModels as o, getModels as p, setupGitHubToken as r, cacheVSCodeVersion as s, setupCopilotToken as t, resolveCodexModel as u, classifyTransportError as v, copilotBaseUrl as w, withTransientRetry as x, fetchWithTransientRetry as y };
1762
1762
 
1763
- //# sourceMappingURL=token-CCg-Ov5q.js.map
1763
+ //# sourceMappingURL=token-C1gthnbV.js.map