github-router 0.3.276 → 0.3.285

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 (107) hide show
  1. package/dist/{attribution-settings-VOPvSn6v.js → attribution-settings-B8M2fvhz.js} +44 -45
  2. package/dist/attribution-settings-B8M2fvhz.js.map +1 -0
  3. package/dist/{auth-CYoRwhC9.js → auth-VUL2Zxvw.js} +3 -3
  4. package/dist/{auth-CYoRwhC9.js.map → auth-VUL2Zxvw.js.map} +1 -1
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/{check-usage-DDUyg8ZB.js → check-usage-CcLdFPGr.js} +4 -4
  7. package/dist/{check-usage-DDUyg8ZB.js.map → check-usage-CcLdFPGr.js.map} +1 -1
  8. package/dist/{claude-OTHh7iKz.js → claude-e-JIQGhR.js} +55 -38
  9. package/dist/claude-e-JIQGhR.js.map +1 -0
  10. package/dist/{codex-tCxK30Su.js → codex-DSnVZ9oK.js} +5 -5
  11. package/dist/{codex-tCxK30Su.js.map → codex-DSnVZ9oK.js.map} +1 -1
  12. package/dist/{debug-CJgsxoVw.js → debug-CtzYWxpJ.js} +2 -2
  13. package/dist/{debug-CJgsxoVw.js.map → debug-CtzYWxpJ.js.map} +1 -1
  14. package/dist/engine-DMveEa9x.js +2 -0
  15. package/dist/{gate-discovery-nf7PGgnS.js → gate-discovery-BCFwLm0q.js} +5 -5
  16. package/dist/{gate-discovery-nf7PGgnS.js.map → gate-discovery-BCFwLm0q.js.map} +1 -1
  17. package/dist/{get-copilot-usage-D1OAH0EU.js → get-copilot-usage-B-EDAqQb.js} +2 -2
  18. package/dist/{get-copilot-usage-D1OAH0EU.js.map → get-copilot-usage-B-EDAqQb.js.map} +1 -1
  19. package/dist/hooks.mjs +37870 -0
  20. package/dist/hooks.sha256 +1 -0
  21. package/dist/{internal-artifact-open-BCz9lnq0.js → internal-artifact-open-Dj5Nlq0L.js} +2 -2
  22. package/dist/{internal-artifact-open-BCz9lnq0.js.map → internal-artifact-open-Dj5Nlq0L.js.map} +1 -1
  23. package/dist/{internal-first-mate-guard-7v2CtMA6.js → internal-first-mate-guard-B7p4NttK.js} +1 -1
  24. package/dist/{internal-first-mate-guard-La0tIjTU.js → internal-first-mate-guard-DOgFVki5.js} +5 -4
  25. package/dist/{internal-first-mate-guard-La0tIjTU.js.map → internal-first-mate-guard-DOgFVki5.js.map} +1 -1
  26. package/dist/{internal-plan-review-jRMpHu68.js → internal-plan-review-DRFHET_B.js} +11 -4
  27. package/dist/internal-plan-review-DRFHET_B.js.map +1 -0
  28. package/dist/{internal-prompt-submit-CqWRFCuv.js → internal-prompt-submit-f1LR2P2G.js} +4 -4
  29. package/dist/{internal-prompt-submit-CqWRFCuv.js.map → internal-prompt-submit-f1LR2P2G.js.map} +1 -1
  30. package/dist/{internal-session-bind-BiHWDxuC.js → internal-session-bind-DhZhxJ3T.js} +2 -2
  31. package/dist/{internal-session-bind-BiHWDxuC.js.map → internal-session-bind-DhZhxJ3T.js.map} +1 -1
  32. package/dist/{internal-stop-hook-Ds9LwmZA.js → internal-stop-hook-Cf-7w3RH.js} +68 -20
  33. package/dist/internal-stop-hook-Cf-7w3RH.js.map +1 -0
  34. package/dist/{internal-stop-review-BOFj7R3q.js → internal-stop-review-BBsLcbPG.js} +2 -2
  35. package/dist/{internal-stop-review-BOFj7R3q.js.map → internal-stop-review-BBsLcbPG.js.map} +1 -1
  36. package/dist/{internal-worker-guard-Lu8VHj5k.js → internal-worker-guard-CKgYFiFO.js} +2 -2
  37. package/dist/{internal-worker-guard-Lu8VHj5k.js.map → internal-worker-guard-CKgYFiFO.js.map} +1 -1
  38. package/dist/{internal-workspace-header-BRMz0Yql.js → internal-workspace-header-OYgHEnFt.js} +2 -2
  39. package/dist/{internal-workspace-header-BRMz0Yql.js.map → internal-workspace-header-OYgHEnFt.js.map} +1 -1
  40. package/dist/lib/tree-sitter-pool/lifecycle-C7W8D2lx.js +165 -0
  41. package/dist/lib/tree-sitter-pool/lifecycle-Dx0SnP7Z.js +157 -0
  42. package/dist/lib/tree-sitter-pool/worker.js +287 -14
  43. package/dist/{lifecycle-LA4rAuAL.js → lifecycle-B7CHqKlF.js} +2 -2
  44. package/dist/{lifecycle-LA4rAuAL.js.map → lifecycle-B7CHqKlF.js.map} +1 -1
  45. package/dist/lifecycle-CbmHMMSD.js +2 -0
  46. package/dist/{lifecycle-AGXnd-ZY.js → lifecycle-D-rL81tT.js} +2 -2
  47. package/dist/{lifecycle-AGXnd-ZY.js.map → lifecycle-D-rL81tT.js.map} +1 -1
  48. package/dist/lifecycle-DTcZwa7U.js +2 -0
  49. package/dist/lifecycle-K9oVtGde.mjs +16 -0
  50. package/dist/main.js +18 -18
  51. package/dist/{mcp-workspace-header-CGJbNeHb.js → mcp-workspace-header-ucs2SDST.js} +5 -6
  52. package/dist/mcp-workspace-header-ucs2SDST.js.map +1 -0
  53. package/dist/{models-4Q45Q2Dw.js → models-C16mBK2M.js} +3 -3
  54. package/dist/{models-4Q45Q2Dw.js.map → models-C16mBK2M.js.map} +1 -1
  55. package/dist/{orchestration-CpkG8ScN.js → orchestration-CtM6FYNx.js} +2 -2
  56. package/dist/{orchestration-CpkG8ScN.js.map → orchestration-CtM6FYNx.js.map} +1 -1
  57. package/dist/package-root-B-osctCk.js +29 -0
  58. package/dist/package-root-B-osctCk.js.map +1 -0
  59. package/dist/paths-CV9K7Xqm.js +2 -0
  60. package/dist/{paths-j2B7b0DZ.js → paths-wLC0InjX.js} +28 -4
  61. package/dist/paths-wLC0InjX.js.map +1 -0
  62. package/dist/{peer-mcp-personas-DRL_xT4h.js → peer-mcp-personas-V6stFvpq.js} +1158 -297
  63. package/dist/peer-mcp-personas-V6stFvpq.js.map +1 -0
  64. package/dist/{plan-review-hook-Dk5zTNke.js → plan-review-hook-Lf9ISdF6.js} +5 -6
  65. package/dist/plan-review-hook-Lf9ISdF6.js.map +1 -0
  66. package/dist/{prompt-submit-hook-lvTWwaTV.js → prompt-submit-hook-BlijaOn7.js} +6 -11
  67. package/dist/prompt-submit-hook-BlijaOn7.js.map +1 -0
  68. package/dist/{provision-HzZ547dW.js → provision-BpL6gZIt.js} +4 -4
  69. package/dist/{provision-HzZ547dW.js.map → provision-BpL6gZIt.js.map} +1 -1
  70. package/dist/self-invocation-CKMjcA5F.js +380 -0
  71. package/dist/self-invocation-CKMjcA5F.js.map +1 -0
  72. package/dist/{serve-CM3OmF4I.js → serve-CXUf7RtJ.js} +34 -27
  73. package/dist/serve-CXUf7RtJ.js.map +1 -0
  74. package/dist/{server-setup-C9r7jYnz.js → server-setup-Bppjt9xO.js} +81 -207
  75. package/dist/server-setup-Bppjt9xO.js.map +1 -0
  76. package/dist/{start-KAUCsDao.js → start-C1-jrHfU.js} +3 -3
  77. package/dist/{start-KAUCsDao.js.map → start-C1-jrHfU.js.map} +1 -1
  78. package/dist/{stop-gate-hook-CaUrldLh.js → stop-gate-hook-DgJ6sW8N.js} +100 -52
  79. package/dist/stop-gate-hook-DgJ6sW8N.js.map +1 -0
  80. package/dist/{stop-gate-policy-lAabj_pP.js → stop-gate-policy-DG5yYWGn.js} +2 -2
  81. package/dist/{stop-gate-policy-lAabj_pP.js.map → stop-gate-policy-DG5yYWGn.js.map} +1 -1
  82. package/dist/{token-CxPaBEUS.js → token-CnlB0884.js} +2 -2
  83. package/dist/token-CnlB0884.js.map +1 -0
  84. package/dist/{version-_Q1WpsQp.js → version-C8x2hQrZ.js} +14 -2
  85. package/dist/version-C8x2hQrZ.js.map +1 -0
  86. package/dist/{worker-dispatch-Bj1uYyG9.js → worker-dispatch-zW8Zi69V.js} +11 -7
  87. package/dist/worker-dispatch-zW8Zi69V.js.map +1 -0
  88. package/package.json +2 -2
  89. package/dist/attribution-settings-VOPvSn6v.js.map +0 -1
  90. package/dist/claude-OTHh7iKz.js.map +0 -1
  91. package/dist/engine-BKJKMAzh.js +0 -2
  92. package/dist/internal-plan-review-jRMpHu68.js.map +0 -1
  93. package/dist/internal-stop-hook-Ds9LwmZA.js.map +0 -1
  94. package/dist/lifecycle-DCrKbaIU.js +0 -2
  95. package/dist/lifecycle-KKSTKwd6.js +0 -2
  96. package/dist/mcp-workspace-header-CGJbNeHb.js.map +0 -1
  97. package/dist/paths-DN3Nio42.js +0 -2
  98. package/dist/paths-j2B7b0DZ.js.map +0 -1
  99. package/dist/peer-mcp-personas-DRL_xT4h.js.map +0 -1
  100. package/dist/plan-review-hook-Dk5zTNke.js.map +0 -1
  101. package/dist/prompt-submit-hook-lvTWwaTV.js.map +0 -1
  102. package/dist/serve-CM3OmF4I.js.map +0 -1
  103. package/dist/server-setup-C9r7jYnz.js.map +0 -1
  104. package/dist/stop-gate-hook-CaUrldLh.js.map +0 -1
  105. package/dist/token-CxPaBEUS.js.map +0 -1
  106. package/dist/version-_Q1WpsQp.js.map +0 -1
  107. package/dist/worker-dispatch-Bj1uYyG9.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"stop-gate-hook-CaUrldLh.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\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<{ name: string; re: RegExp }> = [\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*\\()/ },\n]\n\n/** Language-specific weakening patterns, selected by the added line's file type. */\nconst LANG_PATTERNS: Readonly<Record<string, ReadonlyArray<{ name: string; re: RegExp }>>> = {\n ts: [\n // Silencing the type-checker.\n { name: \"ts-suppression\", re: /@ts-(ignore|nocheck|expect-error)\\b/ },\n // Casting away type errors.\n { name: \"any-cast\", re: /\\bas\\s+any\\b|:\\s*any\\b/ },\n // Silencing the linter.\n { name: \"eslint-disable\", re: /eslint-disable\\b/ },\n ],\n py: [\n { name: \"py-type-ignore\", re: /#\\s*type:\\s*ignore\\b/ },\n { name: \"py-noqa\", re: /#\\s*noqa\\b/ },\n { name: \"py-skip\", re: /@(pytest\\.mark\\.skip|unittest\\.skip)\\b/ },\n ],\n go: [\n { name: \"go-skip\", re: /\\bt\\.Skip\\s*\\(/ },\n { name: \"go-nolint\", re: /\\/\\/\\s*nolint\\b/ },\n ],\n rust: [\n { name: \"rust-ignore\", re: /#\\[\\s*ignore\\b/ },\n { name: \"rust-allow\", re: /#\\[\\s*allow\\s*\\(/ },\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<{ name: string; re: RegExp }> = [\n ...COMMON_PATTERNS,\n ...Object.values(LANG_PATTERNS).flat(),\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<{ name: string; re: RegExp }> = 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 for (const p of patterns) {\n if (p.re.test(added)) {\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 { 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 * PLAN-MODE env fallback. When the session is in plan mode, plan/memory scratch\n * edits should not trip the gate-WEAKENING scan (a plan/memory markdown file may\n * legitimately quote `it.skip` / `as any` while describing work). The launcher\n * sets `GH_ROUTER_STOP_GATE_PLAN_MODE` for a plan-mode session; the payload may\n * ALSO carry a forward-compat `plan_mode` field (see `decideStopHook`). Either\n * signal being present activates the scoping — the executable gate STILL runs\n * and non-plan hunks are STILL scanned.\n */\nexport function stopGatePlanMode(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_STOP_GATE_PLAN_MODE) === true\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 /**\n * PLAN-MODE scoping (env-fallback signal). When true — OR when the stdin\n * payload carries a truthy `plan_mode` field — diff hunks confined to\n * `plans/` or `memory/` are EXCLUDED from the gate-weakening scan. The\n * executable gate still runs and non-plan (`src/…`) hunks are still scanned;\n * this only narrows the weakening heuristic so plan/memory scratch docs don't\n * trip it. The live wrapper passes `stopGatePlanMode()`.\n */\n planMode?: boolean\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 // PLAN-MODE: active via the env-fallback input OR a forward-compat truthy\n // `plan_mode` payload field. When active, the gate-weakening scan runs over a\n // diff with plans/ + memory/ file sections stripped; the FULL diff is still\n // returned for the advisory review, and the executable gate (which ignores the\n // diff) is unaffected. A truly empty full diff short-circuits before checks,\n // which subsumes plan-only turns with no code changes; the env remains an\n // explicit override for non-empty plan/memory scratch diffs.\n const planMode = input.planMode === true || payload.plan_mode === true\n const scanDiff = (diff: string): string => (planMode ? 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/**\n * Build the shell command string Claude Code runs for the Stop hook. Invokes the\n * running github-router via its node/bun binary so it works regardless of PATH.\n * Pure (takes the binary + script paths) so the quoting is unit-testable; the\n * cross-platform firing is verified by the gated E2E.\n */\nexport function buildStopHookCommand(execPath: string, scriptPath: string | undefined): string {\n const q = (s: string): string => `\"${s}\"`\n if (scriptPath && scriptPath !== execPath) {\n return `${q(execPath)} ${q(scriptPath)} internal-stop-hook`\n }\n return `${q(execPath)} 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 execPath: string,\n scriptPath: string | undefined,\n outPath: string,\n): string {\n const q = (s: string): string => `\"${s}\"`\n const head = scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)}` : q(execPath)\n return `${head} internal-session-bind --out ${q(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(execPath: string, scriptPath: string | undefined): string {\n const q = (s: string): string => `\"${s}\"`\n const head = scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)}` : q(execPath)\n return `${head} 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 +0,0 @@
1
- {"version":3,"file":"token-CxPaBEUS.js","names":["process"],"sources":["../src/lib/state.ts","../src/lib/api-config.ts","../src/lib/upstream-retry.ts","../src/lib/error.ts","../src/services/github/get-copilot-token.ts","../src/services/github/get-device-code.ts","../src/services/github/get-user.ts","../src/services/copilot/get-models.ts","../src/services/get-copilot-version.ts","../src/services/get-vscode-version.ts","../src/lib/editor-version-cache.ts","../src/lib/utils.ts","../src/services/github/poll-access-token.ts","../src/lib/token.ts"],"sourcesContent":["import { randomBytes, randomUUID } from \"node:crypto\"\n\nimport type { ModelsResponse } from \"~/services/copilot/get-models\"\n\nexport interface State {\n githubToken?: string\n copilotToken?: string\n\n /**\n * Where `githubToken` came from, which decides whether the proxy may\n * re-read it from disk.\n *\n * `\"file\"` — read from `PATHS.GITHUB_TOKEN_PATH`. A re-read is allowed, so\n * an out-of-band `github-router auth` heals a running proxy without a\n * restart.\n *\n * `\"explicit\"` — supplied by the operator via `--github-token` / `GH_TOKEN`.\n * The file is NEVER read on this path: silently replacing a\n * caller-supplied credential with whatever happens to be on disk would\n * override an explicit instruction. It also means \"run `github-router\n * auth`\" is false advice for this source — the remediation is to replace\n * the supplied value.\n */\n githubTokenSource?: \"file\" | \"explicit\"\n\n /**\n * Monotonic counter bumped on every SUCCESSFUL Copilot token exchange.\n *\n * This is the retry criterion for `tryRefreshAndRetry`, and it is\n * deliberately not \"did the token string change\": two consecutive\n * successful exchanges were observed returning an identical token, so a\n * string comparison would silently suppress legitimate retries. Nor is it\n * the refresh's own verdict — with requests A and B both holding a stale\n * token, A can refresh while B's later 401 is cooled down to a no-op, and\n * a verdict of \"nothing happened\" would fail B even though a good token is\n * already in `copilotToken`. Comparing generations answers the question\n * each request actually has: \"is what is in state now different from what\n * I already tried?\"\n */\n copilotTokenGeneration: number\n\n /**\n * Wall-clock ms after which `copilotToken` should be refreshed — already\n * skew-adjusted, so the per-request check is a plain `Date.now() >=` compare.\n *\n * Derived from the exchange's `refresh_in` (a DURATION) and never from its\n * absolute `expires_at`. A local clock running ahead of GitHub's would make\n * an absolute timestamp look already-past on a brand-new token and drive a\n * refresh storm; a duration is immune, because it is anchored to the same\n * clock that later reads it.\n */\n copilotTokenRefreshAt?: number\n\n /**\n * Second, WRITE-capable GitHub token for the first-mate\n * agent-orchestration surface (`--agents`). Minted by a separate\n * device-flow login against the GitHub CLI's OAuth client\n * (`GITHUB_AGENT_CLIENT_ID`, scopes `repo workflow read:org`) and\n * stored at `PATHS.GITHUB_AGENT_TOKEN_PATH`, apart from `githubToken`\n * (the Copilot App token, read:user). The first-mate GitHub service\n * layer uses this for ALL its calls (reads included, so it also works\n * on private repos). Undefined until the agent login has run.\n */\n githubAgentToken?: string\n\n accountType: string\n copilotApiUrl?: string\n models?: ModelsResponse\n vsCodeVersion?: string\n copilotVersion?: string\n\n manualApprove: boolean\n rateLimitWait: boolean\n showToken: boolean\n extendedBetas: boolean\n\n /**\n * Opt-in flag for the browser-control MCP tools (`browser_*`). Set by\n * `setupAndServe` from the `--browse` CLI flag or\n * `GH_ROUTER_ENABLE_BROWSE=1` env var. When false, all `browser_*`\n * tools are dropped from `tools/list` AND `tools/call` returns\n * -32601 — same defense-in-depth pattern as `workerToolsEnabled()` /\n * `standInToolEnabled()`. See `browserToolsEnabled()` in\n * `src/routes/mcp/handler.ts`.\n */\n browseEnabled: boolean\n\n /**\n * Opt-in flag for the fleet session-control MCP tools (`mcp__fleet__*`).\n * Set by `setupAndServe` from the `--fleet` CLI flag or\n * `GH_ROUTER_ENABLE_FLEET=1` env var. When false, all fleet tools are\n * dropped from `tools/list` AND `tools/call` returns -32601 — same\n * defense-in-depth pattern as `workerToolsEnabled()` /\n * `standInToolEnabled()`. See `fleetToolsEnabled()` in\n * `src/lib/mcp-capabilities.ts`.\n */\n fleetEnabled: boolean\n\n /**\n * Opt-in flag for the first-mate agent-orchestration MCP tools\n * (`mcp__first-mate__*`) and the in-process GitHub cloud-agent driving\n * layer. Set by `setupAndServe` from the `--agents` CLI flag or\n * `GH_ROUTER_ENABLE_AGENTS=1`. When false, the first-mate tools are\n * dropped from `tools/list` AND `tools/call` returns -32601 — same\n * defense-in-depth pattern as `fleetToolsEnabled()`. Additionally\n * requires `state.githubAgentToken` present (the write login), so the\n * surface is invisible rather than 401-ing when unauthenticated. See\n * `agentToolsEnabled()` in `src/lib/mcp-capabilities.ts`.\n */\n agentsEnabled: boolean\n\n /**\n * When true, --power-browse was passed (or GH_ROUTER_ENABLE_POWER_BROWSE=1\n * is set). Exposes the FULL browser MCP surface (~18 tools) on /mcp,\n * including the L0/L1 primitives that hand DOM details (refs,\n * bboxes, role/name dumps) to the lead model. Default --browse mode\n * exposes only the 6 lead-model tools (act, observe, extract,\n * navigate, screenshot, open_tab). Always implies browseEnabled.\n */\n powerBrowseEnabled: boolean\n\n /**\n * Humanlike pacing override:\n * \"on\" - --humanlike CLI flag or GH_ROUTER_HUMANLIKE=1 env;\n * inject Beta-distributed inter-action delays, Bezier\n * mouse paths, per-keystroke jitter, scroll chunking\n * into every browser_* action dispatch.\n * \"off\" - GH_ROUTER_BROWSER_NO_HUMANLIKE=1; HARD disable, wins\n * over \"on\" so tests are reproducible.\n * \"auto\" - default; pacing engages only when bot-challenge\n * detection fires (Phase 4-future).\n *\n * Lead model never sees this state — it's an internal concern.\n */\n humanlikeForce: \"on\" | \"off\" | \"auto\"\n\n // Rate limiting configuration\n rateLimitSeconds?: number\n lastRequestTimestamp?: number\n\n // Persistent session identifiers to match VS Code fingerprint\n sessionId: string\n machineId: string\n\n /**\n * Per-launch nonce for the loopback `/mcp` endpoint. Set by the\n * `claude` subcommand after `setupAndServe` and before spawning\n * Claude Code; the spawned MCP client reads it from the\n * `--mcp-config` tempfile and presents it as `Authorization: Bearer`.\n * When unset, `/mcp` rejects all requests — closes the\n * loopback-no-auth gap (DNS rebinding, malicious browser-ext\n * native messaging, sibling-process probe).\n */\n peerMcpNonce?: string\n\n /**\n * Set by `github-router serve` (a single machine-wide control plane serving\n * many repos). When true, repo-scoped worker tools require an explicit/So-derived\n * `workspace` and must NOT silently default to the proxy launch cwd.\n */\n serveMode?: boolean\n}\n\nexport const state: State = {\n accountType: \"enterprise\",\n copilotTokenGeneration: 0,\n manualApprove: false,\n rateLimitWait: false,\n showToken: false,\n extendedBetas: false,\n browseEnabled: false,\n fleetEnabled: false,\n agentsEnabled: false,\n powerBrowseEnabled: false,\n humanlikeForce: \"auto\",\n sessionId: randomUUID(),\n machineId: randomBytes(32).toString(\"hex\"),\n}\n","import { randomUUID } from \"node:crypto\"\n\nimport type { State } from \"./state\"\n\nexport const standardHeaders = () => ({\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n})\n\nconst DEFAULT_COPILOT_VERSION = \"0.43.2026033101\"\n\nexport function copilotVersion(state: State): string {\n return state.copilotVersion ?? DEFAULT_COPILOT_VERSION\n}\n\nconst API_VERSION = \"2026-01-09\"\n\nexport const copilotBaseUrl = (state: State) =>\n state.copilotApiUrl ?? \"https://api.githubcopilot.com\"\nexport const copilotHeaders = (\n state: State,\n vision: boolean = false,\n integrationId: string = \"vscode-chat\",\n) => {\n const version = copilotVersion(state)\n const headers: Record<string, string> = {\n Authorization: `Bearer ${state.copilotToken}`,\n \"content-type\": standardHeaders()[\"content-type\"],\n \"copilot-integration-id\": integrationId,\n \"editor-version\": `vscode/${state.vsCodeVersion}`,\n \"editor-plugin-version\": `copilot-chat/${version}`,\n \"user-agent\": `GitHubCopilotChat/${version}`,\n \"openai-intent\": \"conversation-panel\",\n \"x-interaction-type\": \"conversation-panel\",\n \"x-github-api-version\": API_VERSION,\n \"x-request-id\": randomUUID(),\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n \"VScode-SessionId\": state.sessionId,\n \"VScode-MachineId\": state.machineId,\n }\n\n if (vision) headers[\"copilot-vision-request\"] = \"true\"\n\n return headers\n}\n\nexport const GITHUB_API_BASE_URL =\n process.env.GITHUB_API_URL ?? \"https://api.github.com\"\nexport const githubHeaders = (state: State) => ({\n ...standardHeaders(),\n authorization: `token ${state.githubToken}`,\n \"editor-version\": `vscode/${state.vsCodeVersion}`,\n \"editor-plugin-version\": `copilot-chat/${copilotVersion(state)}`,\n \"user-agent\": `GitHubCopilotChat/${copilotVersion(state)}`,\n \"x-github-api-version\": API_VERSION,\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n})\n\nexport const GITHUB_BASE_URL = \"https://github.com\"\nexport const GITHUB_CLIENT_ID = \"Iv1.b507a08c87ecfe98\"\nexport const GITHUB_APP_SCOPES = [\"read:user\"].join(\" \")\n\n// --- First-mate agent-orchestration write identity (`--agents`) ---------\n// The Copilot device-login token above (a GitHub *App* token, read:user)\n// can READ the agent surface but 403s on any write/push. The first-mate\n// controller needs a WRITE-capable token to create issues, assign cloud\n// coding agents, comment, dispatch workflows, and merge. We mint a second,\n// isolated token via a separate device-flow login against the GitHub CLI's\n// public OAuth client (a classic OAuth App that honors the requested\n// scopes — verified to accept `repo workflow read:org` at\n// `/login/device/code`). Stored apart at PATHS.GITHUB_AGENT_TOKEN_PATH;\n// never mixed with `state.githubToken`.\nexport const GITHUB_AGENT_CLIENT_ID = \"178c6fc778ccc68e1d6a\"\nexport const GITHUB_AGENT_SCOPES = [\"repo\", \"workflow\", \"read:org\"].join(\" \")\nexport const GITHUB_GRAPHQL_URL =\n process.env.GITHUB_GRAPHQL_URL ?? \"https://api.github.com/graphql\"\n// GitHub's documented REST/GraphQL API version for the general-purpose\n// (non-Copilot) surface the first-mate layer drives. The Agent-Tasks\n// preview client overrides this per-call with its own dated version.\nexport const GITHUB_REST_API_VERSION = \"2022-11-28\"\nexport const githubAgentHeaders = (state: State) => ({\n ...standardHeaders(),\n authorization: `token ${state.githubAgentToken}`,\n \"x-github-api-version\": GITHUB_REST_API_VERSION,\n \"user-agent\": \"github-router-first-mate\",\n})\nexport const githubAgentGraphQLHeaders = (state: State, features?: string) => ({\n ...githubAgentHeaders(state),\n ...(features ? { \"GraphQL-Features\": features } : {}),\n})\n","/**\n * Shared transient-failure retry for upstream calls (`src/lib/upstream-retry.ts`).\n *\n * Retries ONLY on transient conditions — never on success, never on a\n * deterministic 4xx (400/401/403/404…), so a malformed request or auth\n * failure fails fast instead of being hammered. 401 is intentionally NOT\n * retried here: it stays with the existing token-refresh path so the\n * 401→503 forwardError invariant is preserved.\n *\n * Retryable:\n * - HTTP 429 + 5xx (500/502/503/504) — the \"upstream is sick\" class.\n * - network errors (ECONNRESET / \"fetch failed\" / \"terminated\" / EPIPE …).\n * - upstream TIMEOUT aborts: an AbortError thrown while the CALLER'S\n * `signal` is NOT aborted (a user cancel aborts the caller's signal\n * and is rethrown immediately, never retried).\n *\n * Exponential backoff + FULL jitter, capped, honoring `Retry-After`. The\n * inter-attempt sleep is abortable so a user cancel during backoff\n * returns promptly. Bounded `attempts` keep a single call from holding an\n * inflight slot indefinitely (robust AND fast).\n *\n * Streaming note: the user-facing passthrough and the worker loop must\n * only retry in the PRE-FIRST-BYTE window — a retry after bytes have\n * streamed would duplicate output. Callers there pass a `doFetch` that\n * has not yet handed its body to the consumer.\n */\n\nimport consola from \"consola\"\n\nexport interface TransientRetryOpts {\n /** Total attempts including the first (default 3 → up to 2 retries). */\n attempts?: number\n /** Retryable HTTP statuses (default 429, 500, 502, 503, 504). */\n retryStatuses?: ReadonlyArray<number>\n /** Backoff base (default 250ms) and cap (default 4000ms). */\n baseDelayMs?: number\n maxDelayMs?: number\n /** Caller's abort signal — a user cancel fails fast (never retried). */\n signal?: AbortSignal\n /** Short label for debug logging (e.g. \"codex_critic\", \"advisor\"). */\n label?: string\n /** Upstream endpoint when it differs from the human-readable label. */\n endpoint?: string\n}\n\nconst DEFAULT_RETRY_STATUSES: ReadonlyArray<number> = [429, 500, 502, 503, 504]\nconst TRANSPORT_MESSAGE_MAX = 512\nconst TRANSIENT_CODES = new Set([\n \"ECONNRESET\",\n \"ETIMEDOUT\",\n \"EPIPE\",\n \"EAI_AGAIN\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n // A dispatch that raced a pool teardown. Previously in NEITHER set, so it\n // fell through to \"non_transport\" and was rethrown raw at the retry loop —\n // an unclassified error escaping a classifier is a gap, not a policy.\n \"UND_ERR_CLOSED\",\n // HTTP/2 session death. These reached \"transient\" only incidentally, via the\n // outer TypeError(\"fetch failed\") message match, which means a future change\n // to that string would silently reclassify them. Name them.\n \"ERR_HTTP2_INVALID_SESSION\",\n \"ERR_HTTP2_GOAWAY_SESSION\",\n \"ERR_HTTP2_STREAM_CANCEL\",\n])\n\n/**\n * TLS faults that are genuinely permanent: a protocol/version/cipher mismatch,\n * or a certificate this client cannot validate. Retrying any of these buys\n * three times the latency before the identical failure.\n *\n * Deliberately NOT the blanket `\"ssl routines\"` match this replaces. Every\n * OpenSSL error carries that string, so the catch-all also swallowed alert 20\n * (`bad record mac`) — an integrity fault a fresh connection routinely clears.\n * See TRANSIENT_TLS_MESSAGES.\n *\n * Each entry must stay disjoint from TRANSIENT_TLS_MESSAGES. The bucket order\n * in `classifyTransportError` checks the narrower transient alert first, so an\n * overlap would resolve as transient; `tests/upstream-retry.test.ts` pins that\n * no error matches two buckets.\n */\nconst DETERMINISTIC_TLS_MESSAGES = [\n \"self signed certificate\",\n \"certificate has expired\",\n \"unable to verify the first certificate\",\n \"unable to get local issuer certificate\",\n \"hostname/ip does not match certificate\",\n \"certificate verify failed\",\n \"unknown ca\",\n \"wrong version number\",\n \"unsupported protocol\",\n \"no protocols available\",\n \"alert handshake failure\",\n \"alert protocol version\",\n \"alert insufficient security\",\n \"tls handshake\",\n]\n\n/**\n * TLS alert 20 (`bad_record_mac`) ONLY — a record failed AEAD authentication\n * somewhere on the path. The connection is unusable, but the next one is\n * typically fine, which is exactly what a bounded retry is for.\n *\n * This also matches OpenSSL's local-detection wording (\"decryption failed or\n * bad record mac\", reason 281) as well as the received-alert wording (\"ssl/tls\n * alert bad record mac\", reason 1020). Both are integrity faults and both\n * warrant the same one-bounded-replay policy.\n *\n * An earlier revision promoted four alert phrases; three were wrong and stay\n * deterministic on purpose. Alert 10 (`unexpected message`) is protocol state,\n * alert 21 (`decryption failed`) is legacy-server incompatibility, and alert 22\n * (`record overflow`) is a malformed or desynchronised stream. Promoting those\n * converts fail-fast configuration errors into 3x latency.\n */\nconst TRANSIENT_TLS_MESSAGES = [\"bad record mac\"]\nconst DETERMINISTIC_CODES = new Set([\n \"ENOTFOUND\",\n \"ECONNREFUSED\",\n \"DEPTH_ZERO_SELF_SIGNED_CERT\",\n \"SELF_SIGNED_CERT_IN_CHAIN\",\n \"UNABLE_TO_GET_ISSUER_CERT\",\n \"UNABLE_TO_GET_ISSUER_CERT_LOCALLY\",\n \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\",\n \"CERT_HAS_EXPIRED\",\n \"CERT_NOT_YET_VALID\",\n \"ERR_TLS_CERT_ALTNAME_INVALID\",\n \"ERR_TLS_CERT_SIGNATURE_ALGORITHM_UNSUPPORTED\",\n \"ERR_SSL_WRONG_VERSION_NUMBER\",\n])\nconst CREDENTIAL_RE =\n /\\b(eyJ[A-Za-z0-9_-]{20,}(?:\\.[A-Za-z0-9_-]+){0,2}|gh[opsu]_[A-Za-z0-9_]{20,}|Bearer\\s+\\S{20,})\\b/gi\nconst SECRET_ASSIGNMENT_RE =\n /\\b(authorization|api[_-]?key|access[_-]?token|token|password)\\s*[:=]\\s*([^\\s,;]+)/gi\nconst REQUEST_BODY_RE =\n /\\b(request\\s+body|body)\\s*[:=]\\s*.*$/gi\n\nexport type TransportErrorClassification =\n | \"transient\"\n | \"deterministic\"\n | \"non_transport\"\n | \"cancelled\"\n\nexport interface TransportErrorDetails {\n classification: TransportErrorClassification\n name: string\n message: string\n code?: string\n causeCode?: string\n}\n\nexport interface TransportExhaustionMetadata {\n endpoint?: string\n label?: string\n attempts: number\n classification: \"transient\" | \"deterministic\"\n lastError: Omit<TransportErrorDetails, \"classification\">\n}\n\nexport class TransportExhaustionError extends Error {\n readonly endpoint?: string\n readonly label?: string\n readonly attempts: number\n readonly classification: \"transient\" | \"deterministic\"\n readonly lastError: Omit<TransportErrorDetails, \"classification\">\n\n constructor(metadata: TransportExhaustionMetadata, cause: unknown) {\n const target = metadata.endpoint ?? metadata.label ?? \"upstream\"\n const lastError = {\n name: sanitizeTransportText(metadata.lastError.name),\n message: sanitizeTransportText(metadata.lastError.message),\n code: metadata.lastError.code\n ? sanitizeTransportText(metadata.lastError.code)\n : undefined,\n causeCode: metadata.lastError.causeCode\n ? sanitizeTransportText(metadata.lastError.causeCode)\n : undefined,\n }\n const code = lastError.causeCode ?? lastError.code\n const suffix = code ? ` (${code})` : \"\"\n super(\n `Upstream transport ${metadata.classification === \"transient\" ? \"failed\" : \"is unreachable\"} at ${sanitizeTransportText(target)} after ${metadata.attempts} attempt${metadata.attempts === 1 ? \"\" : \"s\"}: ${lastError.name}: ${lastError.message}${suffix}`,\n { cause },\n )\n this.name = \"TransportExhaustionError\"\n this.endpoint = metadata.endpoint\n ? sanitizeTransportText(metadata.endpoint)\n : undefined\n this.label = metadata.label ? sanitizeTransportText(metadata.label) : undefined\n this.attempts = metadata.attempts\n this.classification = metadata.classification\n this.lastError = lastError\n }\n}\n\nfunction readStringProperty(value: unknown, key: string): string | undefined {\n if (\n (typeof value !== \"object\" && typeof value !== \"function\")\n || value === null\n ) {\n return undefined\n }\n try {\n const property = (value as Record<string, unknown>)[key]\n return typeof property === \"string\" ? property : undefined\n } catch {\n return undefined\n }\n}\n\nfunction readCause(value: unknown): unknown {\n if (\n (typeof value !== \"object\" && typeof value !== \"function\")\n || value === null\n ) {\n return undefined\n }\n try {\n return (value as { cause?: unknown }).cause\n } catch {\n return undefined\n }\n}\n\nfunction safeString(value: unknown): string {\n try {\n return String(value)\n } catch {\n return \"[unserializable error]\"\n }\n}\n\nexport function sanitizeTransportText(value: string): string {\n const sanitized = value\n .replace(/\\r\\n|\\r|\\n/g, \" \")\n .replace(CREDENTIAL_RE, \"[REDACTED]\")\n .replace(SECRET_ASSIGNMENT_RE, \"$1=[REDACTED]\")\n .replace(REQUEST_BODY_RE, \"$1=[REDACTED]\")\n return sanitized.length > TRANSPORT_MESSAGE_MAX\n ? `${sanitized.slice(0, TRANSPORT_MESSAGE_MAX)}…`\n : sanitized\n}\n\n/**\n * Classify errors thrown before the first response byte. Deterministic\n * connectivity/configuration signals win over generic runtime wording such as\n * TypeError(\"fetch failed\"), because the nested cause is more specific.\n *\n * Buckets are evaluated in a stated total order, narrowest first, and the first\n * match wins — so no error can land in two of them:\n *\n * 1. caller cancel\n * 2. TLS alert 20 (`bad record mac`) — integrity, retryable\n * 3. deterministic cert/config codes and messages, plus refused/DNS-miss\n * 4. transient codes (incl. HTTP/2 session death) and generic transport text\n * 5. everything else — `non_transport`, rethrown raw\n */\nexport function classifyTransportError(\n error: unknown,\n opts: { callerCancelled?: boolean } = {},\n): TransportErrorDetails {\n const name = readStringProperty(error, \"name\") ?? \"Error\"\n const rawMessage =\n readStringProperty(error, \"message\")\n ?? (typeof error === \"string\" ? error : safeString(error))\n const message = sanitizeTransportText(rawMessage)\n const code = readStringProperty(error, \"code\")\n\n const causes: unknown[] = []\n const seen = new Set<unknown>()\n let current = readCause(error)\n for (let depth = 0; current !== undefined && depth < 4; depth++) {\n if (\n (typeof current === \"object\" || typeof current === \"function\")\n && current !== null\n ) {\n if (seen.has(current)) break\n seen.add(current)\n }\n causes.push(current)\n current = readCause(current)\n }\n const causeCode = causes\n .map((cause) => readStringProperty(cause, \"code\"))\n .find((value): value is string => value !== undefined)\n const codes = [code, ...causes.map((cause) => readStringProperty(cause, \"code\"))]\n .filter((value): value is string => value !== undefined)\n .map((value) => value.toUpperCase())\n const messages = [\n rawMessage,\n ...causes.map((cause) => readStringProperty(cause, \"message\") ?? \"\"),\n ]\n .join(\" \")\n .toLowerCase()\n\n if (opts.callerCancelled) {\n return { classification: \"cancelled\", name, message, code, causeCode }\n }\n // Narrowest match first: TLS alert 20 is an integrity fault, not a\n // configuration fault, and it must not be swallowed by the broader cert /\n // protocol patterns below (which is exactly what the old blanket\n // `\"ssl routines\"` match did — it failed the alert after ONE attempt).\n if (TRANSIENT_TLS_MESSAGES.some((phrase) => messages.includes(phrase))) {\n return { classification: \"transient\", name, message, code, causeCode }\n }\n if (\n codes.some((value) => DETERMINISTIC_CODES.has(value))\n || DETERMINISTIC_TLS_MESSAGES.some((phrase) => messages.includes(phrase))\n // Catch-all for every OTHER OpenSSL failure. Safe here, and only here,\n // because the one retryable alert already returned above — so this keeps\n // fail-fast for the long tail (`tlsv13 alert certificate required`,\n // `sslv3 alert bad certificate`, `tlsv1 alert internal error`, …) without\n // re-swallowing alert 20.\n //\n // Without it those errors fall through to the generic transient branch via\n // the outer `TypeError: fetch failed`, turning a permanent configuration\n // failure into three attempts. Narrowing the deterministic list alone was\n // not sufficient; the residue has to land somewhere deliberate.\n || messages.includes(\"ssl routines\")\n // A refused connection or a DNS miss is a configuration signal, so it\n // fails fast rather than being hammered. Kept deliberately, not inherited:\n // transient DNS is already covered by EAI_AGAIN, and a genuinely\n // restarting upstream surfaces as ECONNRESET or a 5xx, both of which do\n // retry.\n || messages.includes(\"econnrefused\")\n || messages.includes(\"enotfound\")\n ) {\n return { classification: \"deterministic\", name, message, code, causeCode }\n }\n if (\n name === \"AbortError\"\n || name === \"TimeoutError\"\n || name === \"InactivityTimeout\"\n || codes.some((value) => TRANSIENT_CODES.has(value))\n || messages.includes(\"terminated\")\n || messages.includes(\"fetch failed\")\n || messages.includes(\"network error\")\n || messages.includes(\"socket hang up\")\n || messages.includes(\"socket closed\")\n || messages.includes(\"econnreset\")\n || messages.includes(\"etimedout\")\n || messages.includes(\"epipe\")\n || messages.includes(\"eai_again\")\n ) {\n return { classification: \"transient\", name, message, code, causeCode }\n }\n return { classification: \"non_transport\", name, message, code, causeCode }\n}\n\nfunction transportExhaustion(\n error: unknown,\n attempts: number,\n opts: TransientRetryOpts,\n details: TransportErrorDetails,\n): TransportExhaustionError {\n if (\n details.classification !== \"transient\"\n && details.classification !== \"deterministic\"\n ) {\n throw new TypeError(\"transportExhaustion requires a transport error\")\n }\n return new TransportExhaustionError(\n {\n endpoint: opts.endpoint ?? opts.label,\n label: opts.label,\n attempts,\n classification: details.classification,\n lastError: {\n name: details.name,\n message: details.message,\n code: details.code,\n causeCode: details.causeCode,\n },\n },\n error,\n )\n}\n\nfunction parseRetryAfter(headerValue: string | null): number | undefined {\n if (!headerValue) return undefined\n const secs = Number(headerValue)\n if (Number.isFinite(secs)) return Math.max(0, secs * 1000)\n const dateMs = Date.parse(headerValue)\n if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now())\n return undefined\n}\n\n/** Sleep that resolves early (does not reject) when `signal` aborts — the\n * retry loop re-checks `signal.aborted` at the top and throws there. */\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n let settled = false\n const done = (): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n signal?.removeEventListener(\"abort\", done)\n resolve()\n }\n const timer = setTimeout(done, ms)\n if (signal) {\n if (signal.aborted) {\n done()\n return\n }\n signal.addEventListener(\"abort\", done, { once: true })\n }\n })\n}\n\n/**\n * Run `doFetch` with bounded transient-failure retries. `doFetch` is a\n * thunk so each attempt issues a FRESH request (bodies can't be replayed\n * from a consumed stream). Returns the final `Response` — which may still\n * carry a retryable status if all attempts are exhausted (the caller\n * handles that as it would a single-shot failure).\n */\nexport async function fetchWithTransientRetry(\n doFetch: (attempt: number) => Promise<Response>,\n opts: TransientRetryOpts = {},\n): Promise<Response> {\n const attempts = Math.max(1, opts.attempts ?? 3)\n const retryStatuses = opts.retryStatuses ?? DEFAULT_RETRY_STATUSES\n const baseDelayMs = opts.baseDelayMs ?? 250\n const maxDelayMs = opts.maxDelayMs ?? 4000\n const { signal, label } = opts\n\n for (let attempt = 1; ; attempt++) {\n if (signal?.aborted) {\n throw new DOMException(\"This operation was aborted\", \"AbortError\")\n }\n\n let res: Response | undefined\n let caught: unknown\n try {\n res = await doFetch(attempt)\n } catch (err) {\n caught = err\n }\n\n // Success or a non-retryable status → done.\n if (res && !retryStatuses.includes(res.status)) return res\n\n // A thrown error: a user cancel fails fast; otherwise only retry the\n // transient network/timeout class.\n if (caught !== undefined) {\n if (signal?.aborted) throw caught\n const details = classifyTransportError(caught)\n if (details.classification === \"deterministic\") {\n throw transportExhaustion(caught, attempt, opts, details)\n }\n if (details.classification !== \"transient\") throw caught\n }\n\n // Out of attempts → return the last error response (or rethrow).\n if (attempt >= attempts) {\n if (res) return res\n const details = classifyTransportError(caught)\n throw transportExhaustion(caught, attempt, opts, details)\n }\n\n // Free the connection before retrying a retryable-status response.\n const retryAfterMs = res\n ? parseRetryAfter(res.headers.get(\"retry-after\"))\n : undefined\n if (res?.body) {\n try {\n await res.body.cancel()\n } catch {\n /* already torn down */\n }\n }\n\n // Full jitter (random within the exponential cap), Retry-After wins.\n const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1))\n const delay = Math.min(\n maxDelayMs,\n retryAfterMs ?? Math.round(Math.random() * expCap),\n )\n if (label) {\n const why = res\n ? `HTTP ${res.status}`\n : (caught as { name?: string } | undefined)?.name ?? \"error\"\n consola.debug(\n `[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay}ms`,\n )\n }\n await abortableSleep(delay, signal)\n }\n}\n\n/** Extract an HTTP status from a thrown error (HTTPError carries\n * `.response.status`; others may carry `.status`/`.statusCode`; last\n * resort parses `\"HTTP <code>\"` from the message). */\nfunction errorStatus(err: unknown): number | undefined {\n const e = err as\n | {\n status?: unknown\n statusCode?: unknown\n response?: { status?: unknown }\n message?: string\n }\n | undefined\n for (const v of [e?.status, e?.statusCode, e?.response?.status]) {\n if (typeof v === \"number\") return v\n }\n const m = /\\bHTTP (\\d{3})\\b/.exec(e?.message ?? \"\")\n return m ? Number(m[1]) : undefined\n}\n\n/**\n * Generic transient-retry for a non-`Response`-returning call (e.g. the\n * Copilot service clients, which throw `HTTPError` on non-OK and throw on\n * network errors). Retries `fn()` when it throws a transient error — an\n * `HTTPError`-like with a retryable status (429/5xx) OR a transient\n * network/timeout error — using the same backoff + abort semantics as\n * `fetchWithTransientRetry`. Never retries a deterministic 4xx (incl.\n * 401), a non-transient throw, or a user cancel.\n */\nexport async function withTransientRetry<T>(\n fn: (attempt: number) => Promise<T>,\n opts: TransientRetryOpts = {},\n): Promise<T> {\n const attempts = Math.max(1, opts.attempts ?? 3)\n const retryStatuses = opts.retryStatuses ?? DEFAULT_RETRY_STATUSES\n const baseDelayMs = opts.baseDelayMs ?? 250\n const maxDelayMs = opts.maxDelayMs ?? 4000\n const { signal, label } = opts\n\n for (let attempt = 1; ; attempt++) {\n if (signal?.aborted) {\n throw new DOMException(\"This operation was aborted\", \"AbortError\")\n }\n try {\n return await fn(attempt)\n } catch (err) {\n if (signal?.aborted) throw err\n const status = errorStatus(err)\n const details = classifyTransportError(err)\n if (status !== undefined) {\n if (!retryStatuses.includes(status) || attempt >= attempts) throw err\n } else {\n if (details.classification === \"deterministic\") {\n throw transportExhaustion(err, attempt, opts, details)\n }\n if (details.classification !== \"transient\") throw err\n if (attempt >= attempts) {\n throw transportExhaustion(err, attempt, opts, details)\n }\n }\n\n const retryAfterMs = parseRetryAfter(\n (err as { response?: { headers?: { get?: (k: string) => string | null } } })\n ?.response?.headers?.get?.(\"retry-after\") ?? null,\n )\n const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1))\n const delay = Math.min(\n maxDelayMs,\n retryAfterMs ?? Math.round(Math.random() * expCap),\n )\n if (label) {\n consola.debug(\n `[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${\n status !== undefined ? `HTTP ${status}` : (err as { name?: string })?.name ?? \"error\"\n }); retrying in ${delay}ms`,\n )\n }\n await abortableSleep(delay, signal)\n }\n }\n}\n","import type { Context } from \"hono\"\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\"\n\nimport consola from \"consola\"\n\nimport {\n sanitizeTransportText,\n TransportExhaustionError,\n} from \"~/lib/upstream-retry\"\n\nexport class HTTPError extends Error {\n response: Response\n\n constructor(message: string, response: Response) {\n super(message)\n this.response = response\n }\n}\n\nexport async function forwardError(c: Context, error: unknown) {\n consola.error(`Error occurred at ${c.req.path}:`, error)\n\n if (error instanceof TransportExhaustionError) {\n const target = sanitizeTransportText(\n error.endpoint ?? error.label ?? c.req.path,\n )\n const diagnostic = formatTransportDiagnostic(error)\n if (error.classification === \"transient\") {\n // 502 / api_error, NOT 503 / overloaded_error.\n //\n // We already exhausted our own retries against a transport that failed,\n // so \"I am busy, retry later\" is both inaccurate and expensive:\n // `overloaded_error` is the type Claude Code retries hardest against,\n // and each client retry costs another full round of upstream attempts.\n // On a session near the 1M-token context limit that re-uploads a\n // multi-megabyte body every time, which is what turned a fast failure\n // into multi-minute stalls when this mapping shipped.\n //\n // Trade-off, deliberate: the client now surfaces the error instead of\n // papering over a genuinely recoverable blip. That is the behaviour\n // operators had before the mapping was introduced.\n return c.json(\n {\n type: \"error\",\n error: {\n type: \"api_error\",\n message: `Upstream transport was interrupted at ${target} after ${error.attempts} attempts. ${diagnostic}`,\n },\n },\n 502,\n )\n }\n return c.json(\n {\n type: \"error\",\n error: {\n type: \"api_error\",\n message: `Could not connect to upstream at ${target}. Check DNS, proxy, firewall, and TLS configuration. ${diagnostic}`,\n },\n },\n 502,\n )\n }\n\n if (error instanceof HTTPError) {\n const errorText = await error.response.text().catch(() => \"\")\n let errorJson: unknown\n try {\n errorJson = JSON.parse(errorText)\n } catch {\n errorJson = undefined\n }\n\n // Map upstream context-overflow errors (413, or 400 with a known\n // overflow substring) to Anthropic's \"prompt is too long\" 400 shape so\n // Claude Code triggers self-compaction instead of bubbling the error.\n // Note: a live probe of an oversized prompt against Copilot returned\n // 200 with stop_reason:\"refusal\" rather than 413/400 — this guard is\n // defensive for the documented Anthropic contract, not load-bearing.\n if (isContextOverflow(error.response.status, errorJson, errorText)) {\n const upstream = resolveErrorMessage(errorJson, errorText)\n consola.error(\"HTTP error (mapped to overflow):\", errorJson ?? errorText)\n return c.json(\n {\n type: \"error\",\n error: {\n type: \"invalid_request_error\",\n message: `prompt is too long: ${upstream}`,\n },\n },\n 400,\n )\n }\n\n // Remap upstream 401 to 503 — maintain the no-401 invariant on the\n // Anthropic-shape boundary. Claude Code's reactive refresh path\n // (function `SZ1` → `D3(0,true,...)` in v2.1.140 binary) fires on\n // any 401 from upstream and attempts to refresh the OAuth token.\n // Spawned-via-proxy sessions use a synthetic credential\n // (`ensureClaudeConfigMirror`'s SYNTHETIC_CREDENTIAL); refreshing\n // it would fail and degrade the session. Mapping 401 → 503 lets\n // the upstream message still reach the user while side-stepping\n // the refresh path. 503 maps to Anthropic's \"overloaded_error\"\n // type — semantically reasonable for \"proxy got an upstream\n // failure, retry later\".\n const responseStatus =\n error.response.status === 401 ? 503 : error.response.status\n\n // Forward upstream Anthropic-format errors as-is (with remapped status)\n if (isAnthropicError(errorJson)) {\n consola.error(\"HTTP error:\", errorJson)\n return c.json(errorJson, responseStatus as ContentfulStatusCode)\n }\n\n const message = resolveErrorMessage(errorJson, errorText)\n consola.error(\"HTTP error:\", errorJson ?? errorText)\n return c.json(\n {\n type: \"error\",\n error: {\n type: resolveErrorType(responseStatus),\n message,\n },\n },\n responseStatus as ContentfulStatusCode,\n )\n }\n\n return c.json(\n {\n type: \"error\",\n error: {\n type: \"api_error\",\n message: error instanceof Error ? error.message : String(error),\n },\n },\n 500,\n )\n}\n\nfunction formatTransportDiagnostic(error: TransportExhaustionError): string {\n const code = error.lastError.causeCode ?? error.lastError.code\n const codeText = code ? ` (${sanitizeTransportText(code)})` : \"\"\n return `${sanitizeTransportText(error.lastError.name)}: ${sanitizeTransportText(error.lastError.message)}${codeText}`\n}\n\n// Extracts error message from { message } or { error: { message } } payloads.\nfunction resolveErrorMessage(errorJson: unknown, fallback: string): string {\n if (typeof errorJson !== \"object\" || errorJson === null) return fallback\n\n const errorRecord = errorJson as Record<string, unknown>\n if (errorRecord.message !== undefined) return String(errorRecord.message)\n\n if (typeof errorRecord.error === \"object\" && errorRecord.error !== null) {\n const nestedRecord = errorRecord.error as Record<string, unknown>\n if (nestedRecord.message !== undefined) return String(nestedRecord.message)\n }\n\n return fallback\n}\n\n/**\n * Check if a parsed JSON body is already in Anthropic error format:\n * { type: \"error\", error: { type: \"...\", message: \"...\" } }\n */\nfunction isAnthropicError(json: unknown): boolean {\n if (typeof json !== \"object\" || json === null) return false\n const record = json as Record<string, unknown>\n if (record.type !== \"error\") return false\n if (typeof record.error !== \"object\" || record.error === null) return false\n const inner = record.error as Record<string, unknown>\n return typeof inner.type === \"string\" && typeof inner.message === \"string\"\n}\n\nconst CONTEXT_OVERFLOW_SUBSTRINGS = [\n \"prompt is too long\",\n \"context_length_exceeded\",\n \"context length exceeded\",\n \"input is too long\",\n \"maximum context length\",\n \"too many tokens\",\n]\n\n/**\n * Detect upstream context-overflow errors so we can remap them to a 400\n * \"prompt is too long\" shape that triggers Claude Code self-compaction.\n *\n * Always remaps 413 (treated as a hard payload-size signal regardless of\n * body wording). Remaps 400 only when the error text contains one of the\n * known overflow substrings — a regular 400 (e.g. \"model not found\") must\n * NOT remap.\n */\nexport function isContextOverflow(\n status: number,\n errorJson: unknown,\n errorText: string,\n): boolean {\n if (status === 413) return true\n if (status !== 400) return false\n\n const haystack = (\n errorText +\n \" \" +\n (typeof errorJson === \"object\" && errorJson !== null\n ? JSON.stringify(errorJson)\n : \"\")\n ).toLowerCase()\n\n return CONTEXT_OVERFLOW_SUBSTRINGS.some((s) => haystack.includes(s))\n}\n\n/**\n * Map HTTP status to Anthropic error type.\n *\n * Note: a 401 from upstream is remapped to 503 in `forwardError` BEFORE\n * this function is called (no-401 invariant — see comment there). The\n * 401 → \"authentication_error\" mapping below is preserved for\n * defensive coverage in case any code path calls `resolveErrorType`\n * directly with an unsanitized status.\n */\nfunction resolveErrorType(status: number): string {\n if (status === 400) return \"invalid_request_error\"\n if (status === 401) return \"authentication_error\"\n if (status === 403) return \"permission_error\"\n if (status === 404) return \"not_found_error\"\n if (status === 429) return \"rate_limit_error\"\n if (status === 503) return \"overloaded_error\"\n if (status === 529) return \"overloaded_error\"\n return \"api_error\"\n}\n","import consola from \"consola\"\nimport { createHash } from \"node:crypto\"\n\nimport { GITHUB_API_BASE_URL, githubHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\nimport { fetchWithTransientRetry } from \"~/lib/upstream-retry\"\n\n/**\n * Allowlist of hosts the router will trust as the Copilot API base URL.\n * Anything else returned in `endpoints.api` (e.g. via a tampered or\n * misconfigured token-exchange response) is rejected — otherwise a\n * malicious value would receive the long-lived GitHub PAT we send to\n * `/mcp` for web search (see `src/services/copilot/web-search.ts`).\n */\nconst COPILOT_HOST_ALLOWLIST = [\n \"api.githubcopilot.com\",\n \"api.individual.githubcopilot.com\",\n \"api.business.githubcopilot.com\",\n \"api.enterprise.githubcopilot.com\",\n]\n\n/**\n * True iff `rawUrl` is an HTTPS URL whose host is a trusted Copilot API host.\n * Exported so every path that sends a long-lived token to a discovered\n * Copilot host (web search, first-mate CAPI session logs) shares one allowlist.\n */\nexport function isAllowedCopilotHost(rawUrl: string): boolean {\n let parsed: URL\n try {\n parsed = new URL(rawUrl)\n } catch {\n return false\n }\n if (parsed.protocol !== \"https:\") return false\n return COPILOT_HOST_ALLOWLIST.includes(parsed.hostname)\n}\n\n/**\n * Short, non-reversible identifier for a credential, safe to log.\n *\n * During the 2026-08-08 incident the logs could not answer the one question\n * that mattered: was the process holding a stale credential, or was the\n * credential on disk itself dead? The operator re-authenticated at 13:53 and\n * the proxy kept failing, and nothing recorded whether those were the same\n * secret. A truncated SHA-256 makes two credentials comparable across log\n * lines without ever printing one. 8 hex chars is ample to distinguish the\n * handful of credentials a machine sees, and far too short to attack.\n */\nexport function credentialFingerprint(token: string | undefined): string {\n if (!token) return \"none\"\n return createHash(\"sha256\").update(token).digest(\"hex\").slice(0, 8)\n}\n\n/** How a failed token exchange should be treated. */\nexport type TokenExchangeFailureKind =\n /** The credential itself was rejected. Only a human can fix this. */\n | \"credential_rejected\"\n /** The entitlement lapsed; the credential is fine. Different remedy. */\n | \"entitlement_lapsed\"\n /** Anything else — rate limits, 5xx, transport. Retrying may work. */\n | \"transient\"\n\n/**\n * A failed `/copilot_internal/v2/token` exchange, carrying enough to diagnose\n * it after the fact.\n *\n * The predecessor threw `HTTPError(\"Failed to get Copilot token\", response)`\n * and every catch logged only `.message`, so status and body were dropped —\n * which is why the incident is unattributable.\n */\nexport class CopilotTokenExchangeError extends HTTPError {\n readonly kind: TokenExchangeFailureKind\n readonly status: number\n /** Fingerprint of the credential that was rejected, never the credential. */\n readonly credential: string\n\n constructor(opts: {\n kind: TokenExchangeFailureKind\n status: number\n credential: string\n detail: string\n response: Response\n }) {\n super(\n `Failed to get Copilot token: HTTP ${opts.status} (${opts.kind}, credential ${opts.credential})${\n opts.detail ? ` — ${opts.detail}` : \"\"\n }`,\n opts.response,\n )\n this.kind = opts.kind\n this.status = opts.status\n this.credential = opts.credential\n }\n}\n\n/** Upper bound on how much of an error body we quote into a log line. */\nconst ERROR_BODY_SNIPPET_CHARS = 300\n\n/**\n * `notification_id` values that mean \"the credential is valid but the\n * entitlement is not\". These need a different remedy than a bad credential:\n * telling someone whose subscription lapsed to re-authenticate sends them\n * around a loop that cannot help.\n */\nconst ENTITLEMENT_NOTIFICATIONS = new Set([\n \"access_revoked\",\n \"subscription_ended\",\n \"no_copilot_access\",\n])\n\n/**\n * Classify a non-OK exchange response.\n *\n * Deliberately asymmetric: only a 401, or a 403 that NAMES an entitlement\n * problem, is treated as terminal. A bare 403 stays transient because GitHub\n * returns 403 for primary rate limits (`x-ratelimit-remaining: 0`) and\n * secondary abuse limits — telling a user with a perfectly good credential to\n * re-authenticate because they were briefly rate-limited would be a worse\n * failure than the one being fixed. The costs are not symmetric: calling a\n * terminal failure transient wastes some retries, while calling a transient\n * failure terminal kills a working session.\n */\nfunction classifyExchangeFailure(\n status: number,\n body: string,\n): TokenExchangeFailureKind {\n if (status === 401) return \"credential_rejected\"\n if (status === 403) {\n let notificationId: unknown\n try {\n const parsed = JSON.parse(body) as Record<string, unknown>\n notificationId =\n parsed.notification_id\n ?? (parsed.error as Record<string, unknown> | undefined)?.notification_id\n } catch {\n // Not JSON: a rate-limit body, an HTML error page. Transient.\n }\n if (\n typeof notificationId === \"string\"\n && ENTITLEMENT_NOTIFICATIONS.has(notificationId)\n ) {\n return \"entitlement_lapsed\"\n }\n }\n return \"transient\"\n}\n\n/**\n * Exchange the GitHub credential for a short-lived Copilot token.\n *\n * `credentialOverride` lets a caller try a candidate credential WITHOUT\n * publishing it to the process-global `state` first. That matters because\n * `state` is shared by every concurrent request: installing an unvalidated\n * credential globally, even briefly, would make unrelated in-flight requests\n * send it and 401 while a perfectly good credential was still in hand.\n */\nexport const getCopilotToken = async (credentialOverride?: string) => {\n const credentialInUse = credentialOverride ?? state.githubToken\n const credential = credentialFingerprint(credentialInUse)\n // A shallow view over the real state: everything else (editor version,\n // plugin version) still comes from the live object, only the credential is\n // substituted, and nothing is mutated.\n const authState =\n credentialOverride === undefined\n ? state\n : { ...state, githubToken: credentialOverride }\n\n // GitHub PAT → Copilot token exchange. A transient 429/5xx/network blip\n // here aborts launch (and the interval-driven refresh that keeps the\n // session alive), so retry the transient class with bounded backoff. NO\n // 401-refresh compose: this call IS the token source, and a 401 means a\n // bad/expired GitHub PAT — deterministic, fail fast (retrying would burn\n // budget against a credential that can't recover without re-auth).\n const response = await fetchWithTransientRetry(\n () =>\n fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, {\n headers: githubHeaders(authState),\n }),\n { label: \"/copilot_internal/v2/token\" },\n )\n\n if (!response.ok) {\n // Read the body BEFORE throwing. The predecessor discarded it, so every\n // occurrence of this failure was unattributable after the fact.\n //\n // Read the ORIGINAL rather than a clone: a clone leaves the original body\n // unconsumed, and since the only consumer downstream is a `.message` log,\n // that stream — and its socket — would stay pinned open. The body we need\n // is captured into `detail` here, so nothing downstream has to re-read it.\n let body: string\n try {\n body = await response.text()\n } catch {\n body = \"(could not read error body)\"\n }\n const kind = classifyExchangeFailure(response.status, body)\n throw new CopilotTokenExchangeError({\n kind,\n status: response.status,\n credential,\n detail: body.slice(0, ERROR_BODY_SNIPPET_CHARS).replaceAll(/\\s+/g, \" \").trim(),\n // The original body is now consumed, so hand the error a fresh Response\n // carrying the same bytes. `HTTPError.response` stays readable for any\n // future consumer, without leaving an unconsumed stream behind.\n response: new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n }),\n })\n }\n\n const data = (await response.json()) as GetCopilotTokenResponse\n\n // Use the API base URL from the token response if available, matching\n // how VS Code determines the CAPI endpoint dynamically — but only when\n // it points at a github-controlled host (see allowlist above).\n // We deliberately do NOT clobber an existing `state.copilotApiUrl` in\n // the disallowed branch: when the user sets `COPILOT_API_URL` themselves\n // (e.g. for local testing or a CI mock), that's an explicit opt-in and\n // a different threat model than a tampered token-exchange response.\n // Allowlist-failing token-response values are simply ignored.\n if (data.endpoints?.api) {\n if (isAllowedCopilotHost(data.endpoints.api)) {\n state.copilotApiUrl = data.endpoints.api\n } else {\n consola.warn(\n `Refusing to honor Copilot API endpoint \"${data.endpoints.api}\" from ` +\n `the token-exchange response — not in allowlist ` +\n `(${COPILOT_HOST_ALLOWLIST.join(\", \")}). ` +\n (state.copilotApiUrl\n ? `Keeping existing override \"${state.copilotApiUrl}\".`\n : `Falling back to the default api.githubcopilot.com.`),\n )\n }\n }\n\n return data\n}\n\ninterface GetCopilotTokenResponse {\n expires_at: number\n refresh_in: number\n token: string\n endpoints?: {\n api?: string\n proxy?: string\n telemetry?: string\n \"origin-tracker\"?: string\n }\n}\n","import {\n GITHUB_APP_SCOPES,\n GITHUB_BASE_URL,\n GITHUB_CLIENT_ID,\n standardHeaders,\n} from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { fetchWithTransientRetry } from \"~/lib/upstream-retry\"\n\nexport interface DeviceApp {\n clientId: string\n scope: string\n}\n\nconst DEFAULT_DEVICE_APP: DeviceApp = {\n clientId: GITHUB_CLIENT_ID,\n scope: GITHUB_APP_SCOPES,\n}\n\nexport async function getDeviceCode(\n app: DeviceApp = DEFAULT_DEVICE_APP,\n): Promise<DeviceCodeResponse> {\n // Idempotent device-code bootstrap POST (just requests a fresh code) — a\n // transient 429/5xx/network blip here aborts the whole login flow, so\n // retry the transient class. No auth on this call, so no 401 concern.\n //\n // `app` is parameterized so a SECOND identity (the first-mate write\n // token via the GitHub CLI OAuth client) can reuse this exact flow\n // without a copy-paste fork; defaults preserve the original Copilot\n // App device login.\n const response = await fetchWithTransientRetry(\n () =>\n fetch(`${GITHUB_BASE_URL}/login/device/code`, {\n method: \"POST\",\n headers: standardHeaders(),\n body: JSON.stringify({\n client_id: app.clientId,\n scope: app.scope,\n }),\n }),\n { label: \"/login/device/code\" },\n )\n\n if (!response.ok) throw new HTTPError(\"Failed to get device code\", response)\n\n return (await response.json()) as DeviceCodeResponse\n}\n\nexport interface DeviceCodeResponse {\n device_code: string\n user_code: string\n verification_uri: string\n expires_in: number\n interval: number\n}\n","import { GITHUB_API_BASE_URL, standardHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\nimport { fetchWithTransientRetry } from \"~/lib/upstream-retry\"\n\nexport async function getGitHubUser() {\n // GitHub PAT GET — retry transient 429/5xx/network; a 401 (bad PAT) fails\n // fast (not retried by the helper).\n const response = await fetchWithTransientRetry(\n () =>\n fetch(`${GITHUB_API_BASE_URL}/user`, {\n headers: {\n authorization: `token ${state.githubToken}`,\n ...standardHeaders(),\n },\n }),\n { label: \"/user\" },\n )\n\n if (!response.ok) throw new HTTPError(\"Failed to get GitHub user\", response)\n\n return (await response.json()) as GithubUserResponse\n}\n\n// Trimmed for the sake of simplicity\ninterface GithubUserResponse {\n login: string\n}\n","import { copilotBaseUrl, copilotHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\nimport { tryRefreshAndRetry } from \"~/lib/token\"\nimport { fetchWithTransientRetry } from \"~/lib/upstream-retry\"\n\nexport const getModels = async () => {\n // Startup catalog fetch — a transient 429/5xx/network blip here leaves\n // the model catalog empty for the whole session, so retry it. This is a\n // Copilot-token GET, so it keeps the 401-refresh path\n // (`tryRefreshAndRetry`) nested INSIDE the transient retry: 401 →\n // refresh once (never retried by the transient layer); 429/5xx/network\n // → bounded retry with backoff. A consumed (non-streamed) GET body is\n // safe to replay.\n const response = await fetchWithTransientRetry(\n () =>\n tryRefreshAndRetry(\n () =>\n fetch(`${copilotBaseUrl(state)}/models`, {\n headers: copilotHeaders(state),\n }),\n \"/models\",\n ),\n { label: \"/models\" },\n )\n\n if (!response.ok) throw new HTTPError(\"Failed to get models\", response)\n\n return (await response.json()) as ModelsResponse\n}\n\nexport interface ModelsResponse {\n data: Array<Model>\n object: string\n}\n\ninterface ModelLimits {\n max_context_window_tokens?: number\n max_output_tokens?: number\n max_prompt_tokens?: number\n max_inputs?: number\n max_non_streaming_output_tokens?: number\n vision?: {\n max_prompt_image_size?: number\n max_prompt_images?: number\n supported_media_types?: string[]\n }\n}\n\ninterface ModelSupports {\n tool_calls?: boolean\n parallel_tool_calls?: boolean\n dimensions?: boolean\n streaming?: boolean\n vision?: boolean\n structured_outputs?: boolean\n adaptive_thinking?: boolean\n max_thinking_budget?: number\n min_thinking_budget?: number\n reasoning_effort?: Array<string>\n}\n\ninterface ModelCapabilities {\n family: string\n limits?: ModelLimits\n object: string\n supports?: ModelSupports\n tokenizer: string\n type: string\n}\n\nexport interface Model {\n capabilities: ModelCapabilities\n id: string\n model_picker_enabled: boolean\n name: string\n object: string\n preview: boolean\n vendor: string\n version: string\n supported_endpoints?: Array<string>\n requestHeaders?: Record<string, string>\n policy?: {\n state: string\n terms: string\n }\n billing?: {\n is_premium: boolean\n multiplier: number\n restricted_to?: string[]\n /**\n * Per-token prices, scaled by 1e9. Present on the live catalog even\n * where `is_premium` / `multiplier` are absent (usage-based-billing\n * accounts populate this instead), which is why both shapes are\n * modelled: reading only the premium pair silently yields nothing\n * on those accounts.\n */\n token_prices?: {\n input_price?: number\n output_price?: number\n cache_price?: number\n batch_size?: number\n }\n }\n is_chat_default?: boolean\n is_chat_fallback?: boolean\n model_picker_category?: string\n /** Vendor-authored cost tier (`low` | `medium` | `high` | `very_high`). */\n model_picker_price_category?: string\n info_messages?: Array<{ code: string; message: string }>\n warning_messages?: Array<{ code: string; message: string }>\n}\n","/**\n * Last-resort floor when the lookup fails. A fallback must never become the\n * cached steady state — see the note on `VSCODE_VERSION_FALLBACK` in\n * `./get-vscode-version`.\n */\nexport const COPILOT_CHAT_VERSION_FALLBACK = \"0.43.2026033101\"\nconst FALLBACK = COPILOT_CHAT_VERSION_FALLBACK\n\ninterface MarketplaceResult {\n results: Array<{\n extensions: Array<{\n versions: Array<{ version: string }>\n }>\n }>\n}\n\n/**\n * The lookup, reporting failure as `undefined` rather than as the fallback.\n * See `getVSCodeVersionOrUndefined` for why the distinction matters to the\n * version cache.\n */\nexport async function getCopilotChatVersionOrUndefined(): Promise<\n string | undefined\n> {\n const controller = new AbortController()\n const timeout = setTimeout(() => {\n controller.abort()\n }, 5000)\n\n try {\n const response = await fetch(\n \"https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json;api-version=7.1-preview.1\",\n },\n body: JSON.stringify({\n filters: [\n {\n criteria: [{ filterType: 7, value: \"GitHub.copilot-chat\" }],\n },\n ],\n flags: 914,\n }),\n signal: controller.signal,\n },\n )\n\n if (!response.ok) return undefined\n\n const data = (await response.json()) as MarketplaceResult\n const version =\n data?.results?.[0]?.extensions?.[0]?.versions?.[0]?.version\n\n return version\n } catch {\n return undefined\n } finally {\n clearTimeout(timeout)\n }\n}\n\n/** Fallback-substituting wrapper, for callers that just want a version. */\nexport async function getCopilotChatVersion(): Promise<string> {\n return (await getCopilotChatVersionOrUndefined()) ?? FALLBACK\n}\n","/**\n * Last-resort floor when the lookup fails. A fallback must never become the\n * cached steady state, or a transient outage would freeze the version we\n * impersonate. See `src/lib/editor-version-cache.ts`.\n */\nexport const VSCODE_VERSION_FALLBACK = \"1.104.3\"\nconst FALLBACK = VSCODE_VERSION_FALLBACK\n\n/**\n * The lookup, reporting failure as `undefined` rather than as the fallback.\n *\n * The distinction is load-bearing for the cache: it must not infer \"the fetch\n * failed\" from `value === FALLBACK`, because the live version legitimately\n * equals the constant right after someone bumps `VSCODE_VERSION_FALLBACK` to\n * the then-current release — and that reading would refuse to cache a perfectly\n * good result, making every launch re-pay the ~1.5s network call forever.\n */\nexport async function getVSCodeVersionOrUndefined(): Promise<string | undefined> {\n const controller = new AbortController()\n const timeout = setTimeout(() => {\n controller.abort()\n }, 5000)\n\n try {\n const response = await fetch(\n \"https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin\",\n {\n signal: controller.signal,\n },\n )\n\n const pkgbuild = await response.text()\n const pkgverRegex = /pkgver=([0-9.]+)/\n const match = pkgbuild.match(pkgverRegex)\n\n return match ? match[1] : undefined\n } catch {\n return undefined\n } finally {\n clearTimeout(timeout)\n }\n}\n\n/** Fallback-substituting wrapper, for callers that just want a version. */\nexport async function getVSCodeVersion(): Promise<string> {\n return (await getVSCodeVersionOrUndefined()) ?? FALLBACK\n}\n\n","/**\n * Disk-backed TTL cache for the EDITOR IDENTITY versions the proxy sends\n * upstream: the VS Code version (`editor-version`) and the Copilot Chat\n * extension version (`editor-plugin-version`), see `src/lib/api-config.ts`.\n *\n * Why this exists: impersonating VS Code is non-negotiable — Copilot is\n * addressed as if by the real editor, so those headers must carry real,\n * current versions. But the two lookups are NETWORK calls to third parties\n * (the Arch AUR PKGBUILD and the VS Marketplace), and they sat on the\n * blocking launch path of EVERY invocation. The AUR fetch alone measured\n * ~1.5-1.8s, which every `github-router` process paid — including the\n * per-turn Claude Code hooks (`internal-prompt-submit`, `internal-stop-hook`,\n * …), each of which is a fresh `node dist/main.js` spawn.\n *\n * The fix is a cache, NOT a removal. Both fetches are still made; they are\n * just made at most once per `TTL_HOURS` instead of once (twice, before the\n * duplicate top-level await in `get-vscode-version.ts` was dropped) per\n * launch. A 12h TTL keeps the impersonated versions well inside the real\n * editor's release cadence.\n *\n * Two invariants that keep impersonation honest:\n *\n * 1. A FALLBACK is NEVER cached. `getVSCodeVersion()` /\n * `getCopilotChatVersion()` return a hardcoded constant when the network\n * fails, and that constant must stay a last-resort floor for the current\n * process — never the persisted steady state. Persisting it would let a\n * transient outage silently freeze the advertised version for 12h, and\n * (worse) survive restarts. `writeEntry` is therefore only reached on a\n * value that differs from the fallback.\n * 2. The timestamp is written only on SUCCESS. An offline machine retries\n * on the next launch rather than pinning a stale entry, which is the\n * opposite of `claude-version-check.ts`'s throttle (that one stamps\n * after the probes regardless, so an offline box pays both timeouts\n * hourly). Do not copy that detail here.\n *\n * The file lives beside `last-update-check` / `last-self-update-check` and is\n * deliberately named distinctly so the three throttles never cross-suppress.\n */\n\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport { PATHS } from \"./paths\"\n\nconst TTL_HOURS = 12\n\n/** One cached lookup: the resolved value plus when it was resolved. */\ninterface CacheEntry {\n value: string\n checkedAt: string\n}\n\ninterface EditorVersionCache {\n vscode?: CacheEntry\n copilotChat?: CacheEntry\n}\n\n/** Which upstream identity a cached entry belongs to. */\nexport type EditorVersionKey = \"vscode\" | \"copilotChat\"\n\nfunction cacheFilePath(): string {\n return path.join(PATHS.APP_DIR, \"last-editor-versions\")\n}\n\n/**\n * True when running under a test runner.\n *\n * Detected from the entry file, not an env var: `bun test` sets no marker of\n * its own and this repo builds with `NODE_ENV=production`, so `argv[1]` (the\n * spec file being executed) is the reliable signal. `BUN_TEST` / `NODE_ENV`\n * are still honored for other runners.\n *\n * The cache's own tests sandbox `homedir()` and opt back in via\n * `__allowEditorVersionWritesForTests`, so they still cover the write path.\n */\nfunction isTestRunner(): boolean {\n if (_allowWriteInTests) return false\n if (process.env.BUN_TEST === \"1\" || process.env.NODE_ENV === \"test\") {\n return true\n }\n const entry = process.argv[1] ?? \"\"\n return /\\.(test|spec)\\.[cm]?[jt]sx?$/.test(entry)\n}\n\n/** Set by the cache's own (sandboxed) tests so they can cover the write path. */\nlet _allowWriteInTests = false\n\n/**\n * Test-only: re-enable persistence. ONLY safe from a test that has redirected\n * `homedir()` to a temp dir — otherwise it writes the caller's fixture into the\n * developer's real cache and the proxy advertises it upstream.\n */\nexport function __allowEditorVersionWritesForTests(on: boolean): void {\n _allowWriteInTests = on\n}\n\nasync function readCache(): Promise<EditorVersionCache> {\n try {\n const parsed = JSON.parse(\n await fs.readFile(cacheFilePath(), \"utf8\"),\n ) as EditorVersionCache\n // A hand-edited or truncated file must degrade to \"no cache\", not throw.\n if (!parsed || typeof parsed !== \"object\") return {}\n return parsed\n } catch {\n return {}\n }\n}\n\nfunction isFresh(entry: CacheEntry | undefined): entry is CacheEntry {\n if (!entry || typeof entry.value !== \"string\" || entry.value.length === 0) {\n return false\n }\n const last = new Date(entry.checkedAt).getTime()\n if (Number.isNaN(last)) return false\n // A clock moved backwards would make `age` negative; treating that as stale\n // costs one refetch, whereas treating it as fresh could pin a bad value.\n const ageHours = (Date.now() - last) / 1000 / 3600\n return ageHours >= 0 && ageHours < TTL_HOURS\n}\n\n/**\n * Serializes the read-modify-write below.\n *\n * `setupAndServe` resolves both keys concurrently (`Promise.all`). Without\n * this, both callers would `readCache()` into their OWN object, spend ~1.5s in\n * their network fetch, and then write that stale object back — so whichever\n * finished last would clobber the other's entry, and one of the two versions\n * would re-fetch on every launch forever.\n *\n * The chain also re-reads immediately before mutating, so the write is against\n * current disk state rather than the snapshot taken before the fetch.\n */\nlet writeChain: Promise<void> = Promise.resolve()\n\nfunction serializeWrite(fn: () => Promise<void>): Promise<void> {\n const next = writeChain.then(fn, fn)\n // Never let a rejection poison the chain for later writers.\n writeChain = next.catch(() => {})\n return next\n}\n\n/**\n * Resolve `key` through the cache, calling `fetchFresh` only on a miss.\n *\n * `fetchFresh` signals failure by returning `undefined` — NOT by returning the\n * fallback constant. Inferring failure from `value === fallback` would break\n * exactly when the live version happens to equal the hardcoded constant, which\n * is the normal state right after someone bumps that constant to the\n * then-current version: the entry would never be written and every launch\n * would pay the ~1.5s fetch forever, silently defeating the cache.\n *\n * `fallback` is returned when the lookup fails and no usable cached value\n * exists. It is never persisted (invariant 1 above), so a transient outage\n * cannot freeze the version we impersonate.\n */\nexport async function resolveEditorVersion(\n key: EditorVersionKey,\n fetchFresh: () => Promise<string | undefined>,\n fallback: string,\n): Promise<string> {\n const entry = (await readCache())[key]\n const cachedValue = isFresh(entry) ? entry.value : undefined\n if (cachedValue !== undefined) return cachedValue\n\n const fetched = await fetchFresh()\n\n if (fetched !== undefined && fetched.length > 0) {\n // Never persist from a test process. The cached value is sent upstream as\n // the `editor-version` header, so a test that stubs the fetcher would\n // otherwise write its fixture into the DEVELOPER'S real cache and the\n // proxy would advertise it to Copilot for the full 12h TTL. That is not\n // hypothetical — a smoke test caught `editor-version: vscode/1.2.3` live\n // on this machine, sourced from a unit-test stub.\n //\n // Belt-and-braces with sandboxing the cache's own tests: any FUTURE test\n // that reaches this function through some other caller is covered too,\n // without every such test having to remember to mock homedir(). Reads stay\n // enabled, so warm-cache behavior is still exercisable.\n if (isTestRunner()) return fetched\n await serializeWrite(async () => {\n // Re-read INSIDE the chain: another key's write may have landed while\n // this one was in its fetch, and that entry must survive.\n const cache = await readCache()\n cache[key] = { value: fetched, checkedAt: new Date().toISOString() }\n try {\n await fs.mkdir(path.dirname(cacheFilePath()), { recursive: true })\n await fs.writeFile(cacheFilePath(), JSON.stringify(cache), {\n mode: 0o600,\n })\n } catch (err) {\n // A write failure only costs a refetch next launch.\n consola.debug(\"Failed to write editor-version cache:\", err)\n }\n })\n return fetched\n }\n\n // The lookup failed. A stale-but-real cached value beats the hardcoded\n // constant: a version that was genuinely current when it was cached is a\n // closer impersonation than one frozen at release time. Left un-restamped,\n // so the next launch still retries.\n if (typeof entry?.value === \"string\" && entry.value.length > 0) {\n return entry.value\n }\n return fallback\n}\n\n/** Test seam: the on-disk path, so tests can point HOME at a temp dir. */\nexport function editorVersionCachePath(): string {\n return cacheFilePath()\n}\n","import consola from \"consola\"\n\nimport { getModels } from \"~/services/copilot/get-models\"\nimport {\n COPILOT_CHAT_VERSION_FALLBACK,\n getCopilotChatVersionOrUndefined,\n} from \"~/services/get-copilot-version\"\nimport {\n getVSCodeVersionOrUndefined,\n VSCODE_VERSION_FALLBACK,\n} from \"~/services/get-vscode-version\"\n\nimport { resolveEditorVersion } from \"./editor-version-cache\"\nimport { state } from \"./state\"\n\nexport const sleep = (ms: number) =>\n new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n\nexport const isNullish = (value: unknown): value is null | undefined =>\n value === null || value === undefined\n\n/**\n * Beta prefixes VS Code Copilot Chat v0.43 actually sends.\n * Default mode — makes proxy traffic indistinguishable from VS Code.\n */\nconst VSCODE_BETA_PREFIXES = [\n \"interleaved-thinking-\",\n \"context-management-\",\n \"advanced-tool-use-\",\n]\n\n/**\n * Extended beta prefixes for Claude CLI compatibility.\n * Enabled via --extended-betas flag. Includes all betas confirmed\n * to work with the Copilot API.\n *\n * Notably absent (Copilot 400s on these — verified live):\n * context-1m-, skills-, files-api-, code-execution-, output-128k-,\n * advisor-tool- (see EXPLICITLY_STRIPPED_BETA_PREFIXES below).\n * 1M context is unlocked by selecting `claude-opus-4.7-1m-internal`\n * as the model id, not via a beta header.\n *\n * Empirical verification (2026-05-11 against api.enterprise.githubcopilot.com):\n * task-budgets-2026-03-13 → 200 ACCEPTED (cost-ceiling leverage)\n * token-efficient-tools-2026-03-28 → 200 ACCEPTED (per-tool token saving)\n * summarize-connector-text-2026-03-13 → 200 (Anthropic-internal feature flag,\n * won't fire for non-ant users; allowlisted defensively for ant edge case)\n * afk-mode-2026-01-31 → 200 (Anthropic-internal feature flag)\n * cli-internal-2026-02-09 → 200 (USER_TYPE=ant only)\n * oauth-2025-04-20 → 200 (Files-API path; Files-API itself\n * is not supportable via Copilot, but the header alone is harmless)\n * prompt-caching-scope-2026-01-05 → 200 even with body cache_control.scope\n * stripped (already covered by `prompt-caching-` prefix above)\n */\nconst EXTENDED_BETA_PREFIXES = [\n ...VSCODE_BETA_PREFIXES,\n \"claude-code-\",\n \"effort-\",\n \"prompt-caching-\",\n \"computer-use-\",\n \"pdfs-\",\n \"max-tokens-\",\n \"token-counting-\",\n \"compact-\",\n \"structured-outputs-\",\n \"fast-mode-\",\n \"mcp-client-\",\n \"mcp-servers-\",\n \"redact-thinking-\",\n \"web-search-\",\n // Empirically accepted by Copilot, sent by Claude Code v2.1.138+\n \"task-budgets-\",\n \"token-efficient-tools-\",\n // Anthropic-internal feature flags (won't reach proxy from non-ant users\n // due to Bun build-time dead-code elimination, but allowlisted so the rare\n // ant-user / managed-deployment case flows cleanly).\n \"summarize-connector-text-\",\n \"afk-mode-\",\n \"cli-internal-\",\n \"oauth-\",\n]\n\n/**\n * Beta prefixes the proxy explicitly STRIPS even from the extended\n * allowlist (and even if a future leverage mode broadens the allowlist\n * further). Defensive layer: today's allowlist-only filter would already\n * drop these because they're not in any allowlist, but keeping an\n * explicit deny-list catches future changes that broaden allow rules\n * (e.g. a hypothetical pattern-based mode that lets `claude-*` through).\n *\n * Empirical (2026-05-11): Copilot returns HTTP 400\n * `unsupported beta header(s): advisor-tool-2026-03-01`\n * on every request that includes `advisor-tool-`. Stripping it is the\n * difference between a working request (no ADVISOR semantics) and a\n * fully-failed request. Document upstream limitation in CLAUDE.md.\n */\nconst EXPLICITLY_STRIPPED_BETA_PREFIXES = [\n \"advisor-tool-\",\n] as const\n\n/**\n * Filter an `anthropic-beta` header value, keeping only beta flags\n * in the active whitelist AND not in the explicit-strip list.\n * Uses extended prefixes when --extended-betas is enabled, VS Code-only\n * prefixes otherwise. Returns the filtered comma-separated string,\n * or undefined if nothing remains.\n */\nexport function filterBetaHeader(value: string): string | undefined {\n const prefixes = state.extendedBetas\n ? EXTENDED_BETA_PREFIXES\n : VSCODE_BETA_PREFIXES\n const filtered = value\n .split(\",\")\n .map((v) => v.trim())\n .filter(\n (v) =>\n v\n && prefixes.some((prefix) => v.startsWith(prefix))\n && !EXPLICITLY_STRIPPED_BETA_PREFIXES.some((p) => v.startsWith(p)),\n )\n .join(\",\")\n return filtered || undefined\n}\n\n/**\n * Normalize a model ID for fuzzy comparison: lowercase, replace dots with\n * dashes, insert dash at letter→digit boundaries, and collapse repeated\n * dashes. E.g. \"gpt5.3-codex\" → \"gpt-5-3-codex\", \"GPT-5.3-Codex\" → \"gpt-5-3-codex\".\n */\nexport function normalizeModelId(id: string): string {\n return id\n .toLowerCase()\n .replace(/\\./g, \"-\")\n .replace(/([a-z])(\\d)/g, \"$1-$2\")\n .replace(/-{2,}/g, \"-\")\n}\n\n/**\n * Resolve a model name to the best available variant in the Copilot model list.\n *\n * Resolution cascade:\n * 0. `[1m]` literal-bracket suffix: strip, delegate, warn if downgraded.\n * Bracketed slug must never reach Copilot (400s on it). See cc-backup\n * `src/utils/context.ts:35-40` for Claude Code's 1M unlock mechanism.\n * 1. Exact match\n * 2. Case-insensitive match\n * 3. Family preference (opus→1m, codex→highest version)\n * 4. Normalized match (dots→dashes, letter-digit boundaries)\n * 5. Anthropic dated-slug retry: if the input matches `claude-...-YYYYMMDD`,\n * strip the date and re-run the cascade once. Family-guarded so non-claude\n * 8-digit suffixes can't be mis-stripped; runs after Steps 1-4 so explicit\n * version pinning (a dated catalog id matched at Step 1) always wins.\n * 6. Return as-is with a warning\n */\n/**\n * `[1m]`-downgrade warnings we've already emitted this process. `resolveModel`\n * is called many times per launch (env setup, model validation, the banner,\n * and — under `serve` — on every model-picker `/models` poll), so the downgrade\n * notice is deduped to once per model id to avoid log spam.\n */\nconst warnedOneMDowngrade = new Set<string>()\n\n/** Test-only: clear the once-per-process `[1m]`-downgrade warning cache. */\nexport function __resetOneMDowngradeWarnings(): void {\n warnedOneMDowngrade.clear()\n}\n\nexport function resolveModel(modelId: string): string {\n const models = state.models?.data\n if (!models) return modelId\n\n // [1m] literal-bracket suffix: Claude Code's request for 1M context\n // accounting. cc-backup `src/utils/context.ts:35-40` has1mContext\n // matches `/\\[1m\\]/i`; getContextWindowForModel returns 1_000_000\n // when true. parseUserSpecifiedModel (model.ts:445-506) reattaches\n // the bracket after alias resolution, so Claude Code SENDS the\n // bracketed slug verbatim on the wire (`model: \"claude-opus-4-7[1m]\"`).\n // Copilot doesn't recognize the bracket → 400.\n //\n // Strip for the catalog lookup and delegate. If the stripped\n // resolution lands on a `-1m` variant (enterprise opus path via\n // family preference), perfect — the upstream call routes to the 1M\n // backend and Claude Code's local accounting was right. Otherwise\n // (non-enterprise for opus, or any [1m] on sonnet/haiku where\n // Copilot has no -1m backend), warn and return the 200K resolution\n // so the request still succeeds — at the cost of Claude Code\n // over-accounting context against the proxy (it will compact early\n // because it thinks the window is 1M).\n //\n // Bounded recursion: the stripped form no longer matches the regex,\n // so the inner resolveModel call cannot re-enter this branch.\n const oneMMatch = modelId.match(/^(.*)\\[1m\\]$/i)\n if (oneMMatch) {\n const stripped = oneMMatch[1]\n const resolved = resolveModel(stripped)\n // The resolved slug delivers 1M context either as a `-1m` variant OR\n // as a base slug that natively advertises a >=1M window (how opus-4.8\n // and opus-5 ship — a single slug, no `-1m` sibling). Only warn about a\n // real downgrade: a resolved model whose catalog window is under 1M.\n const resolvedIs1M =\n /-1m(?:$|-)/.test(resolved)\n || (models.find((m) => m.id === resolved)?.capabilities?.limits\n ?.max_context_window_tokens ?? 0) >= 1_000_000\n if (!resolvedIs1M && !warnedOneMDowngrade.has(modelId)) {\n warnedOneMDowngrade.add(modelId)\n consola.warn(\n `Model \"${modelId}\" requested 1M context but no 1M backend is in Copilot's catalog for this tier/family; downgrading upstream to \"${resolved}\" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`,\n )\n }\n return resolved\n }\n\n // 1. Exact match\n if (models.some((m) => m.id === modelId)) return modelId\n\n // 2. Case-insensitive match\n const lower = modelId.toLowerCase()\n const ciMatch = models.find((m) => m.id.toLowerCase() === lower)\n if (ciMatch) return ciMatch.id\n\n // 3. Family preference — before normalization so product aliases\n // (opus→1m, codex→latest) take priority over fuzzy matches\n if (lower.includes(\"opus\")) {\n // Match ...-1m or ...-1m-<anything> (e.g. claude-opus-4.7-1m-internal).\n // Prefer the 1M variant whose major.minor matches the requested version,\n // otherwise find() would silently downgrade claude-opus-4.7 to a\n // claude-opus-4.6-1m if the latter happens to come first in the list.\n // Accept both dotted (\"opus-4.7\") and dashed (\"opus-4-7\") inputs —\n // Claude Code historically sends the dashed form.\n const oneMs = models.filter(\n (m) => m.id.includes(\"opus\") && /-1m(?:$|-)/.test(m.id),\n )\n // Capture the requested version. The minor segment is OPTIONAL so a\n // single-segment slug like `claude-opus-5` registers as a specific\n // version (\"5\") rather than falling through to the `oneMs[0]` wildcard\n // below — which would silently downgrade a catalog-absent `claude-opus-5`\n // to the first unrelated opus `-1m` sibling (e.g. claude-opus-4.6-1m).\n const versionMatch = lower.match(/opus-(\\d+)(?:[.-](\\d+))?/)\n const requestedVersion = versionMatch\n ? versionMatch[2]\n ? `${versionMatch[1]}.${versionMatch[2]}`\n : versionMatch[1]\n : undefined\n const preferred = requestedVersion\n ? oneMs.find((m) => m.id.includes(`opus-${requestedVersion}-`))\n : undefined\n // When a specific version was requested (e.g. \"claude-opus-4-8\") but\n // no matching 1M variant exists in the catalog, do NOT fall back to a\n // different version's 1M variant — that silently downgrades (e.g.\n // 4.8 → 4.6-1m). Instead, fall through to step 4 (normalization)\n // which will resolve the bare dotted entry (claude-opus-4.8).\n const oneM = preferred ?? (requestedVersion ? undefined : oneMs[0])\n if (oneM) return oneM.id\n }\n\n if (lower.includes(\"codex\")) {\n const codexModels = models.filter(\n (m) => m.id.includes(\"codex\") && !m.id.includes(\"mini\"),\n )\n if (codexModels.length > 0) {\n codexModels.sort((a, b) => b.id.localeCompare(a.id))\n return codexModels[0].id\n }\n }\n\n // 4. Normalized match (dots → dashes, letter-digit boundaries)\n const normalized = normalizeModelId(modelId)\n const normMatch = models.find(\n (m) => normalizeModelId(m.id) === normalized,\n )\n if (normMatch) return normMatch.id\n\n // 5. Anthropic dated-slug retry. Claude Code's /model UI ships Anthropic's\n // published slugs (e.g. \"claude-haiku-4-5-20251001\") that carry a\n // -YYYYMMDD suffix Copilot's catalog doesn't use. Strip the date and\n // re-run the cascade once so the request maps to the floating tag\n // (claude-haiku-4.5). Family-guarded to `claude-` so a hypothetical\n // \"gpt-...-20260101\" can't be silently stripped. Bounded recursion:\n // the stripped id no longer matches the regex, so the retry's own\n // Step 5 is a no-op.\n const dateStripped = modelId.replace(/^(claude-[\\w.-]+)-20\\d{6}$/i, \"$1\")\n if (dateStripped !== modelId) {\n const retried = resolveModel(dateStripped)\n // resolveModel returns the input unchanged on miss; treat unchanged-and-\n // not-in-catalog as miss to avoid logging a misleading \"resolved\" hop.\n const retryHit =\n retried !== dateStripped || models.some((m) => m.id === dateStripped)\n if (retryHit) {\n consola.info(\n `Resolved Anthropic dated slug \"${modelId}\" → \"${retried}\" (stripped -YYYYMMDD; pass an explicit catalog id to pin a snapshot)`,\n )\n return retried\n }\n }\n\n // 6. Legacy family fallback. Claude Code's settings.json may pin slugs\n // whose Copilot equivalent does not exist (e.g. claude-3-7-sonnet-20250219\n // or claude-sonnet-4-0 — neither is in Copilot's enterprise catalog as\n // of 2026-05-11; a request for either returns HTTP 400 \"model not\n // supported\"). Step 5's dated-retry strips the date but the resulting\n // \"claude-3-7-sonnet\" still has no Copilot equivalent. Rather than\n // dead-end the request, fall back to the highest available family\n // member (sonnet → highest sonnet, haiku → highest haiku). Surfaces\n // via consola.info so the user sees the substitution. Opus is already\n // handled by the family preference in Step 3.\n //\n // Guards (codex-reviewer findings):\n // (a) Family fires only for `claude-` prefixed inputs — protects\n // against custom-sonnet-future or any non-Anthropic provider\n // coincidentally containing \"sonnet\"/\"haiku\" in its slug.\n // (b) Family token must be word-bounded (`-sonnet-` / `-sonnet$`)\n // so a hypothetical claude-supersonnet-* doesn't match.\n // (c) Sort uses numeric collation (`{numeric: true}`) so a future\n // claude-sonnet-4.10 sorts higher than claude-sonnet-4.6\n // (lexicographic alone would invert).\n if (lower.startsWith(\"claude-\")) {\n const matchSonnet = /(?:^|-)sonnet(?:-|$)/.test(lower)\n const matchHaiku = /(?:^|-)haiku(?:-|$)/.test(lower)\n if (matchSonnet || matchHaiku) {\n const family = matchSonnet ? \"sonnet\" : \"haiku\"\n const familyMembers = models.filter((m) =>\n new RegExp(`(?:^|-)${family}(?:-|$|\\\\.)`).test(m.id),\n )\n if (familyMembers.length > 0) {\n familyMembers.sort((a, b) =>\n b.id.localeCompare(a.id, undefined, { numeric: true }),\n )\n const best = familyMembers[0].id\n consola.info(\n `Model \"${modelId}\" not in Copilot catalog; falling back to highest available \"${best}\" (legacy ${family} slug). Pin a current catalog id to silence.`,\n )\n return best\n }\n }\n }\n\n // 7. No match — warn and return as-is\n consola.warn(\n `Model \"${modelId}\" not found in Copilot model list. Available: ${models.map((m) => m.id).join(\", \")}`,\n )\n return modelId\n}\n\n/**\n * Resolve a codex model ID, falling back to the best available codex model.\n * Used by the codex subcommand for model selection.\n */\nexport function resolveCodexModel(modelId: string): string {\n const resolved = resolveModel(modelId)\n const models = state.models?.data\n if (!models) return resolved\n\n // Check if the resolved model exists in the model list\n if (models.some((m) => m.id === resolved)) return resolved\n\n // Fall back to the best available codex-class model. The /responses\n // endpoint is the discriminator — gpt-5.5 dropped the -codex suffix but\n // still routes through /responses. Prefer explicit -codex ids when both\n // exist, otherwise pick the highest version-like id.\n const candidates = models.filter((m) => {\n const endpoints = m.supported_endpoints ?? []\n if (m.id.includes(\"mini\") || m.id.includes(\"nano\")) return false\n return endpoints.length === 0 || endpoints.includes(\"/responses\")\n })\n\n if (candidates.length > 0) {\n candidates.sort((a, b) => {\n const aCodex = a.id.includes(\"codex\") ? 1 : 0\n const bCodex = b.id.includes(\"codex\") ? 1 : 0\n if (aCodex !== bCodex) return bCodex - aCodex\n return b.id.localeCompare(a.id)\n })\n const best = candidates[0].id\n consola.warn(`Model \"${modelId}\" not available, using \"${best}\" instead`)\n return best\n }\n\n return resolved\n}\n\nexport async function cacheModels(): Promise<void> {\n const models = await getModels()\n state.models = models\n}\n\nexport const cacheVSCodeVersion = async () => {\n const response = await resolveEditorVersion(\n \"vscode\",\n getVSCodeVersionOrUndefined,\n VSCODE_VERSION_FALLBACK,\n )\n state.vsCodeVersion = response\n\n consola.info(`Using VSCode version: ${response}`)\n}\n\nexport const cacheCopilotVersion = async () => {\n const version = await resolveEditorVersion(\n \"copilotChat\",\n getCopilotChatVersionOrUndefined,\n COPILOT_CHAT_VERSION_FALLBACK,\n )\n state.copilotVersion = version\n\n consola.info(`Using Copilot Chat version: ${version}`)\n}\n","import consola from \"consola\"\n\nimport {\n GITHUB_BASE_URL,\n GITHUB_CLIENT_ID,\n standardHeaders,\n} from \"~/lib/api-config\"\nimport { sleep } from \"~/lib/utils\"\n\nimport type { DeviceCodeResponse } from \"./get-device-code\"\n\n/**\n * Per-attempt cap for a single device-code poll.\n *\n * Deliberately NOT the shared transient-retry layer: a non-OK response here is\n * the EXPECTED steady state (`authorization_pending` until the user finishes in\n * their browser), so retrying it as a transient fault would just hammer GitHub\n * inside a loop that is already a retry loop with its own interval.\n *\n * What was genuinely missing is a bound on a single attempt. Without a signal a\n * hung socket blocks the loop forever: the `expiresAt` check only runs between\n * attempts, so it can never fire while one attempt is stuck. This caps each\n * attempt so the loop keeps ticking and the expiry bound stays real.\n */\nconst POLL_ATTEMPT_TIMEOUT_MS = 30_000\n\nexport async function pollAccessToken(\n deviceCode: DeviceCodeResponse,\n clientId: string = GITHUB_CLIENT_ID,\n): Promise<string> {\n // Interval is in seconds, we need to multiply by 1000 to get milliseconds\n // I'm also adding another second, just to be safe\n const sleepDuration = (deviceCode.interval + 1) * 1000\n consola.debug(`Polling access token with interval of ${sleepDuration}ms`)\n const expiresAt = Date.now() + deviceCode.expires_in * 1000\n // Remembered so a permanent misconfiguration (bad DNS, TLS interception, a\n // blocking proxy) is reported as itself rather than surfacing as a bare\n // \"device code expired\" after the full window has silently burned.\n let lastFetchError: unknown\n\n while (Date.now() < expiresAt) {\n let response: Response\n try {\n response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {\n method: \"POST\",\n headers: standardHeaders(),\n body: JSON.stringify({\n client_id: clientId,\n device_code: deviceCode.device_code,\n grant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n }),\n signal: AbortSignal.timeout(POLL_ATTEMPT_TIMEOUT_MS),\n })\n } catch (err) {\n // A timed-out or failed attempt is not fatal — the device code may still\n // be authorized before it expires, so keep polling until it does.\n lastFetchError = err\n consola.debug(\"Access-token poll attempt failed; retrying:\", err)\n if (Date.now() >= expiresAt) break\n await sleep(sleepDuration)\n continue\n }\n\n // Reaching here means the transport worked, whatever the HTTP status. Clear\n // any earlier transport failure so a transient blip mid-flow cannot make\n // the final message blame the network for a genuine expiry.\n lastFetchError = undefined\n\n if (!response.ok) {\n consola.error(\"Failed to poll access token:\", await response.text())\n if (Date.now() >= expiresAt) break\n await sleep(sleepDuration)\n continue\n }\n\n const json = await response.json()\n consola.debug(\"Polling access token response:\", json)\n\n const { access_token } = json as AccessTokenResponse\n\n if (access_token) {\n return access_token\n }\n\n if (Date.now() >= expiresAt) break\n await sleep(sleepDuration)\n }\n\n // If every attempt failed at the transport layer, the device code almost\n // certainly did NOT expire — the network did. Report that, with the real\n // error as `cause`, so a misconfigured proxy or TLS setup is diagnosable\n // instead of masquerading as a timing problem.\n if (lastFetchError !== undefined) {\n throw new Error(\n \"Could not reach GitHub to complete device-code authorization. Check network/proxy/TLS settings and run auth again.\",\n { cause: lastFetchError },\n )\n }\n throw new Error(\"Device code expired. Please run auth again.\")\n}\n\ninterface AccessTokenResponse {\n access_token: string\n token_type: string\n scope: string\n}\n","import consola from \"consola\"\nimport { randomBytes } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\n\nimport { PATHS } from \"~/lib/paths\"\nimport {\n CopilotTokenExchangeError,\n credentialFingerprint,\n getCopilotToken,\n} from \"~/services/github/get-copilot-token\"\nimport { getDeviceCode } from \"~/services/github/get-device-code\"\nimport { getGitHubUser } from \"~/services/github/get-user\"\nimport { pollAccessToken } from \"~/services/github/poll-access-token\"\n\nimport {\n GITHUB_AGENT_CLIENT_ID,\n GITHUB_AGENT_SCOPES,\n GITHUB_API_BASE_URL,\n githubAgentHeaders,\n} from \"./api-config\"\nimport { HTTPError } from \"./error\"\nimport { state } from \"./state\"\n\nconst readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, \"utf8\")\n\n/**\n * Transient filesystem errors from a read that raced the temp+rename write.\n *\n * On Windows a read landing in that window fails with one of these even\n * though a perfectly good file exists microseconds later. Treating one as\n * fatal at startup would cost a launch for no reason.\n */\nconst TRANSIENT_READ_CODES = new Set([\"EPERM\", \"EBUSY\", \"EACCES\", \"ENOENT\"])\n\nfunction isTransientReadError(err: unknown): boolean {\n const code = (err as NodeJS.ErrnoException | undefined)?.code\n return typeof code === \"string\" && TRANSIENT_READ_CODES.has(code)\n}\n\n/**\n * Read the credential file, retrying briefly through a racing rename.\n *\n * Same delays and the same reasoning as {@link RENAME_RETRY_DELAYS_MS} on the\n * write side: the contending handle closes almost immediately, so a short\n * bounded retry converts a spurious launch failure into a non-event. A\n * persistent failure still propagates — the caller must not believe a\n * credential was read when it was not.\n */\nasync function readGithubTokenWithRetry(): Promise<string> {\n let lastErr: unknown\n for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt++) {\n try {\n return await readGithubToken()\n } catch (err) {\n lastErr = err\n if (!isTransientReadError(err)) throw err\n if (attempt < RENAME_RETRY_DELAYS_MS.length) {\n await new Promise<void>((resolve) =>\n setTimeout(resolve, RENAME_RETRY_DELAYS_MS[attempt]),\n )\n }\n }\n }\n throw lastErr\n}\n\n/**\n * Backoff for a transient Windows rename failure. On Windows `fs.rename` over\n * an existing destination transiently fails with EPERM / EBUSY / EACCES when\n * anything else holds the target open for a moment (antivirus, the search\n * indexer, a backup agent). Nothing is wrong and the handle closes microseconds\n * later — but without a retry, a scan landing on the wrong millisecond throws\n * out of a credential write and costs the user a re-auth. Same delays as\n * `renameWithRetry` in `~/lib/claude-md-injection`, which documents the\n * underlying behavior at length.\n */\nconst RENAME_RETRY_DELAYS_MS = [50, 200, 500] as const\n\n/**\n * Replace a credential file atomically: temp + rename, with bounded retry.\n *\n * A plain `fs.writeFile` truncates the destination before it writes, so a\n * crash, a full disk, or a kill mid-write leaves a truncated token on disk and\n * forces the user through a re-auth. `rename` within the same directory is\n * atomic on both NTFS and POSIX, so a reader sees either the old token or the\n * new one, never a partial.\n *\n * The temp name uses `randomBytes`, not `Math.random()`: this names a file that\n * briefly holds a credential, and a predictable name in a shared directory\n * invites a pre-creation race. Combined with `flag: \"wx\"` (fail if it exists)\n * the write refuses to follow anything an attacker pre-placed, rather than\n * silently writing the token through a planted symlink.\n *\n * Permissions: the temp file is created 0o600, and `rename` carries that mode\n * with it on POSIX — so the destination's mode is preserved, not weakened.\n * (On Windows `chmod` is a no-op anyway; see `chmodIfPossible` in `~/lib/paths`.\n * Both token files live under `CLAUDE_RUNTIME_DIR`, which is chmod'd 0o700, so\n * directory ACLs are what actually protect them there.)\n */\nasync function writeTokenFileAtomic(\n filePath: string,\n token: string,\n): Promise<void> {\n const tmp = `${filePath}.${process.pid}.${randomBytes(8).toString(\"hex\")}.tmp`\n try {\n await fs.writeFile(tmp, token, { mode: 0o600, flag: \"wx\" })\n\n let lastErr: unknown\n let renamed = false\n for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt++) {\n try {\n await fs.rename(tmp, filePath)\n renamed = true\n break\n } catch (err) {\n lastErr = err\n // Don't sleep after the final attempt.\n if (attempt < RENAME_RETRY_DELAYS_MS.length) {\n await new Promise<void>((resolve) =>\n setTimeout(resolve, RENAME_RETRY_DELAYS_MS[attempt]),\n )\n }\n }\n }\n // A persistent failure (permissions, full disk) still throws: the caller\n // must not believe a credential was stored when it was not.\n if (!renamed) throw lastErr\n } catch (err) {\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n}\n\nconst writeGithubToken = (token: string) =>\n writeTokenFileAtomic(PATHS.GITHUB_TOKEN_PATH, token)\n\nconst readGithubAgentToken = () =>\n fs.readFile(PATHS.GITHUB_AGENT_TOKEN_PATH, \"utf8\")\n\nconst writeGithubAgentToken = (token: string) =>\n writeTokenFileAtomic(PATHS.GITHUB_AGENT_TOKEN_PATH, token)\n\n/**\n * Stops the background Copilot-token refresh started by\n * {@link setupCopilotToken}. Idempotent.\n */\nexport type StopCopilotTokenRefresh = () => void\n\n/**\n * Safety margin subtracted from the derived refresh deadline, so a request\n * arriving just before expiry refreshes rather than racing it.\n */\nconst REFRESH_SKEW_MS = 120_000\n\n/**\n * Bounds on a `refresh_in` we are willing to turn into a timer delay. An\n * absurd or missing upstream value must not produce a zero-length timer (a\n * hot loop hammering GitHub) or an effectively infinite one (a token that is\n * never refreshed). 60s floor, 6h ceiling, 1500s default — the value GitHub\n * actually returns today.\n */\nconst MIN_REFRESH_IN_S = 60\nconst MAX_REFRESH_IN_S = 6 * 60 * 60\nconst DEFAULT_REFRESH_IN_S = 1500\n\n/** Backoff schedule after a failed refresh, then hold at the last value. */\nconst REFRESH_BACKOFF_MS = [5_000, 15_000, 60_000, 300_000] as const\n\nfunction clampRefreshIn(refreshIn: unknown): number {\n if (typeof refreshIn !== \"number\" || !Number.isFinite(refreshIn)) {\n return DEFAULT_REFRESH_IN_S\n }\n return Math.min(Math.max(refreshIn, MIN_REFRESH_IN_S), MAX_REFRESH_IN_S)\n}\n\n/**\n * Adopt an exchange result into `state`.\n *\n * The generation bump is what lets an in-flight request know a newer token\n * exists (see `State.copilotTokenGeneration`), so it happens on EVERY success\n * — including one that returns a byte-identical token, which upstream\n * demonstrably does.\n */\n/**\n * Floor on how far ahead the derived refresh deadline may be placed.\n *\n * The deadline is `now + refresh_in - skew`, and the skew (120s) is larger\n * than the `refresh_in` floor (60s) — so a short upstream `refresh_in` would\n * put the deadline in the PAST, the scheduler would floor its delay to 1s,\n * and the proxy would hammer the token endpoint at 1 Hz forever. The deadline\n * must always be in the future, whatever upstream says.\n */\nconst MIN_REFRESH_LEAD_MS = 30_000\n\nfunction commitCopilotToken(token: string, refreshIn: unknown): void {\n state.copilotToken = token\n state.copilotTokenGeneration += 1\n // Derived from the DURATION, never the absolute `expires_at`: a clock\n // running ahead of GitHub's would make an absolute deadline look\n // already-past on a brand-new token and drive a refresh storm.\n const lead = Math.max(\n clampRefreshIn(refreshIn) * 1000 - REFRESH_SKEW_MS,\n MIN_REFRESH_LEAD_MS,\n )\n state.copilotTokenRefreshAt = Date.now() + lead\n}\n\n/**\n * Fetch the Copilot token and keep it fresh in the background.\n *\n * Returns a disposer that stops the refresh loop. **Long-lived callers**\n * (`start`/`claude`/`codex`, via `setupAndServe`) can ignore it — the timer\n * is `unref()`d, so it never holds the event loop open on its own. The one-shot\n * `models` command calls it, so ownership of the timer is explicit rather than\n * implied by a runtime flag. (`check-usage` does not call this function at all;\n * it only needs `setupGitHubToken`.)\n *\n * Both halves are load-bearing, for different failure modes. Without the\n * `unref()`, `github-router models` printed its full correct output and then\n * hung forever: its success path just returns (only the failure branches call\n * `process.exit`), and the un-unref'd timer pinned the event loop. Without\n * the disposer, that fix would depend on a property no caller can see, so the\n * next one-shot command would inherit the same trap.\n *\n * The schedule is a SELF-RE-ARMING `setTimeout`, not a fixed `setInterval`.\n * Three reasons: the delay tracks each response's own `refresh_in` instead of\n * the one captured at startup; a failure can back off (5s → 15s → 60s → 300s)\n * instead of waiting a full period during which the token is already dead;\n * and one timer cannot disagree with itself the way a period timer plus a\n * separate retry timer can.\n */\nexport const setupCopilotToken = async (): Promise<StopCopilotTokenRefresh> => {\n const { token, refresh_in } = await getCopilotToken()\n commitCopilotToken(token, refresh_in)\n\n consola.debug(\n `GitHub Copilot Token fetched successfully! (credential ${credentialFingerprint(state.githubToken)})`,\n )\n if (state.showToken) {\n consola.info(\"Copilot token:\", token)\n }\n\n let handle: ReturnType<typeof setTimeout> | undefined\n let disposed = false\n let failures = 0\n\n const arm = (delayMs: number) => {\n // Checked here rather than only at the call sites because every caller\n // reaches this after an `await`, and disposal can land in that window.\n if (disposed) return\n handle = setTimeout(() => {\n void tick()\n }, delayMs)\n // A refresh timer is not a reason for the process to stay alive; it exists\n // to serve work that is already keeping it alive.\n handle.unref?.()\n }\n\n const tick = async () => {\n const outcome = await refreshCopilotToken(\"interval\")\n // The disposer may have run while the exchange was in flight. Re-arming\n // now would resurrect the loop after it was explicitly stopped — the\n // classic leaked-timer-after-dispose bug.\n if (disposed) return\n if (outcome === \"refreshed\") {\n failures = 0\n arm(nextDelayFromState())\n return\n }\n // Any non-success (including a skipped no-op) backs off rather than\n // spinning: the token is dead or dying and hammering will not help.\n const delay =\n REFRESH_BACKOFF_MS[Math.min(failures, REFRESH_BACKOFF_MS.length - 1)]\n failures += 1\n arm(delay)\n }\n\n arm(nextDelayFromState())\n\n return () => {\n if (disposed) return\n disposed = true\n if (handle) clearTimeout(handle)\n }\n}\n\n/**\n * Delay until the next scheduled refresh, from the deadline already stored on\n * `state`. Floored at 1s so a pathological deadline can never busy-loop.\n */\nfunction nextDelayFromState(): number {\n const deadline = state.copilotTokenRefreshAt\n if (deadline === undefined) return DEFAULT_REFRESH_IN_S * 1000\n return Math.max(deadline - Date.now(), 1000)\n}\n\n// Single-flight mutex around the refresh fetch. Concurrent triggers (interval\n// + a 401-retry path) share one in-flight refresh promise so we never\n// overlap network calls or race writes to state.copilotToken.\nlet inflightRefresh: Promise<RefreshOutcome> | undefined\n// Cooldowns are keyed off the OUTCOME of the last refresh, not the attempt:\n// - lastRefreshSuccess: throttles 401-retries when the token is fresh\n// (don't pointlessly re-fetch a token we just got).\n// - lastRefreshFailure: shorter backoff so a transient upstream blip\n// doesn't suppress legitimate refresh attempts for a full 30s, but\n// still prevents a thundering-herd refresh-storm against an upstream\n// that's persistently failing.\nlet lastRefreshSuccess = 0\nlet lastRefreshFailure = 0\nconst REFRESH_SUCCESS_COOLDOWN_MS = 30_000\nconst REFRESH_FAILURE_COOLDOWN_MS = 5_000\n\n/**\n * Clear the refresh cooldowns. TEST-ONLY.\n *\n * The cooldowns are module state that outlives any one test, so without a\n * reset a test's refresh silently suppresses the next test's. The alternative\n * — stubbing `Date.now` far into the future — leaks into every other test\n * sharing the process (it broke the cooldown assertions in `token.test.ts`),\n * so an explicit reset is both narrower and honest about what it touches.\n */\nexport function __resetRefreshCooldownsForTests(): void {\n lastRefreshSuccess = 0\n lastRefreshFailure = 0\n}\n\n/**\n * What a refresh attempt did. Used for LOGGING and classification only —\n * never to decide whether a request should retry. That decision belongs to\n * the generation counter (see {@link tryRefreshAndRetry}), because a \"skipped\"\n * outcome for one request can coincide with another request having just\n * installed a perfectly good token.\n */\nexport type RefreshOutcome =\n | \"refreshed\"\n | \"skipped\"\n | \"credential_rejected\"\n | \"entitlement_lapsed\"\n | \"transient_failure\"\n\n/**\n * Read the credential from disk, returning it only if it is usable.\n *\n * Every failure mode here resolves to `undefined` (meaning \"keep using what\n * is in memory\") rather than throwing or returning a partial value. The file\n * is replaced by temp+rename (`writeTokenFileAtomic`), and on Windows a read\n * racing that rename fails with EPERM/EBUSY/EACCES/ENOENT. None of those mean\n * the in-memory credential went bad, so none of them may clear it — and a\n * filesystem error must never escape into the refresh loop and kill the timer.\n */\nasync function readGithubTokenIfUsable(): Promise<string | undefined> {\n if (state.githubTokenSource === \"explicit\") return undefined\n try {\n const raw = await readGithubToken()\n const trimmed = raw.trim()\n // A zero-length read is what a torn/partial write looks like. Adopting it\n // would replace a working credential with nothing.\n return trimmed.length > 0 ? trimmed : undefined\n } catch (err) {\n consola.debug(\"Could not re-read GitHub credential from disk:\", err)\n return undefined\n }\n}\n\n/**\n * Perform one exchange, preferring a newer credential from disk if there is one.\n *\n * The disk token is a CANDIDATE: it is passed to the exchange directly and\n * published to `state.githubToken` only after that exchange succeeds. It is\n * never installed globally on spec — `state` is shared by every concurrent\n * request, so an unvalidated credential visible there would be sent by\n * unrelated in-flight requests and 401 them while a working credential was\n * still in hand.\n */\nasync function exchangeWithFreshestCredential(): Promise<{\n token: string\n refresh_in: number\n}> {\n const onDisk = await readGithubTokenIfUsable()\n const inMemory = state.githubToken\n\n if (onDisk && onDisk !== inMemory) {\n consola.info(\n `GitHub credential on disk differs from the one in memory `\n + `(${credentialFingerprint(inMemory)} → ${credentialFingerprint(onDisk)}); trying it.`,\n )\n // Try the candidate WITHOUT publishing it. On success it becomes the\n // credential of record; on failure nothing was ever exposed to roll back.\n const result = await getCopilotToken(onDisk)\n state.githubToken = onDisk\n state.githubTokenSource = \"file\"\n return result\n }\n\n return getCopilotToken()\n}\n\nexport async function refreshCopilotToken(\n reason: \"interval\" | \"401-retry\" | \"expiry\",\n): Promise<RefreshOutcome> {\n // Single-flight: this check and the assignment below MUST NOT be separated\n // by an await. Anything awaited in between yields the event loop, letting a\n // second caller pass this guard before the first has published its promise\n // — which would launch overlapping exchanges against GitHub, exactly the\n // storm the mutex exists to prevent. The cooldown check needs to read the\n // credential from disk, so it lives INSIDE the body below rather than here.\n if (inflightRefresh) return inflightRefresh\n\n const run = async (): Promise<RefreshOutcome> => {\n // The `finally` that clears `inflightRefresh` must cover EVERY exit from\n // this body, including the early cooldown returns below. If a `return`\n // escaped it, the stale promise would stay published and every later\n // refresh would short-circuit to it forever — a permanently wedged\n // refresh loop, which is worse than the bug being fixed.\n try {\n // Refresh-storm protection: if a recent refresh already completed,\n // decline new reactive attempts. Interval refreshes always proceed\n // (they're spaced by the derived deadline, well outside the window).\n // Reactive attempts respect both cooldowns:\n // - skip if a refresh succeeded within the last 30s (token is fresh)\n // - skip if a refresh failed within the last 5s (back off briefly)\n //\n // A cooldown skip is NOT the same as \"your request should give up\": the\n // caller re-checks the generation counter and will still retry if some\n // other caller refreshed. See `tryRefreshAndRetry`.\n if (reason === \"401-retry\" || reason === \"expiry\") {\n const now = Date.now()\n // A credential newer than the one we last tried is new information,\n // not a repeat attempt, so it bypasses both cooldowns. Without this,\n // the window right after `github-router auth` — exactly the recovery\n // path — would be swallowed by the 30s success cooldown.\n const onDisk = await readGithubTokenIfUsable()\n const hasNewerCredential = Boolean(\n onDisk && onDisk !== state.githubToken,\n )\n\n if (!hasNewerCredential) {\n if (now - lastRefreshSuccess < REFRESH_SUCCESS_COOLDOWN_MS) {\n consola.debug(\n `refreshCopilotToken(${reason}) skipped: prior success within ${REFRESH_SUCCESS_COOLDOWN_MS}ms`,\n )\n return \"skipped\"\n }\n if (now - lastRefreshFailure < REFRESH_FAILURE_COOLDOWN_MS) {\n consola.debug(\n `refreshCopilotToken(${reason}) skipped: prior failure within ${REFRESH_FAILURE_COOLDOWN_MS}ms`,\n )\n return \"skipped\"\n }\n }\n }\n\n consola.debug(`Refreshing Copilot token (reason=${reason})`)\n try {\n const { token, refresh_in } = await exchangeWithFreshestCredential()\n commitCopilotToken(token, refresh_in)\n lastRefreshSuccess = Date.now()\n consola.debug(\n `Copilot token refreshed (credential ${credentialFingerprint(state.githubToken)}, generation ${state.copilotTokenGeneration})`,\n )\n if (state.showToken) {\n consola.info(\"Refreshed Copilot token:\", token)\n }\n return \"refreshed\"\n } catch (error) {\n lastRefreshFailure = Date.now()\n // Log the STATUS and body, not just the message. Their absence is why\n // the 2026-08-08 incident could not be attributed after the fact.\n consola.error(\n `Failed to refresh Copilot token (reason=${reason}):`,\n error instanceof Error ? error.message : error,\n )\n if (error instanceof CopilotTokenExchangeError) {\n if (error.kind === \"credential_rejected\") return \"credential_rejected\"\n if (error.kind === \"entitlement_lapsed\") return \"entitlement_lapsed\"\n }\n return \"transient_failure\"\n }\n } finally {\n // Only release the lock if it is still OURS. An unconditional clear\n // would let a finished attempt release a lock a newer attempt holds,\n // re-opening the overlapping-exchange window this mutex exists to\n // close. Identity-checking makes that safe no matter how the code\n // around it is later rearranged. `attempt` is assigned before any\n // await inside `run()` can settle, so it is always bound here.\n if (inflightRefresh === attempt) inflightRefresh = undefined\n }\n }\n\n const attempt = run()\n inflightRefresh = attempt\n return attempt\n}\n\n/**\n * Refresh the Copilot token if its derived deadline has passed.\n *\n * Called on EVERY request. The common path is one `Date.now()` and one\n * integer compare — no I/O — against an upstream call that costs hundreds of\n * milliseconds, so the overhead is not measurable. Sampling (every Nth\n * request) was considered and rejected: a session that idles and then resumes\n * may issue only a handful of requests, so a sampled guard would fail to fire\n * in precisely the scenario it exists for. Actual refreshes do not get more\n * frequent, because `refreshCopilotToken` is single-flighted and the deadline\n * moves forward on success.\n */\nexport async function ensureFreshCopilotToken(): Promise<void> {\n const deadline = state.copilotTokenRefreshAt\n if (deadline === undefined || Date.now() < deadline) return\n await refreshCopilotToken(\"expiry\")\n}\n\n/**\n * Try `request()`. If it returns a 401, refresh the Copilot token (subject\n * to the single-flight + refresh-storm-protection of `refreshCopilotToken`)\n * and retry once — but ONLY if the token actually moved on.\n *\n * The retry criterion is the generation counter, not the refresh's verdict\n * and not a token-string comparison. Both alternatives are wrong:\n *\n * - By verdict: requests A and B both hold token T0 and both 401. A\n * refreshes to T1. B's refresh call lands inside the 30s success cooldown\n * and reports \"skipped\", so a verdict-based rule would give up and fail B\n * with a 503 — even though T1 is sitting in state and would have worked.\n * - By string: upstream demonstrably returns a byte-identical token from\n * two consecutive successful exchanges, so an unchanged string does not\n * mean an unchanged credential state.\n *\n * Comparing generations answers the question the request actually has: is\n * what is in state now different from what I already tried? When it is not,\n * we skip a re-send that is guaranteed to 401 again.\n *\n * The `request` callback is responsible for capturing `state.copilotToken`\n * locally before any await; this helper does NOT re-build the request\n * itself, just re-invokes the callback after a refresh.\n */\nexport async function tryRefreshAndRetry(\n request: () => Promise<Response>,\n routePath: string,\n): Promise<Response> {\n await ensureFreshCopilotToken()\n\n const generationBefore = state.copilotTokenGeneration\n const first = await request()\n if (first.status !== 401) return first\n\n consola.warn(\n `${routePath}: upstream returned 401, attempting one token refresh + retry`,\n )\n await refreshCopilotToken(\"401-retry\")\n\n if (state.copilotTokenGeneration === generationBefore) {\n // Nothing new to try. Re-sending the same credential would buy a second\n // guaranteed 401 — which is what every request during the 2026-08-08\n // outage paid, for an hour.\n consola.debug(\n `${routePath}: no newer Copilot token available (generation ${generationBefore}); not retrying`,\n )\n return first\n }\n\n // Re-invoke the request with the new token in state.\n return request()\n}\n\ninterface SetupGitHubTokenOptions {\n force?: boolean\n}\n\nexport async function setupGitHubToken(\n options?: SetupGitHubTokenOptions,\n): Promise<void> {\n try {\n // Retrying: a startup read can race an `auth` rewrite in another\n // terminal, and on Windows that surfaces as a transient EPERM/EBUSY/\n // ENOENT. Failing launch over a window that closes in milliseconds\n // would be a self-inflicted outage.\n const githubToken = await readGithubTokenWithRetry()\n\n if (githubToken && !options?.force) {\n state.githubToken = githubToken\n state.githubTokenSource = \"file\"\n if (state.showToken) {\n consola.info(\"GitHub token:\", githubToken)\n }\n await logUser()\n\n return\n }\n\n consola.info(\n options?.force\n ? \"Re-authenticating, getting new access token\"\n : \"Not logged in, getting new access token\",\n )\n const response = await getDeviceCode()\n consola.debug(\"Device code response:\", response)\n\n consola.info(\n `Please enter the code \"${response.user_code}\" in ${response.verification_uri}`,\n )\n\n const token = await pollAccessToken(response)\n await writeGithubToken(token)\n state.githubToken = token\n state.githubTokenSource = \"file\"\n\n if (state.showToken) {\n consola.info(\"GitHub token:\", token)\n }\n await logUser()\n } catch (error) {\n if (error instanceof HTTPError) {\n consola.error(\"Failed to get GitHub token:\", await error.response.json())\n throw error\n }\n\n consola.error(\"Failed to get GitHub token:\", error)\n throw error\n }\n}\n\nasync function logUser() {\n const user = await getGitHubUser()\n consola.info(`Logged in as ${user.login}`)\n}\n\n/**\n * Set up the SECOND, write-capable GitHub token used by the first-mate\n * agent-orchestration surface (`--agents`). Mirrors `setupGitHubToken`\n * but authenticates against the GitHub CLI's OAuth client\n * (`GITHUB_AGENT_CLIENT_ID`) requesting `repo workflow read:org`, and\n * stores the result apart at `PATHS.GITHUB_AGENT_TOKEN_PATH`. The Copilot\n * App token (`state.githubToken`) is left completely untouched — this is\n * a distinct identity for a distinct capability.\n *\n * Long-lived (device-flow user token) → no refresh loop; a later 401 is\n * surfaced to the caller as a revoked grant to re-run the login. Called\n * once from `setupAndServe` when `state.agentsEnabled` is true.\n */\nexport async function setupGitHubAgentToken(\n options?: SetupGitHubTokenOptions,\n): Promise<void> {\n try {\n const existing = (await readGithubAgentToken().catch(() => \"\")).trim()\n\n if (existing && !options?.force) {\n state.githubAgentToken = existing\n if (state.showToken) {\n consola.info(\"GitHub agent token:\", existing)\n }\n await warnIfAgentScopesInsufficient()\n return\n }\n\n consola.info(\n \"Agent mode (--agents): a second GitHub login is required for a write-capable token (repo, workflow, read:org).\",\n )\n const response = await getDeviceCode({\n clientId: GITHUB_AGENT_CLIENT_ID,\n scope: GITHUB_AGENT_SCOPES,\n })\n consola.debug(\"Agent device code response:\", response)\n\n consola.info(\n `Please enter the code \"${response.user_code}\" in ${response.verification_uri} to authorize github-router's cloud-agent orchestration to act on your repositories.`,\n )\n\n const token = await pollAccessToken(response, GITHUB_AGENT_CLIENT_ID)\n await writeGithubAgentToken(token)\n state.githubAgentToken = token\n\n if (state.showToken) {\n consola.info(\"GitHub agent token:\", token)\n }\n await warnIfAgentScopesInsufficient()\n } catch (error) {\n if (error instanceof HTTPError) {\n consola.error(\n \"Failed to get GitHub agent token:\",\n await error.response.json(),\n )\n throw error\n }\n\n consola.error(\"Failed to get GitHub agent token:\", error)\n throw error\n }\n}\n\n/**\n * Best-effort check that the agent token actually carries the scopes we\n * asked for. The GitHub CLI OAuth client is a classic OAuth App, so the\n * granted scopes are echoed in the `x-oauth-scopes` response header on\n * any authenticated call. Warn loudly (not fatal) if `repo`/`workflow`\n * are missing so the failure is diagnosable at login rather than at the\n * first write 403.\n */\nasync function warnIfAgentScopesInsufficient(): Promise<void> {\n try {\n const res = await fetch(`${GITHUB_API_BASE_URL}/user`, {\n headers: githubAgentHeaders(state),\n })\n if (res.status === 401) {\n consola.warn(\n \"GitHub agent token was rejected (401) — the grant may have been revoked. Re-run with --agents to log in again.\",\n )\n return\n }\n const scopes = (res.headers.get(\"x-oauth-scopes\") ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n const missing = [\"repo\", \"workflow\"].filter((s) => !scopes.includes(s))\n if (missing.length > 0) {\n consola.warn(\n `GitHub agent token is missing scope(s): ${missing.join(\", \")}. `\n + \"The first-mate surface needs 'repo' + 'workflow' to create issues, \"\n + \"assign cloud agents, and dispatch workflows. Re-run the agent login \"\n + \"and grant the requested scopes.\",\n )\n }\n } catch (err) {\n consola.debug(\"Agent token scope check skipped:\", err)\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"version-_Q1WpsQp.js","names":[],"sources":["../src/lib/version.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\n/**\n * Read this binary's published version from package.json at runtime.\n *\n * Done at runtime (not baked at build time) because release.yml builds\n * BEFORE `npm version patch` bumps the version — a build-time inline\n * would always ship the pre-bump value. The npm tarball ships package.json\n * alongside `dist/`, so a sibling-up lookup from import.meta.url resolves\n * cleanly in both dev (`src/lib/`) and bundled (`dist/`) layouts.\n *\n * Returns `\"unknown\"` if package.json can't be located or parsed —\n * never throws, so the CLI never fails to start over version reporting.\n */\nexport function getPackageVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n // src/lib/version.ts → ../../package.json (dev)\n // dist/main.js → ../package.json (built npm tarball)\n // dist/<chunk>.js → ../package.json (split bundle)\n const candidates = [\n join(here, \"..\", \"..\", \"package.json\"),\n join(here, \"..\", \"package.json\"),\n ]\n for (const path of candidates) {\n try {\n const raw = readFileSync(path, \"utf8\")\n const parsed = JSON.parse(raw) as { version?: unknown; name?: unknown }\n if (\n typeof parsed.version === \"string\"\n && (parsed.name === \"github-router\"\n || parsed.name === \"@animeshkundu/github-router\")\n ) {\n return parsed.version\n }\n } catch {\n // Try next candidate.\n }\n }\n } catch {\n // Fall through to \"unknown\".\n }\n return \"unknown\"\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,oBAA4B;CAC1C,IAAI;EACF,MAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;EAInD,MAAM,aAAa,CACjB,KAAK,MAAM,MAAM,MAAM,cAAc,GACrC,KAAK,MAAM,MAAM,cAAc,CACjC;EACA,KAAK,MAAM,QAAQ,YACjB,IAAI;GACF,MAAM,MAAM,aAAa,MAAM,MAAM;GACrC,MAAM,SAAS,KAAK,MAAM,GAAG;GAC7B,IACE,OAAO,OAAO,YAAY,aACtB,OAAO,SAAS,mBACf,OAAO,SAAS,gCAErB,OAAO,OAAO;EAElB,QAAQ,CAER;CAEJ,QAAQ,CAER;CACA,OAAO;AACT"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"worker-dispatch-Bj1uYyG9.js","names":[],"sources":["../src/lib/worker-dispatch.ts"],"sourcesContent":["/**\n * Frozen contract for the NON-BLOCKING workers surface.\n *\n * The `workers` MCP tools (`explore`/`implement`/`review`/`plan`/`test`, and\n * `browse` when the browse agent is enabled) BLOCK the caller for up to 6h\n * (`runWorkerAgent`). The MAIN Claude Code agent must never block on one, so a\n * per-mode `worker-*` DISPATCHER SUBAGENT — which Claude Code runs in the\n * background and reports on via a completion notification — is the only\n * sanctioned way to run a worker. This module is the single source of truth for\n * three things that must never drift:\n *\n * 1. the tool → dispatcher map (`mcp__<workersKey>__<mode>` ↔ `worker-<mode>`),\n * 2. the PreToolUse GUARD decision that denies a raw worker call from the main\n * agent (or any non-dispatcher subagent) and redirects it to the matching\n * `worker-*` agent, allowing it only from the dispatcher itself, and\n * 3. the dispatcher subagent bodies (description / system prompt / `tools:`\n * allowlist).\n *\n * Imported by `codex-mcp-config.ts` (dispatcher `.md` generation + the hook\n * command it bakes into settings.json) AND `internal-worker-guard.ts` (the\n * runtime PreToolUse hook). Keeping the map here means the settings matcher, the\n * redirect target, the dispatcher names, and the `tools:` allowlist are all\n * derived from one place.\n *\n * Discrimination mechanism: a Claude Code PreToolUse payload carries\n * `agent_type` (the invoking subagent's name) ONLY inside a subagent context —\n * absent for the top-level/main agent. So `agent_type === \"worker-<mode>\"` is\n * the reliable \"this call came from the dispatcher, allow it\" signal, and its\n * absence (or any other value) means \"main agent or an unrelated subagent →\n * deny\". This is the SAME field this repo's Stop / prompt-submit hooks already\n * key off (`isSubagentContext` in `orchestration/stop-gate-policy.ts`); the\n * guard here is the inverse (allow-only-dispatcher rather than skip-any-sub).\n */\n\n/** The five always-available worker modes (gated by `workerToolsEnabled()`). */\nexport const CORE_WORKER_MODES = [\n \"explore\",\n \"implement\",\n \"review\",\n \"plan\",\n \"test\",\n] as const\n\n/** The browse worker mode, gated separately by `browseAgentEnabled()`. */\nexport const BROWSE_WORKER_MODE = \"browse\" as const\n\nexport type CoreWorkerMode = (typeof CORE_WORKER_MODES)[number]\nexport type WorkerDispatchMode = CoreWorkerMode | typeof BROWSE_WORKER_MODE\n\n/** Every mode the surface can expose — used to build the sweep-regex allowlist\n * and to validate `--modes`. Order is stable (core first, browse last). */\nexport const ALL_WORKER_DISPATCH_MODES: ReadonlyArray<WorkerDispatchMode> = [\n ...CORE_WORKER_MODES,\n BROWSE_WORKER_MODE,\n]\n\n/** The dispatcher subagent name for a mode, e.g. `implement` → `worker-implement`. */\nexport function dispatcherAgentName(mode: WorkerDispatchMode): string {\n return `worker-${mode}`\n}\n\n/** Every possible dispatcher subagent name (used by the stale-`.md` sweep). */\nexport const ALL_DISPATCHER_AGENT_NAMES: ReadonlyArray<string> =\n ALL_WORKER_DISPATCH_MODES.map(dispatcherAgentName)\n\n/** The MCP tool name the workers server exposes for a mode under the resolved key. */\nexport function workerToolName(workersKey: string, mode: WorkerDispatchMode): string {\n return `mcp__${workersKey}__${mode}`\n}\n\n/** The active dispatch modes for a launch: the five core modes plus `browse`\n * only when the browse agent is enabled. */\nexport function activeDispatchModes(opts: { browse: boolean }): Array<WorkerDispatchMode> {\n return opts.browse ? [...CORE_WORKER_MODES, BROWSE_WORKER_MODE] : [...CORE_WORKER_MODES]\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n\n/**\n * The Claude Code PreToolUse `matcher` (a regex over the tool name) that scopes\n * the guard hook to exactly the active worker tools — nothing else invokes the\n * hook. Anchored + exact-alternation so an unrelated `mcp__<key>__status` (a\n * future non-blocking tool with no dispatcher) is never matched, hence never\n * denied.\n */\nexport function guardToolMatcher(workersKey: string, modes: ReadonlyArray<WorkerDispatchMode>): string {\n const alt = modes.map((m) => escapeRegex(m)).join(\"|\")\n return `^mcp__${escapeRegex(workersKey)}__(${alt})$`\n}\n\n/** Parse the worker mode out of a tool name for the resolved key, or null if it\n * isn't one of the recognized worker tools. */\nexport function parseWorkerToolCall(\n toolName: string,\n workersKey: string,\n modes: ReadonlyArray<WorkerDispatchMode>,\n): WorkerDispatchMode | null {\n const prefix = `mcp__${workersKey}__`\n if (!toolName.startsWith(prefix)) return null\n const rest = toolName.slice(prefix.length)\n return (modes as ReadonlyArray<string>).includes(rest) ? (rest as WorkerDispatchMode) : null\n}\n\n/** The `permissionDecisionReason` shown to the model on a deny, steering it to\n * the matching background dispatcher. `mode` is null when the payload was\n * unparseable (fail-closed generic message). */\nexport function guardDenyReason(mode: WorkerDispatchMode | null): string {\n const target = mode\n ? `Agent(subagent_type: \"${dispatcherAgentName(mode)}\", prompt: <your worker brief>)`\n : `the matching background \\`worker-*\\` agent via the Agent tool`\n return (\n \"Workers run as background subagents in this session so your turn never blocks. \"\n + `Re-issue this as ${target}. `\n + \"It returns immediately and delivers the worker's result as a completion notification — \"\n + \"do not call the raw `mcp__…__` worker tool from the main thread.\"\n )\n}\n\n/** The stdout JSON a PreToolUse hook prints to DENY a tool call. */\nexport function guardDenyOutput(mode: WorkerDispatchMode | null): string {\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: guardDenyReason(mode),\n },\n })\n}\n\nexport interface WorkerGuardResult {\n /** JSON to print to stdout (a deny decision), or null to ALLOW (print nothing). */\n output: string | null\n /** Machine-readable verdict for tests/telemetry. */\n verdict: \"allow-dispatcher\" | \"allow-non-worker\" | \"deny-main\" | \"deny-malformed\"\n}\n\n/**\n * Pure PreToolUse guard decision. Given the raw stdin payload, the resolved\n * workers key, and the active modes, decide whether to DENY the tool call.\n *\n * Rules (fail toward protecting the \"main never blocks\" invariant):\n * - Payload unparseable / no string `tool_name`: the matcher only fires this\n * hook for worker tools, so a payload we can't read is still a worker call\n * → DENY (fail closed, generic redirect).\n * - `tool_name` is not a recognized worker tool for this key: ALLOW (not a\n * tool we guard — never deny a non-worker tool).\n * - `agent_type` equals one of the active dispatcher names: ALLOW (the call\n * came from the dispatcher subagent that is meant to run the worker).\n * - Otherwise (main agent: `agent_type` absent; or a non-dispatcher subagent):\n * DENY with a redirect to the matching `worker-<mode>` agent.\n */\nexport function decideWorkerGuard(input: {\n stdin: string\n workersKey: string\n modes: ReadonlyArray<WorkerDispatchMode>\n}): WorkerGuardResult {\n let payload: { tool_name?: unknown; agent_type?: unknown } | null = null\n try {\n const parsed: unknown = JSON.parse(input.stdin)\n if (parsed && typeof parsed === \"object\") {\n payload = parsed as { tool_name?: unknown; agent_type?: unknown }\n }\n } catch {\n payload = null\n }\n\n const toolName = typeof payload?.tool_name === \"string\" ? payload.tool_name : null\n if (!toolName) {\n // Fail CLOSED: the settings matcher only routes worker-tool calls here, so\n // an unreadable payload is still a worker call we must not let through.\n return { output: guardDenyOutput(null), verdict: \"deny-malformed\" }\n }\n\n const mode = parseWorkerToolCall(toolName, input.workersKey, input.modes)\n if (mode === null) {\n // Not one of the worker tools we guard (matcher over-fired, or a future\n // non-blocking tool under the same server) → allow the normal flow.\n return { output: null, verdict: \"allow-non-worker\" }\n }\n\n const agentType = payload?.agent_type\n if (typeof agentType === \"string\" && agentType === dispatcherAgentName(mode)) {\n // The call originated inside the `worker-<mode>` dispatcher subagent that is\n // meant to run THIS worker. Require an exact mode match (not just \"any\n // dispatcher\"), so a read-only `worker-explore` can't invoke the\n // write-capable `implement` worker if it misroutes or is prompt-injected.\n // If a matching dispatcher is still denied, the payload that reached this\n // hook did not carry this exact top-level `agent_type`, or a different\n // PreToolUse hook denied the call after this guard allowed it.\n return { output: null, verdict: \"allow-dispatcher\" }\n }\n\n // Main agent (agent_type absent), a non-dispatcher subagent, or a dispatcher\n // for a DIFFERENT mode → deny + redirect. Denying non-dispatcher subagents\n // too closes the transitive-blocking hole (a foreground subagent that blocked\n // on a worker would transitively block the main agent).\n return { output: guardDenyOutput(mode), verdict: \"deny-main\" }\n}\n\n// ─── Dispatcher subagent bodies ──────────────────────────────────────────────\n\n/** One-line human description shown to the lead when picking a subagent. Uses\n * the documented \"Use proactively\" auto-delegation idiom. */\nexport function dispatcherDescription(mode: WorkerDispatchMode): string {\n const blurb: Record<WorkerDispatchMode, string> = {\n explore:\n \"Non-blocking `explore` worker: dispatches a read-only autonomous worker (its own context) in the background and delivers its summary as a completion notification.\",\n implement:\n \"Non-blocking `implement` worker: dispatches an autonomous coding worker (read/write/bash) that ALWAYS runs in an isolated git worktree and returns the diff (for in-place edits use the `implementer` subagent), in the background, and delivers its result as a completion notification.\",\n review:\n \"Non-blocking `review` worker: the backgrounded counterpart of the `reviewer` subagent, running a cross-lab model. In a git workspace it takes an isolated worktree and can therefore VERIFY rather than only read: reproduce a failure, run the build or suite to check a claim, and author a throwaway probe test. Outside git it degrades to read-only rather than failing. Returns severity-ranked findings with file:line and a go/no-go, as a completion notification. Use `explore` instead when you just need to find or understand something.\",\n plan:\n \"Non-blocking `plan` worker: dispatches a read-only planner that returns an ordered implementation plan, in the background, and delivers it as a completion notification.\",\n test:\n \"Non-blocking `test` worker: dispatches an independent test author (in an isolated git worktree) that writes tests trying to break the implementation, in the background, and delivers pass/fail as a completion notification.\",\n browse:\n \"Non-blocking `browse` worker: dispatches an autonomous browser agent in the background and delivers its result as a completion notification.\",\n }\n return (\n `${blurb[mode]} Use proactively for any ${mode}-mode worker task so a long run never blocks your turn: `\n + \"it returns immediately and notifies you when done.\"\n )\n}\n\n/** The dispatcher subagent's full system prompt: call the one worker tool once,\n * relay verbatim, do nothing else. */\nexport function dispatcherPrompt(mode: WorkerDispatchMode, workersKey: string): string {\n const tool = workerToolName(workersKey, mode)\n const briefField = mode === \"browse\" ? \"task\" : \"prompt\"\n const briefDescription = mode === \"browse\" ? \"the lead's browse task, copied verbatim\" : \"the lead's worker brief, copied verbatim\"\n const modeSpecificPassThrough = mode === \"browse\"\n ? \"\\n - `sessionId` (optional): pass through if the lead specified one\"\n : \"\"\n return [\n `# Subagent: ${dispatcherAgentName(mode)}`,\n \"\",\n `You are a thin DISPATCHER for the \\`${mode}\\` worker. You run in the background so the`,\n \"lead agent's turn is never blocked while the (up-to-6-hour) worker runs.\",\n \"\",\n \"## Your only job\",\n \"\",\n `Call the \\`${tool}\\` tool EXACTLY ONCE, passing through the fields from the lead's brief:`,\n ` - \\`${briefField}\\`: ${briefDescription}`,\n \" - `workspace` (optional): absolute path, if the lead specified one\",\n \" - `model` / `thinking` (optional): only if the lead specified them\",\n \" - `maxWallClockMs` (optional): per-call wall-clock budget in ms, if the lead specified one\"\n + modeSpecificPassThrough,\n \"\",\n \"When the tool returns, output its result VERBATIM as your final message. That final\",\n \"message is what the lead receives in the completion notification — it IS the result.\",\n \"\",\n \"## Hard rules\",\n \"\",\n \"- Call the worker tool exactly once. Do not retry on a normal (non-error) return.\",\n \"- Do NOT attempt the task yourself, do NOT read/edit files, do NOT run other tools.\",\n \"- Do NOT spawn other agents (you have no Agent tool and must not try to gain one).\",\n \"- Do NOT summarize, paraphrase, or add commentary — relay the worker output verbatim.\",\n \"- If the worker returns an error, relay that error verbatim (do not mask it).\",\n ].join(\"\\n\")\n}\n\n/** The `tools:` frontmatter allowlist for a dispatcher: the workers MCP server\n * wildcard (`mcp__<workersKey>__*`). Claude Code's `tools:` field supports\n * MCP patterns only at SERVER granularity (`mcp__<server>__*`), not individual\n * tool names, so this grants exactly the workers tools and NOTHING else — no\n * Agent/Task (so it cannot spawn further agents → no recursion), no Read/Bash\n * (so it cannot do extra work). The dispatcher's prompt narrows it to the one\n * mode; the guard allows only the exact dispatcher for that worker mode. */\nexport function dispatcherTools(_mode: WorkerDispatchMode, workersKey: string): Array<string> {\n return [`mcp__${workersKey}__*`]\n}\n\n/**\n * Build the shell command Claude Code runs for the workers `PreToolUse` guard\n * hook — the running github-router via its node/bun binary so it works\n * regardless of PATH. Mirrors `buildPromptSubmitHookCommand`.\n *\n * The resolved `workersKey` and the active `modes` are baked into the command\n * ARGS (not env): `mergeStopHookIntoSettings` dedups hooks by the command\n * string only, so baking the key/modes makes a changed resolution produce a\n * DISTINCT command — no stale-matcher entry can survive from a prior launch.\n */\nexport function buildWorkerGuardHookCommand(\n execPath: string,\n scriptPath: string | undefined,\n workersKey: string,\n modes: ReadonlyArray<WorkerDispatchMode>,\n): string {\n const q = (s: string): string => `\"${s}\"`\n const args = `internal-worker-guard --workers-key ${q(workersKey)} --modes ${q(modes.join(\",\"))}`\n if (scriptPath && scriptPath !== execPath) {\n return `${q(execPath)} ${q(scriptPath)} ${args}`\n }\n return `${q(execPath)} ${args}`\n}\n\n/** Parse a `--modes` CSV back into validated modes (drops unknown tokens). */\nexport function parseModesCsv(csv: string | undefined): Array<WorkerDispatchMode> {\n if (!csv) return [...CORE_WORKER_MODES]\n const known = new Set<string>(ALL_WORKER_DISPATCH_MODES)\n const out = csv\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => known.has(s)) as Array<WorkerDispatchMode>\n return out.length > 0 ? out : [...CORE_WORKER_MODES]\n}\n"],"mappings":"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"}