github-router 0.3.297 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{attribution-settings-B70GmZ68.js → attribution-settings-dta8MTnd.js} +322 -95
- package/dist/attribution-settings-dta8MTnd.js.map +1 -0
- package/dist/{auth-CluG5e-l.js → auth-CpjOjoUG.js} +4 -3
- package/dist/{auth-CluG5e-l.js.map → auth-CpjOjoUG.js.map} +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{check-usage-iOzfnKJe.js → check-usage-BsN3S4Fu.js} +4 -4
- package/dist/{check-usage-iOzfnKJe.js.map → check-usage-BsN3S4Fu.js.map} +1 -1
- package/dist/{claude-DMMfvfns.js → claude-kr-Augld.js} +169 -170
- package/dist/claude-kr-Augld.js.map +1 -0
- package/dist/{codex-DVOyVv2B.js → codex-B1qnVlzG.js} +6 -5
- package/dist/{codex-DVOyVv2B.js.map → codex-B1qnVlzG.js.map} +1 -1
- package/dist/{debug-DsEwleGg.js → debug-BXRPyeqO.js} +2 -2
- package/dist/{debug-DsEwleGg.js.map → debug-BXRPyeqO.js.map} +1 -1
- package/dist/engine-B3w1TDgR.js +2 -0
- package/dist/{fast-profile-contract-K3P58nj3.js → fast-profile-contract-B6xn-Rzo.js} +26 -23
- package/dist/fast-profile-contract-B6xn-Rzo.js.map +1 -0
- package/dist/{gate-discovery-G6PAG61j.js → gate-discovery-DOIm3XHF.js} +5 -5
- package/dist/{gate-discovery-G6PAG61j.js.map → gate-discovery-DOIm3XHF.js.map} +1 -1
- package/dist/{get-copilot-usage-B7Z4XVWj.js → get-copilot-usage-ByjQqhdi.js} +3 -2
- package/dist/{get-copilot-usage-B7Z4XVWj.js.map → get-copilot-usage-ByjQqhdi.js.map} +1 -1
- package/dist/hooks.mjs +393 -40
- package/dist/hooks.sha256 +1 -1
- package/dist/{internal-artifact-open-D6tKMg8v.js → internal-artifact-open-DRSxShAW.js} +2 -2
- package/dist/{internal-artifact-open-D6tKMg8v.js.map → internal-artifact-open-DRSxShAW.js.map} +1 -1
- package/dist/{internal-fast-dispatch-guard-Bkr_iGOu.js → internal-fast-dispatch-guard-BhpZ6LvO.js} +72 -16
- package/dist/internal-fast-dispatch-guard-BhpZ6LvO.js.map +1 -0
- package/dist/{internal-fast-dispatch-guard-N-RaLrOn.js → internal-fast-dispatch-guard-C6BNoOnn.js} +1 -1
- package/dist/{internal-first-mate-guard-C9BV7J4U.js → internal-first-mate-guard-Bcb5JwiO.js} +3 -3
- package/dist/{internal-first-mate-guard-C9BV7J4U.js.map → internal-first-mate-guard-Bcb5JwiO.js.map} +1 -1
- package/dist/{internal-first-mate-guard-WJApyc8j.js → internal-first-mate-guard-Brh_uois.js} +1 -1
- package/dist/internal-max-dispatch-guard-DhAr8N_o.js +215 -0
- package/dist/internal-max-dispatch-guard-DhAr8N_o.js.map +1 -0
- package/dist/internal-max-dispatch-guard-k52H-PdC.js +2 -0
- package/dist/{internal-plan-review-t3PuNfuL.js → internal-plan-review-DVY-iV2x.js} +3 -3
- package/dist/{internal-plan-review-t3PuNfuL.js.map → internal-plan-review-DVY-iV2x.js.map} +1 -1
- package/dist/{internal-prompt-submit-C3KovHQG.js → internal-prompt-submit-BTqRn4FW.js} +4 -4
- package/dist/{internal-prompt-submit-C3KovHQG.js.map → internal-prompt-submit-BTqRn4FW.js.map} +1 -1
- package/dist/{internal-session-bind-d2uoja8I.js → internal-session-bind-YfH656lt.js} +2 -2
- package/dist/{internal-session-bind-d2uoja8I.js.map → internal-session-bind-YfH656lt.js.map} +1 -1
- package/dist/{internal-stop-hook-BjzrF1zd.js → internal-stop-hook-CWffknT2.js} +5 -5
- package/dist/{internal-stop-hook-BjzrF1zd.js.map → internal-stop-hook-CWffknT2.js.map} +1 -1
- package/dist/{internal-stop-review-YtDUGXpX.js → internal-stop-review-B8MmEyQ1.js} +2 -2
- package/dist/{internal-stop-review-YtDUGXpX.js.map → internal-stop-review-B8MmEyQ1.js.map} +1 -1
- package/dist/{internal-worker-guard-CdkeS9PN.js → internal-worker-guard-Dr6W83TC.js} +2 -2
- package/dist/{internal-worker-guard-CdkeS9PN.js.map → internal-worker-guard-Dr6W83TC.js.map} +1 -1
- package/dist/{internal-workspace-header-tuBXJV22.js → internal-workspace-header-Cep0WSIb.js} +2 -2
- package/dist/{internal-workspace-header-tuBXJV22.js.map → internal-workspace-header-Cep0WSIb.js.map} +1 -1
- package/dist/lifecycle-4TZrJuAa.js +2 -0
- package/dist/{lifecycle-CM9eTzvk.js → lifecycle-CmPJ6e_6.js} +2 -2
- package/dist/{lifecycle-CM9eTzvk.js.map → lifecycle-CmPJ6e_6.js.map} +1 -1
- package/dist/{lifecycle-Bg6doY3-.js → lifecycle-DgsJ_8Am.js} +2 -2
- package/dist/{lifecycle-Bg6doY3-.js.map → lifecycle-DgsJ_8Am.js.map} +1 -1
- package/dist/lifecycle-O91Vzzb-.js +2 -0
- package/dist/main.js +19 -18
- package/dist/main.js.map +1 -1
- package/dist/max-profile-contract-Ct62mtAR.js +276 -0
- package/dist/max-profile-contract-Ct62mtAR.js.map +1 -0
- package/dist/{mcp-workspace-header-dERl2YTT.js → mcp-workspace-header-NzFD0Wgd.js} +2 -2
- package/dist/{mcp-workspace-header-dERl2YTT.js.map → mcp-workspace-header-NzFD0Wgd.js.map} +1 -1
- package/dist/{models-DZ4hYsR7.js → models-1KwEUgzd.js} +3 -3
- package/dist/{models-DZ4hYsR7.js.map → models-1KwEUgzd.js.map} +1 -1
- package/dist/{orchestration-BiGCEwbH.js → orchestration-CYyp9w-t.js} +2 -2
- package/dist/{orchestration-BiGCEwbH.js.map → orchestration-CYyp9w-t.js.map} +1 -1
- package/dist/paths-DDAiPVto.js +2 -0
- package/dist/{paths-Ci485qSJ.js → paths-IcZW8gZx.js} +4 -4
- package/dist/{paths-Ci485qSJ.js.map → paths-IcZW8gZx.js.map} +1 -1
- package/dist/{peer-mcp-personas-ce6_YMnX.js → peer-mcp-personas-BU5dNLxQ.js} +737 -459
- package/dist/peer-mcp-personas-BU5dNLxQ.js.map +1 -0
- package/dist/{plan-review-hook-DuW0pSoM.js → plan-review-hook-szSNt6uU.js} +3 -3
- package/dist/{plan-review-hook-DuW0pSoM.js.map → plan-review-hook-szSNt6uU.js.map} +1 -1
- package/dist/{prompt-submit-hook-IMl7ssfr.js → prompt-submit-hook-ZUxo_jEl.js} +3 -3
- package/dist/{prompt-submit-hook-IMl7ssfr.js.map → prompt-submit-hook-ZUxo_jEl.js.map} +1 -1
- package/dist/{provision-BhCtm3md.js → provision-qNJgF5XD.js} +4 -4
- package/dist/{provision-BhCtm3md.js.map → provision-qNJgF5XD.js.map} +1 -1
- package/dist/{self-invocation-DAB_od0C.js → self-invocation-BJWbA8ka.js} +2 -2
- package/dist/{self-invocation-DAB_od0C.js.map → self-invocation-BJWbA8ka.js.map} +1 -1
- package/dist/{serve-Blj_dRSr.js → serve-T64kVeQN.js} +13 -12
- package/dist/{serve-Blj_dRSr.js.map → serve-T64kVeQN.js.map} +1 -1
- package/dist/{server-setup-DhG3YZPj.js → server-setup-C_LAE8NI.js} +222 -40
- package/dist/server-setup-C_LAE8NI.js.map +1 -0
- package/dist/{start-8QaqICtd.js → start-DHRToEyb.js} +3 -3
- package/dist/{start-8QaqICtd.js.map → start-DHRToEyb.js.map} +1 -1
- package/dist/state-B8adscCv.js +51 -0
- package/dist/state-B8adscCv.js.map +1 -0
- package/dist/{stop-gate-hook-DQnh_KfI.js → stop-gate-hook-CJOk28zd.js} +3 -3
- package/dist/{stop-gate-hook-DQnh_KfI.js.map → stop-gate-hook-CJOk28zd.js.map} +1 -1
- package/dist/{stop-gate-policy-DMr3KVmw.js → stop-gate-policy-zhzRIj6G.js} +2 -2
- package/dist/{stop-gate-policy-DMr3KVmw.js.map → stop-gate-policy-zhzRIj6G.js.map} +1 -1
- package/dist/{token-BtJhjXXu.js → token-CbuFyx-B.js} +4 -50
- package/dist/token-CbuFyx-B.js.map +1 -0
- package/dist/{worker-dispatch-BEHkTbnH.js → worker-dispatch-DgP5yT26.js} +2 -2
- package/dist/{worker-dispatch-BEHkTbnH.js.map → worker-dispatch-DgP5yT26.js.map} +1 -1
- package/package.json +1 -1
- package/dist/attribution-settings-B70GmZ68.js.map +0 -1
- package/dist/claude-DMMfvfns.js.map +0 -1
- package/dist/engine-BkGRuUNs.js +0 -2
- package/dist/fast-profile-contract-K3P58nj3.js.map +0 -1
- package/dist/internal-fast-dispatch-guard-Bkr_iGOu.js.map +0 -1
- package/dist/lifecycle-CGLt1cJQ.js +0 -2
- package/dist/lifecycle-DoUwpVDB.js +0 -2
- package/dist/paths-GD7bgGGy.js +0 -2
- package/dist/peer-mcp-personas-ce6_YMnX.js.map +0 -1
- package/dist/server-setup-DhG3YZPj.js.map +0 -1
- package/dist/token-BtJhjXXu.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-Bg6doY3-.js","names":[],"sources":["../src/lib/worker-agent/lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle plumbing for worker worktrees: in-memory registry, signal\n * handlers, ledger of repos touched, and the boot-time PID+instance\n * safety net.\n *\n * Plan: see `plans/we-have-added-a-dreamy-tide.md` (\"Worktree mode\" →\n * \"Cleanup paths\"). Three layers cooperate, none of them sufficient\n * alone:\n *\n * 1. Per-call cleanup (`engine.ts` finally block invoking\n * `WorktreeHandle.remove()`) — covers the happy path.\n *\n * 2. Session-end signal sweep (this file, registered via\n * `registerExitHandlers`) — covers Ctrl+C, service-manager stop,\n * and (in `github-router claude` mode) the spawned child's exit.\n * Synchronous `execFileSync` is intentional: exit handlers can't\n * reliably await async work.\n *\n * 3. Boot-time PID+instance sweep (`sweepStaleWorktreesAtBoot`) —\n * covers SIGKILL, OOM, container restart. Walks the ledger of\n * repos this proxy has touched and removes worktree dirs whose\n * `<pid>` is dead OR whose `<instance>` UUID doesn't match the\n * current proxy's UUID.\n *\n * Ledger writes are ATOMIC (temp + rename) per peer review — a\n * concurrent-RMW corruption would silently strand worktrees because\n * the boot sweep can't find their repo roots.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { PATHS, writeRuntimeFileSecure } from \"../paths\"\n\n/**\n * Same regex worktree.ts uses for its per-call age sweep — kept in\n * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.\n */\nconst WORKTREE_DIR_NAME_RE =\n /^(\\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/\n\n/**\n * Cap on the ledger: how many repos we remember across boots, and how\n * old an entry may be before it's pruned. Both are belt-and-suspenders\n * — the per-call age sweep is the primary guard against accumulation\n * inside any single repo.\n */\nconst LEDGER_MAX_ENTRIES = 100\nconst LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\nexport interface WorktreeRegistryEntry {\n repoRoot: string\n dir: string\n branch: string\n}\n\n/**\n * Set-like in-memory registry of worktrees this proxy created. Engine\n * passes it to `createWorktree` so per-call cleanup deletes the entry\n * on success; the signal handlers walk what's left at shutdown.\n *\n * Not a bare `Set` because we want to expose only the operations we\n * actually use, and we want a stable testable surface.\n */\nexport class WorktreeRegistry {\n private readonly entries = new Set<WorktreeRegistryEntry>()\n\n add(entry: WorktreeRegistryEntry): void {\n this.entries.add(entry)\n }\n delete(entry: WorktreeRegistryEntry): void {\n this.entries.delete(entry)\n }\n has(entry: WorktreeRegistryEntry): boolean {\n return this.entries.has(entry)\n }\n values(): IterableIterator<WorktreeRegistryEntry> {\n return this.entries.values()\n }\n get size(): number {\n return this.entries.size\n }\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Used in worktree\n * dir/branch names so the boot sweep can reliably distinguish \"this\n * proxy's still-live worktrees\" from \"stranded dirs from a prior\n * proxy that happens to have a recycled PID\" — Docker PID-1 across\n * container restarts is the classic case (peer-review HIGH finding).\n */\nexport function getInstanceUuid(): string {\n if (_instanceUuid === null) {\n _instanceUuid = randomUUID()\n }\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers + sweepRegistry\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _activeRegistry: WorktreeRegistry | null = null\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Synchronous cleanup of every registry entry. Best-effort:\n * `execFileSync` failures are swallowed (the dir may have been\n * removed already, or git may not be on PATH any more in some\n * environments). After a successful removal we drop the entry from\n * the registry so a second call is a true no-op.\n *\n * Synchronous on purpose — exit handlers can't reliably await async\n * work; the process would die before the promise settled.\n */\nexport function sweepRegistry(): void {\n if (!_activeRegistry) return\n // Snapshot the values first so we can mutate the underlying set\n // during iteration without skipping entries.\n const snapshot = [..._activeRegistry.values()]\n for (const entry of snapshot) {\n try {\n // `-C entry.repoRoot` is load-bearing: without it git resolves\n // the worktree path relative to the proxy's cwd (which is the\n // user's launch dir, typically NOT inside the target repo), and\n // fails with `fatal: '<path>' is not a working tree`. The E2E\n // boot-sweep test (worker-agent-boot-sweep.test.ts) is what\n // caught the missing flag.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", entry.dir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // Already gone, EBUSY, or git not on PATH — best effort.\n }\n try {\n execFileSync(\"git\", [\"-C\", entry.repoRoot, \"branch\", \"-D\", entry.branch], {\n stdio: \"ignore\",\n timeout: 5_000,\n windowsHide: true,\n })\n } catch {\n // Same as above.\n }\n _activeRegistry.delete(entry)\n }\n}\n\n/**\n * Windows ConPTY / node-pty signal behavior:\n *\n * When a ConPTY host (VS Code terminal, Windows Terminal, node-pty) closes\n * the pseudo-console, the ConPTY layer sends CTRL_CLOSE_EVENT to the\n * process group. Node.js translates this into SIGINT (NOT SIGTERM). The\n * process has a ~5-second window before forced termination.\n *\n * Implication: the SIGTERM handler below may NEVER fire in node-pty\n * environments. This is by design — the three-layer cleanup architecture\n * ensures coverage:\n * 1. Per-call cleanup (engine.ts finally block) — happy path\n * 2. SIGINT handler (this file) — ConPTY close, Ctrl+C\n * 3. `exit` handler (this file) — unconditional, fires on any exit\n * 4. Boot-time PID+instance sweep (sweepStaleWorktreesAtBoot) — crash recovery\n *\n * Layers 1+2+3 cover ConPTY; layer 4 covers SIGKILL/OOM/container restart.\n */\n\n/**\n * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and\n * remove every entry. Idempotent: subsequent calls swap the registry\n * pointer but do NOT register additional process listeners (otherwise\n * we'd leak listeners on every `runWorkerAgent`).\n *\n * Signal handlers re-raise the signal after sweeping. Naively running\n * the sweep on SIGINT/SIGTERM and returning would *suppress* the\n * signal: Node defaults to terminating the process on these, but only\n * if no user listener is attached. Once we attach a listener, the\n * default action is cancelled and the process keeps running — which\n * means Ctrl-C would clean worktrees but not actually exit, leaving\n * orphan processes in dev. The `process.kill(pid, sig)` re-raise\n * after removing our own listener restores the default behaviour\n * (the second delivery now hits an empty listener list, so Node\n * terminates with the conventional `128 + signum` exit code).\n */\nexport function registerExitHandlers(registry: WorktreeRegistry): void {\n _activeRegistry = registry\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepRegistry()\n _sigintHandler = () => {\n sweepRegistry()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepRegistry()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n // `exit` handlers can only run synchronous code — exactly what\n // sweepRegistry does. Async work here would never complete.\n process.on(\"exit\", _exitHandler)\n}\n\n/**\n * Test-only: unregister the handlers and reset module state. Tests\n * that want to verify `registerExitHandlers` semantics must clean up\n * after themselves or future tests in the same process inherit the\n * (now stale) registry pointer.\n */\nexport function __unregisterExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _activeRegistry = null\n}\n\n// ---------------------------------------------------------------------\n// Ledger: which repos has this proxy touched?\n// ---------------------------------------------------------------------\n\ninterface LedgerEntry {\n repoRoot: string\n lastSeenMs: number\n}\n\ninterface LedgerFile {\n entries: Array<LedgerEntry>\n}\n\nfunction ledgerPath(): string {\n return path.join(PATHS.APP_DIR, \"worker-repos.json\")\n}\n\nasync function readLedger(): Promise<LedgerFile> {\n let raw: string\n try {\n raw = await fs.readFile(ledgerPath(), \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { entries: [] }\n }\n return { entries: [] }\n }\n try {\n const parsed = JSON.parse(raw) as Partial<LedgerFile>\n if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] }\n const cleaned: Array<LedgerEntry> = []\n for (const e of parsed.entries) {\n if (\n e &&\n typeof e === \"object\" &&\n typeof (e as LedgerEntry).repoRoot === \"string\" &&\n typeof (e as LedgerEntry).lastSeenMs === \"number\"\n ) {\n cleaned.push({\n repoRoot: (e as LedgerEntry).repoRoot,\n lastSeenMs: (e as LedgerEntry).lastSeenMs,\n })\n }\n }\n return { entries: cleaned }\n } catch {\n // Corrupted JSON — start fresh rather than crashing the proxy.\n return { entries: [] }\n }\n}\n\n/**\n * Per-process serializer for ledger writes. Multiple concurrent\n * `recordWorkerRepo` calls (legitimate: several workers may start at\n * once) would otherwise race read-modify-write on the JSON file. Each\n * call chains onto the previous so the on-disk sequence is\n * deterministic from this process's perspective.\n *\n * Cross-process safety is provided by the atomic temp+rename below,\n * which makes the final state of the file always be a well-formed\n * full snapshot from ONE writer — never a partial write or\n * interleaved JSON.\n */\nlet _ledgerChain: Promise<void> = Promise.resolve()\n\n/**\n * Append `repoRoot` to the ledger (or update its `lastSeenMs`).\n * Atomic temp+rename per peer review.\n */\nexport function recordWorkerRepo(repoRoot: string): Promise<void> {\n const next = _ledgerChain.then(async () => {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n const current = await readLedger()\n // Dedup: drop any existing entry for this root before appending\n // the fresh one so the array doesn't grow unbounded with repeats.\n const filtered = current.entries.filter((e) => e.repoRoot !== repoRoot)\n filtered.push({ repoRoot, lastSeenMs: Date.now() })\n // Prune by age and cap entry count (newest wins).\n const now = Date.now()\n const pruned = filtered\n .filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS)\n .slice(-LEDGER_MAX_ENTRIES)\n const ledger: LedgerFile = { entries: pruned }\n\n // Atomic temp+rename. The temp filename is unique per call\n // (PID + 8 random hex chars) so concurrent processes don't\n // collide on the temp name; the final `rename` is atomic on\n // POSIX and on Windows (both with same filesystem).\n const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString(\n \"hex\",\n )}`\n try {\n await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2))\n await fs.rename(tmp, ledgerPath())\n } catch (err) {\n // Clean up the temp file if rename failed midway.\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n })\n // Swallow chain-internal errors so one failed write doesn't poison\n // the chain for every subsequent caller. Each call still sees its\n // own rejection (we return `next`, not the catch-handler chain).\n _ledgerChain = next.catch(() => undefined)\n return next\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n // EPERM = process exists but we can't signal it — still alive\n // for our purposes (we just need to know whether to clean up).\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. For every repo we recorded in the ledger,\n * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional\n * location — for repos already inside a worktree, the actual\n * `git-common-dir` may differ, in which case we'll miss this batch\n * and the per-call age sweep will catch them within 7 days) and\n * remove dirs that aren't owned by THIS proxy.\n *\n * Ownership rule: dir is \"ours\" iff its embedded PID is alive AND\n * its embedded UUID equals `getInstanceUuid()`. Either condition\n * failing → remove.\n */\nexport async function sweepStaleWorktreesAtBoot(): Promise<void> {\n const ledger = await readLedger()\n if (ledger.entries.length === 0) return\n const currentUuid = getInstanceUuid()\n for (const entry of ledger.entries) {\n const parent = path.join(entry.repoRoot, \".git\", \"worker-worktrees\")\n let names: Array<string>\n try {\n names = await fs.readdir(parent)\n } catch {\n continue\n }\n for (const name of names) {\n const m = WORKTREE_DIR_NAME_RE.exec(name)\n if (!m) continue\n const pid = Number.parseInt(m[1], 10)\n const uuid = m[2]\n const isOurs = isPidAlive(pid) && uuid === currentUuid\n if (isOurs) continue\n\n const fullDir = path.join(parent, name)\n const branch = `worker/${pid}-${uuid}-${m[3]}`\n try {\n // `-C entry.repoRoot` is load-bearing here too — see the\n // matching comment in `sweepRegistry`. The boot sweep runs\n // BEFORE any worker tool has set cwd, so the proxy's cwd is\n // the user's launch dir, which is almost never inside the\n // target repo.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", fullDir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"branch\", \"-D\", branch],\n { stdio: \"ignore\", timeout: 5_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n await fs.rm(fullDir, { recursive: true, force: true })\n } catch {\n // ignore — git may have removed it already\n }\n }\n }\n}\n\n/** Test-only: clear the ledger file (does NOT remove on-disk worktrees). */\nexport async function __clearLedgerForTests(): Promise<void> {\n await fs.unlink(ledgerPath()).catch(() => {})\n}\n\n/** Test-only: read the ledger as a plain array (no side effects). */\nexport async function __readLedgerForTests(): Promise<Array<LedgerEntry>> {\n return (await readLedger()).entries\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle-DgsJ_8Am.js","names":[],"sources":["../src/lib/worker-agent/lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle plumbing for worker worktrees: in-memory registry, signal\n * handlers, ledger of repos touched, and the boot-time PID+instance\n * safety net.\n *\n * Plan: see `plans/we-have-added-a-dreamy-tide.md` (\"Worktree mode\" →\n * \"Cleanup paths\"). Three layers cooperate, none of them sufficient\n * alone:\n *\n * 1. Per-call cleanup (`engine.ts` finally block invoking\n * `WorktreeHandle.remove()`) — covers the happy path.\n *\n * 2. Session-end signal sweep (this file, registered via\n * `registerExitHandlers`) — covers Ctrl+C, service-manager stop,\n * and (in `github-router claude` mode) the spawned child's exit.\n * Synchronous `execFileSync` is intentional: exit handlers can't\n * reliably await async work.\n *\n * 3. Boot-time PID+instance sweep (`sweepStaleWorktreesAtBoot`) —\n * covers SIGKILL, OOM, container restart. Walks the ledger of\n * repos this proxy has touched and removes worktree dirs whose\n * `<pid>` is dead OR whose `<instance>` UUID doesn't match the\n * current proxy's UUID.\n *\n * Ledger writes are ATOMIC (temp + rename) per peer review — a\n * concurrent-RMW corruption would silently strand worktrees because\n * the boot sweep can't find their repo roots.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { PATHS, writeRuntimeFileSecure } from \"../paths\"\n\n/**\n * Same regex worktree.ts uses for its per-call age sweep — kept in\n * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.\n */\nconst WORKTREE_DIR_NAME_RE =\n /^(\\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/\n\n/**\n * Cap on the ledger: how many repos we remember across boots, and how\n * old an entry may be before it's pruned. Both are belt-and-suspenders\n * — the per-call age sweep is the primary guard against accumulation\n * inside any single repo.\n */\nconst LEDGER_MAX_ENTRIES = 100\nconst LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\nexport interface WorktreeRegistryEntry {\n repoRoot: string\n dir: string\n branch: string\n}\n\n/**\n * Set-like in-memory registry of worktrees this proxy created. Engine\n * passes it to `createWorktree` so per-call cleanup deletes the entry\n * on success; the signal handlers walk what's left at shutdown.\n *\n * Not a bare `Set` because we want to expose only the operations we\n * actually use, and we want a stable testable surface.\n */\nexport class WorktreeRegistry {\n private readonly entries = new Set<WorktreeRegistryEntry>()\n\n add(entry: WorktreeRegistryEntry): void {\n this.entries.add(entry)\n }\n delete(entry: WorktreeRegistryEntry): void {\n this.entries.delete(entry)\n }\n has(entry: WorktreeRegistryEntry): boolean {\n return this.entries.has(entry)\n }\n values(): IterableIterator<WorktreeRegistryEntry> {\n return this.entries.values()\n }\n get size(): number {\n return this.entries.size\n }\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Used in worktree\n * dir/branch names so the boot sweep can reliably distinguish \"this\n * proxy's still-live worktrees\" from \"stranded dirs from a prior\n * proxy that happens to have a recycled PID\" — Docker PID-1 across\n * container restarts is the classic case (peer-review HIGH finding).\n */\nexport function getInstanceUuid(): string {\n if (_instanceUuid === null) {\n _instanceUuid = randomUUID()\n }\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers + sweepRegistry\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _activeRegistry: WorktreeRegistry | null = null\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Synchronous cleanup of every registry entry. Best-effort:\n * `execFileSync` failures are swallowed (the dir may have been\n * removed already, or git may not be on PATH any more in some\n * environments). After a successful removal we drop the entry from\n * the registry so a second call is a true no-op.\n *\n * Synchronous on purpose — exit handlers can't reliably await async\n * work; the process would die before the promise settled.\n */\nexport function sweepRegistry(): void {\n if (!_activeRegistry) return\n // Snapshot the values first so we can mutate the underlying set\n // during iteration without skipping entries.\n const snapshot = [..._activeRegistry.values()]\n for (const entry of snapshot) {\n try {\n // `-C entry.repoRoot` is load-bearing: without it git resolves\n // the worktree path relative to the proxy's cwd (which is the\n // user's launch dir, typically NOT inside the target repo), and\n // fails with `fatal: '<path>' is not a working tree`. The E2E\n // boot-sweep test (worker-agent-boot-sweep.test.ts) is what\n // caught the missing flag.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", entry.dir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // Already gone, EBUSY, or git not on PATH — best effort.\n }\n try {\n execFileSync(\"git\", [\"-C\", entry.repoRoot, \"branch\", \"-D\", entry.branch], {\n stdio: \"ignore\",\n timeout: 5_000,\n windowsHide: true,\n })\n } catch {\n // Same as above.\n }\n _activeRegistry.delete(entry)\n }\n}\n\n/**\n * Windows ConPTY / node-pty signal behavior:\n *\n * When a ConPTY host (VS Code terminal, Windows Terminal, node-pty) closes\n * the pseudo-console, the ConPTY layer sends CTRL_CLOSE_EVENT to the\n * process group. Node.js translates this into SIGINT (NOT SIGTERM). The\n * process has a ~5-second window before forced termination.\n *\n * Implication: the SIGTERM handler below may NEVER fire in node-pty\n * environments. This is by design — the three-layer cleanup architecture\n * ensures coverage:\n * 1. Per-call cleanup (engine.ts finally block) — happy path\n * 2. SIGINT handler (this file) — ConPTY close, Ctrl+C\n * 3. `exit` handler (this file) — unconditional, fires on any exit\n * 4. Boot-time PID+instance sweep (sweepStaleWorktreesAtBoot) — crash recovery\n *\n * Layers 1+2+3 cover ConPTY; layer 4 covers SIGKILL/OOM/container restart.\n */\n\n/**\n * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and\n * remove every entry. Idempotent: subsequent calls swap the registry\n * pointer but do NOT register additional process listeners (otherwise\n * we'd leak listeners on every `runWorkerAgent`).\n *\n * Signal handlers re-raise the signal after sweeping. Naively running\n * the sweep on SIGINT/SIGTERM and returning would *suppress* the\n * signal: Node defaults to terminating the process on these, but only\n * if no user listener is attached. Once we attach a listener, the\n * default action is cancelled and the process keeps running — which\n * means Ctrl-C would clean worktrees but not actually exit, leaving\n * orphan processes in dev. The `process.kill(pid, sig)` re-raise\n * after removing our own listener restores the default behaviour\n * (the second delivery now hits an empty listener list, so Node\n * terminates with the conventional `128 + signum` exit code).\n */\nexport function registerExitHandlers(registry: WorktreeRegistry): void {\n _activeRegistry = registry\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepRegistry()\n _sigintHandler = () => {\n sweepRegistry()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepRegistry()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n // `exit` handlers can only run synchronous code — exactly what\n // sweepRegistry does. Async work here would never complete.\n process.on(\"exit\", _exitHandler)\n}\n\n/**\n * Test-only: unregister the handlers and reset module state. Tests\n * that want to verify `registerExitHandlers` semantics must clean up\n * after themselves or future tests in the same process inherit the\n * (now stale) registry pointer.\n */\nexport function __unregisterExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _activeRegistry = null\n}\n\n// ---------------------------------------------------------------------\n// Ledger: which repos has this proxy touched?\n// ---------------------------------------------------------------------\n\ninterface LedgerEntry {\n repoRoot: string\n lastSeenMs: number\n}\n\ninterface LedgerFile {\n entries: Array<LedgerEntry>\n}\n\nfunction ledgerPath(): string {\n return path.join(PATHS.APP_DIR, \"worker-repos.json\")\n}\n\nasync function readLedger(): Promise<LedgerFile> {\n let raw: string\n try {\n raw = await fs.readFile(ledgerPath(), \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { entries: [] }\n }\n return { entries: [] }\n }\n try {\n const parsed = JSON.parse(raw) as Partial<LedgerFile>\n if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] }\n const cleaned: Array<LedgerEntry> = []\n for (const e of parsed.entries) {\n if (\n e &&\n typeof e === \"object\" &&\n typeof (e as LedgerEntry).repoRoot === \"string\" &&\n typeof (e as LedgerEntry).lastSeenMs === \"number\"\n ) {\n cleaned.push({\n repoRoot: (e as LedgerEntry).repoRoot,\n lastSeenMs: (e as LedgerEntry).lastSeenMs,\n })\n }\n }\n return { entries: cleaned }\n } catch {\n // Corrupted JSON — start fresh rather than crashing the proxy.\n return { entries: [] }\n }\n}\n\n/**\n * Per-process serializer for ledger writes. Multiple concurrent\n * `recordWorkerRepo` calls (legitimate: several workers may start at\n * once) would otherwise race read-modify-write on the JSON file. Each\n * call chains onto the previous so the on-disk sequence is\n * deterministic from this process's perspective.\n *\n * Cross-process safety is provided by the atomic temp+rename below,\n * which makes the final state of the file always be a well-formed\n * full snapshot from ONE writer — never a partial write or\n * interleaved JSON.\n */\nlet _ledgerChain: Promise<void> = Promise.resolve()\n\n/**\n * Append `repoRoot` to the ledger (or update its `lastSeenMs`).\n * Atomic temp+rename per peer review.\n */\nexport function recordWorkerRepo(repoRoot: string): Promise<void> {\n const next = _ledgerChain.then(async () => {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n const current = await readLedger()\n // Dedup: drop any existing entry for this root before appending\n // the fresh one so the array doesn't grow unbounded with repeats.\n const filtered = current.entries.filter((e) => e.repoRoot !== repoRoot)\n filtered.push({ repoRoot, lastSeenMs: Date.now() })\n // Prune by age and cap entry count (newest wins).\n const now = Date.now()\n const pruned = filtered\n .filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS)\n .slice(-LEDGER_MAX_ENTRIES)\n const ledger: LedgerFile = { entries: pruned }\n\n // Atomic temp+rename. The temp filename is unique per call\n // (PID + 8 random hex chars) so concurrent processes don't\n // collide on the temp name; the final `rename` is atomic on\n // POSIX and on Windows (both with same filesystem).\n const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString(\n \"hex\",\n )}`\n try {\n await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2))\n await fs.rename(tmp, ledgerPath())\n } catch (err) {\n // Clean up the temp file if rename failed midway.\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n })\n // Swallow chain-internal errors so one failed write doesn't poison\n // the chain for every subsequent caller. Each call still sees its\n // own rejection (we return `next`, not the catch-handler chain).\n _ledgerChain = next.catch(() => undefined)\n return next\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n // EPERM = process exists but we can't signal it — still alive\n // for our purposes (we just need to know whether to clean up).\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. For every repo we recorded in the ledger,\n * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional\n * location — for repos already inside a worktree, the actual\n * `git-common-dir` may differ, in which case we'll miss this batch\n * and the per-call age sweep will catch them within 7 days) and\n * remove dirs that aren't owned by THIS proxy.\n *\n * Ownership rule: dir is \"ours\" iff its embedded PID is alive AND\n * its embedded UUID equals `getInstanceUuid()`. Either condition\n * failing → remove.\n */\nexport async function sweepStaleWorktreesAtBoot(): Promise<void> {\n const ledger = await readLedger()\n if (ledger.entries.length === 0) return\n const currentUuid = getInstanceUuid()\n for (const entry of ledger.entries) {\n const parent = path.join(entry.repoRoot, \".git\", \"worker-worktrees\")\n let names: Array<string>\n try {\n names = await fs.readdir(parent)\n } catch {\n continue\n }\n for (const name of names) {\n const m = WORKTREE_DIR_NAME_RE.exec(name)\n if (!m) continue\n const pid = Number.parseInt(m[1], 10)\n const uuid = m[2]\n const isOurs = isPidAlive(pid) && uuid === currentUuid\n if (isOurs) continue\n\n const fullDir = path.join(parent, name)\n const branch = `worker/${pid}-${uuid}-${m[3]}`\n try {\n // `-C entry.repoRoot` is load-bearing here too — see the\n // matching comment in `sweepRegistry`. The boot sweep runs\n // BEFORE any worker tool has set cwd, so the proxy's cwd is\n // the user's launch dir, which is almost never inside the\n // target repo.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", fullDir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"branch\", \"-D\", branch],\n { stdio: \"ignore\", timeout: 5_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n await fs.rm(fullDir, { recursive: true, force: true })\n } catch {\n // ignore — git may have removed it already\n }\n }\n }\n}\n\n/** Test-only: clear the ledger file (does NOT remove on-disk worktrees). */\nexport async function __clearLedgerForTests(): Promise<void> {\n await fs.unlink(ledgerPath()).catch(() => {})\n}\n\n/** Test-only: read the ledger as a plain array (no side effects). */\nexport async function __readLedgerForTests(): Promise<Array<LedgerEntry>> {\n return (await readLedger()).entries\n}\n"],"mappings":"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"}
|
package/dist/main.js
CHANGED
|
@@ -46,24 +46,25 @@ const main = defineCommand({
|
|
|
46
46
|
description: "A reverse proxy that exposes GitHub Copilot as OpenAI and Anthropic compatible API endpoints."
|
|
47
47
|
},
|
|
48
48
|
subCommands: {
|
|
49
|
-
auth: () => import("./auth-
|
|
50
|
-
start: () => import("./start-
|
|
51
|
-
claude: () => import("./claude-
|
|
52
|
-
codex: () => import("./codex-
|
|
53
|
-
serve: () => import("./serve-
|
|
54
|
-
models: () => import("./models-
|
|
55
|
-
"check-usage": () => import("./check-usage-
|
|
56
|
-
debug: () => import("./debug-
|
|
57
|
-
"internal-stop-hook": () => import("./internal-stop-hook-
|
|
58
|
-
"internal-prompt-submit": () => import("./internal-prompt-submit-
|
|
59
|
-
"internal-stop-review": () => import("./internal-stop-review-
|
|
60
|
-
"internal-plan-review": () => import("./internal-plan-review-
|
|
61
|
-
"internal-session-bind": () => import("./internal-session-bind-
|
|
62
|
-
"internal-workspace-header": () => import("./internal-workspace-header-
|
|
63
|
-
"internal-artifact-open": () => import("./internal-artifact-open-
|
|
64
|
-
"internal-first-mate-guard": () => import("./internal-first-mate-guard-
|
|
65
|
-
"internal-worker-guard": () => import("./internal-worker-guard-
|
|
66
|
-
"internal-fast-dispatch-guard": () => import("./internal-fast-dispatch-guard-
|
|
49
|
+
auth: () => import("./auth-CpjOjoUG.js").then((m) => m.auth),
|
|
50
|
+
start: () => import("./start-DHRToEyb.js").then((m) => m.start),
|
|
51
|
+
claude: () => import("./claude-kr-Augld.js").then((m) => m.claude),
|
|
52
|
+
codex: () => import("./codex-B1qnVlzG.js").then((m) => m.codex),
|
|
53
|
+
serve: () => import("./serve-T64kVeQN.js").then((m) => m.serve),
|
|
54
|
+
models: () => import("./models-1KwEUgzd.js").then((m) => m.models),
|
|
55
|
+
"check-usage": () => import("./check-usage-BsN3S4Fu.js").then((m) => m.checkUsage),
|
|
56
|
+
debug: () => import("./debug-BXRPyeqO.js").then((m) => m.debug),
|
|
57
|
+
"internal-stop-hook": () => import("./internal-stop-hook-CWffknT2.js").then((m) => m.internalStopHook),
|
|
58
|
+
"internal-prompt-submit": () => import("./internal-prompt-submit-BTqRn4FW.js").then((m) => m.internalPromptSubmit),
|
|
59
|
+
"internal-stop-review": () => import("./internal-stop-review-B8MmEyQ1.js").then((m) => m.internalStopReview),
|
|
60
|
+
"internal-plan-review": () => import("./internal-plan-review-DVY-iV2x.js").then((m) => m.internalPlanReview),
|
|
61
|
+
"internal-session-bind": () => import("./internal-session-bind-YfH656lt.js").then((m) => m.internalSessionBind),
|
|
62
|
+
"internal-workspace-header": () => import("./internal-workspace-header-Cep0WSIb.js").then((m) => m.internalWorkspaceHeader),
|
|
63
|
+
"internal-artifact-open": () => import("./internal-artifact-open-DRSxShAW.js").then((m) => m.internalArtifactOpen),
|
|
64
|
+
"internal-first-mate-guard": () => import("./internal-first-mate-guard-Brh_uois.js").then((m) => m.internalFirstMateGuard),
|
|
65
|
+
"internal-worker-guard": () => import("./internal-worker-guard-Dr6W83TC.js").then((m) => m.internalWorkerGuard),
|
|
66
|
+
"internal-fast-dispatch-guard": () => import("./internal-fast-dispatch-guard-C6BNoOnn.js").then((m) => m.internalFastDispatchGuard),
|
|
67
|
+
"internal-max-dispatch-guard": () => import("./internal-max-dispatch-guard-k52H-PdC.js").then((m) => m.internalMaxDispatchGuard)
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
/**
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","names":[],"sources":["../src/main.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { defineCommand, runMain, showUsage } from \"citty\"\nimport consola from \"consola\"\n\nimport { getPackageVersion } from \"./lib/version\"\n\nprocess.on(\"unhandledRejection\", (error) => {\n consola.error(\"Unhandled rejection:\", error)\n})\n\nprocess.on(\"uncaughtException\", (error) => {\n consola.error(\"Uncaught exception:\", error)\n process.exit(1)\n})\n\nconst version = getPackageVersion()\n\n// Always surface the running version on stderr so crash reports\n// identify which build is in use. Suppressed only for `--version`\n// (citty's built-in handler prints just the bare version number, and\n// a banner above it is redundant). `-v` is NOT special-cased: citty\n// doesn't treat it as `--version`, so users typing `-v` still get\n// the banner — which is the closest thing to \"show me the version\".\nconst argv = process.argv.slice(2)\nconst isVersionFlag = argv.includes(\"--version\")\n// Suppress the banner for the internal hooks: their stdout/stderr is consumed by\n// Claude Code (the Stop hook's stderr is the block message; the prompt-submit\n// hook's stdout is injected context), so it must stay clean. `internal-stop-review`\n// and `internal-plan-review` are detached/background with no consumer, but stay\n// quiet for the same reason.\nconst isInternalHook =\n argv[0] === \"internal-stop-hook\"\n || argv[0] === \"internal-prompt-submit\"\n || argv[0] === \"internal-stop-review\"\n || argv[0] === \"internal-plan-review\"\n || argv[0] === \"internal-session-bind\"\n || argv[0] === \"internal-workspace-header\"\n || argv[0] === \"internal-artifact-open\"\n || argv[0] === \"internal-first-mate-guard\"\n || argv[0] === \"internal-worker-guard\"\n || argv[0] === \"internal-fast-dispatch-guard\"\nif (!isVersionFlag && !isInternalHook) {\n consola.info(`github-router v${version}`)\n}\n\n/**\n * Subcommands are LAZY THUNKS, not static imports.\n *\n * citty resolves only the named entry (`_findSubCommand` → `resolveValue`), so\n * running one subcommand no longer evaluates the other 17 module graphs. That\n * matters because several of these are per-turn Claude Code hooks — the\n * `UserPromptSubmit`, `Stop` and `PreToolUse` handlers registered by\n * `src/claude.ts` each spawn a FRESH `node dist/main.js <sub>` process, so any\n * import-time cost lands in the user's interactive loop on every prompt.\n * `internal-worker-guard`, for instance, needs three modules but was loading\n * the entire claude/serve/MCP graph plus the Pi vendor runtime.\n *\n * Safe because the import-time side effects in this codebase\n * (`assertMcpToolSurfaceConsistent` in `server.ts`, the tree-sitter grammar\n * preload, the worktree/session exit-handler registrations, the first-mate\n * allowlist assertion) are reachable ONLY from claude/codex/serve/start, and\n * all four reach all of them independently — no subcommand relies on another's\n * module being loaded. Verified by an AST walk of the runtime import graph.\n *\n * `--help` is the one path that still resolves everything: citty's\n * `renderUsage` reads each subcommand's meta to print descriptions. That is\n * not latency-critical, and `resolveHelpTarget` below resolves only the single\n * subcommand when help is scoped to one.\n */\nconst main = defineCommand({\n meta: {\n name: \"github-router\",\n version,\n description:\n \"A reverse proxy that exposes GitHub Copilot as OpenAI and Anthropic compatible API endpoints.\",\n },\n subCommands: {\n auth: () => import(\"./auth\").then((m) => m.auth),\n start: () => import(\"./start\").then((m) => m.start),\n claude: () => import(\"./claude\").then((m) => m.claude),\n codex: () => import(\"./codex\").then((m) => m.codex),\n serve: () => import(\"./serve\").then((m) => m.serve),\n models: () => import(\"./models\").then((m) => m.models),\n \"check-usage\": () => import(\"./check-usage\").then((m) => m.checkUsage),\n debug: () => import(\"./debug\").then((m) => m.debug),\n \"internal-stop-hook\": () =>\n import(\"./internal-stop-hook\").then((m) => m.internalStopHook),\n \"internal-prompt-submit\": () =>\n import(\"./internal-prompt-submit\").then((m) => m.internalPromptSubmit),\n \"internal-stop-review\": () =>\n import(\"./internal-stop-review\").then((m) => m.internalStopReview),\n \"internal-plan-review\": () =>\n import(\"./internal-plan-review\").then((m) => m.internalPlanReview),\n \"internal-session-bind\": () =>\n import(\"./internal-session-bind\").then((m) => m.internalSessionBind),\n \"internal-workspace-header\": () =>\n import(\"./internal-workspace-header\").then(\n (m) => m.internalWorkspaceHeader,\n ),\n \"internal-artifact-open\": () =>\n import(\"./internal-artifact-open\").then((m) => m.internalArtifactOpen),\n \"internal-first-mate-guard\": () =>\n import(\"./internal-first-mate-guard\").then(\n (m) => m.internalFirstMateGuard,\n ),\n \"internal-worker-guard\": () =>\n import(\"./internal-worker-guard\").then((m) => m.internalWorkerGuard),\n \"internal-fast-dispatch-guard\": () =>\n import(\"./internal-fast-dispatch-guard\").then((m) => m.internalFastDispatchGuard),\n },\n})\n\n/**\n * Print usage ourselves and let the process DRAIN, instead of letting citty\n * hard-exit while stdout is still queued.\n *\n * citty's `runMain` calls `process.exit(0)` immediately after `showUsage`\n * resolves (citty 0.2.2 dist/index.mjs:389; 0.1.6 does the same, so this is\n * not an upgrade regression). On Windows a PIPE-backed stdout is async, so the\n * usage text is still in a libuv handle when the hard exit tears the loop down\n * and node aborts:\n * `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\\win\\async.c`\n * — after printing the help the user asked for.\n *\n * `--version` is unaffected: citty's version branch has no `process.exit()`\n * and drains naturally. That asymmetry is why this survived so long — the\n * cheapest smoke test passes while every `--help` path aborts. Node 22 on\n * Windows tolerated it; node 24 does not, and release.yml publishes with\n * node 24.\n *\n * Forcing `_handle.setBlocking(true)` first was tried and does NOT fix it —\n * the abort still reproduces through a pipe. The only reliable fix is to not\n * hard-exit at all, which is the same resolution the internal hooks already\n * use (`process.exitCode = 0`, never `process.exit()`), guarded by the tests\n * in this repo's internal-hooks-crash suite.\n *\n * Only the help path is intercepted; everything else still goes through citty\n * so argument parsing, subcommand dispatch and error handling stay citty's.\n */\nconst HELP_FLAGS = new Set([\"--help\", \"-h\"])\n\nasync function resolveHelpTarget(): Promise<{\n cmd: Parameters<typeof showUsage>[0]\n parent?: Parameters<typeof showUsage>[1]\n}> {\n // First non-flag token is the subcommand, matching citty's own resolution\n // for our single-level command tree.\n const name = argv.find((arg) => !arg.startsWith(\"-\"))\n const table = main.subCommands as Record<string, unknown> | undefined\n const entry = name ? table?.[name] : undefined\n if (!entry) return { cmd: main }\n // citty allows a subcommand to be a value, a promise, or a thunk.\n const resolved = typeof entry === \"function\" ? await entry() : await entry\n return {\n cmd: resolved as Parameters<typeof showUsage>[0],\n parent: main as Parameters<typeof showUsage>[1],\n }\n}\n\nif (argv.some((arg) => HELP_FLAGS.has(arg))) {\n const { cmd, parent } = await resolveHelpTarget()\n await showUsage(cmd, parent)\n // Signal success WITHOUT tearing the loop down — node drains stdout and\n // exits 0 on its own.\n process.exitCode = 0\n} else {\n await runMain(main)\n}\n"],"mappings":";;;;;AAOA,QAAQ,GAAG,uBAAuB,UAAU;CAC1C,QAAQ,MAAM,wBAAwB,KAAK;AAC7C,CAAC;AAED,QAAQ,GAAG,sBAAsB,UAAU;CACzC,QAAQ,MAAM,uBAAuB,KAAK;CAC1C,QAAQ,KAAK,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,kBAAkB;AAQlC,MAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,MAAM,gBAAgB,KAAK,SAAS,WAAW;AAM/C,MAAM,iBACJ,KAAK,OAAO,wBACT,KAAK,OAAO,4BACZ,KAAK,OAAO,0BACZ,KAAK,OAAO,0BACZ,KAAK,OAAO,2BACZ,KAAK,OAAO,+BACZ,KAAK,OAAO,4BACZ,KAAK,OAAO,+BACZ,KAAK,OAAO,2BACZ,KAAK,OAAO;AACjB,IAAI,CAAC,iBAAiB,CAAC,gBACrB,QAAQ,KAAK,kBAAkB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;AA2B1C,MAAM,OAAO,cAAc;CACzB,MAAM;EACJ,MAAM;EACN;EACA,aACE;CACJ;CACA,aAAa;EACX,YAAY,OAAO,qBAAS,CAAC,MAAM,MAAM,EAAE,IAAI;EAC/C,aAAa,OAAO,sBAAU,CAAC,MAAM,MAAM,EAAE,KAAK;EAClD,cAAc,OAAO,uBAAW,CAAC,MAAM,MAAM,EAAE,MAAM;EACrD,aAAa,OAAO,sBAAU,CAAC,MAAM,MAAM,EAAE,KAAK;EAClD,aAAa,OAAO,sBAAU,CAAC,MAAM,MAAM,EAAE,KAAK;EAClD,cAAc,OAAO,uBAAW,CAAC,MAAM,MAAM,EAAE,MAAM;EACrD,qBAAqB,OAAO,4BAAgB,CAAC,MAAM,MAAM,EAAE,UAAU;EACrE,aAAa,OAAO,sBAAU,CAAC,MAAM,MAAM,EAAE,KAAK;EAClD,4BACE,OAAO,mCAAuB,CAAC,MAAM,MAAM,EAAE,gBAAgB;EAC/D,gCACE,OAAO,uCAA2B,CAAC,MAAM,MAAM,EAAE,oBAAoB;EACvE,8BACE,OAAO,qCAAyB,CAAC,MAAM,MAAM,EAAE,kBAAkB;EACnE,8BACE,OAAO,qCAAyB,CAAC,MAAM,MAAM,EAAE,kBAAkB;EACnE,+BACE,OAAO,sCAA0B,CAAC,MAAM,MAAM,EAAE,mBAAmB;EACrE,mCACE,OAAO,0CAA8B,CAAC,MACnC,MAAM,EAAE,uBACX;EACF,gCACE,OAAO,uCAA2B,CAAC,MAAM,MAAM,EAAE,oBAAoB;EACvE,mCACE,OAAO,0CAA8B,CAAC,MACnC,MAAM,EAAE,sBACX;EACF,+BACE,OAAO,sCAA0B,CAAC,MAAM,MAAM,EAAE,mBAAmB;EACrE,sCACE,OAAO,6CAAiC,CAAC,MAAM,MAAM,EAAE,yBAAyB;CACpF;AACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BD,MAAM,6BAAa,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC;AAE3C,eAAe,oBAGZ;CAGD,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC,IAAI,WAAW,GAAG,CAAC;CACpD,MAAM,QAAQ,KAAK;CACnB,MAAM,QAAQ,OAAO,QAAQ,QAAQ,KAAA;CACrC,IAAI,CAAC,OAAO,OAAO,EAAE,KAAK,KAAK;CAG/B,OAAO;EACL,KAFe,OAAO,UAAU,aAAa,MAAM,MAAM,IAAI,MAAM;EAGnE,QAAQ;CACV;AACF;AAEA,IAAI,KAAK,MAAM,QAAQ,WAAW,IAAI,GAAG,CAAC,GAAG;CAC3C,MAAM,EAAE,KAAK,WAAW,MAAM,kBAAkB;CAChD,MAAM,UAAU,KAAK,MAAM;CAG3B,QAAQ,WAAW;AACrB,OACE,MAAM,QAAQ,IAAI"}
|
|
1
|
+
{"version":3,"file":"main.js","names":[],"sources":["../src/main.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { defineCommand, runMain, showUsage } from \"citty\"\nimport consola from \"consola\"\n\nimport { getPackageVersion } from \"./lib/version\"\n\nprocess.on(\"unhandledRejection\", (error) => {\n consola.error(\"Unhandled rejection:\", error)\n})\n\nprocess.on(\"uncaughtException\", (error) => {\n consola.error(\"Uncaught exception:\", error)\n process.exit(1)\n})\n\nconst version = getPackageVersion()\n\n// Always surface the running version on stderr so crash reports\n// identify which build is in use. Suppressed only for `--version`\n// (citty's built-in handler prints just the bare version number, and\n// a banner above it is redundant). `-v` is NOT special-cased: citty\n// doesn't treat it as `--version`, so users typing `-v` still get\n// the banner — which is the closest thing to \"show me the version\".\nconst argv = process.argv.slice(2)\nconst isVersionFlag = argv.includes(\"--version\")\n// Suppress the banner for the internal hooks: their stdout/stderr is consumed by\n// Claude Code (the Stop hook's stderr is the block message; the prompt-submit\n// hook's stdout is injected context), so it must stay clean. `internal-stop-review`\n// and `internal-plan-review` are detached/background with no consumer, but stay\n// quiet for the same reason.\nconst isInternalHook =\n argv[0] === \"internal-stop-hook\"\n || argv[0] === \"internal-prompt-submit\"\n || argv[0] === \"internal-stop-review\"\n || argv[0] === \"internal-plan-review\"\n || argv[0] === \"internal-session-bind\"\n || argv[0] === \"internal-workspace-header\"\n || argv[0] === \"internal-artifact-open\"\n || argv[0] === \"internal-first-mate-guard\"\n || argv[0] === \"internal-worker-guard\"\n || argv[0] === \"internal-fast-dispatch-guard\"\nif (!isVersionFlag && !isInternalHook) {\n consola.info(`github-router v${version}`)\n}\n\n/**\n * Subcommands are LAZY THUNKS, not static imports.\n *\n * citty resolves only the named entry (`_findSubCommand` → `resolveValue`), so\n * running one subcommand no longer evaluates the other 17 module graphs. That\n * matters because several of these are per-turn Claude Code hooks — the\n * `UserPromptSubmit`, `Stop` and `PreToolUse` handlers registered by\n * `src/claude.ts` each spawn a FRESH `node dist/main.js <sub>` process, so any\n * import-time cost lands in the user's interactive loop on every prompt.\n * `internal-worker-guard`, for instance, needs three modules but was loading\n * the entire claude/serve/MCP graph plus the Pi vendor runtime.\n *\n * Safe because the import-time side effects in this codebase\n * (`assertMcpToolSurfaceConsistent` in `server.ts`, the tree-sitter grammar\n * preload, the worktree/session exit-handler registrations, the first-mate\n * allowlist assertion) are reachable ONLY from claude/codex/serve/start, and\n * all four reach all of them independently — no subcommand relies on another's\n * module being loaded. Verified by an AST walk of the runtime import graph.\n *\n * `--help` is the one path that still resolves everything: citty's\n * `renderUsage` reads each subcommand's meta to print descriptions. That is\n * not latency-critical, and `resolveHelpTarget` below resolves only the single\n * subcommand when help is scoped to one.\n */\nconst main = defineCommand({\n meta: {\n name: \"github-router\",\n version,\n description:\n \"A reverse proxy that exposes GitHub Copilot as OpenAI and Anthropic compatible API endpoints.\",\n },\n subCommands: {\n auth: () => import(\"./auth\").then((m) => m.auth),\n start: () => import(\"./start\").then((m) => m.start),\n claude: () => import(\"./claude\").then((m) => m.claude),\n codex: () => import(\"./codex\").then((m) => m.codex),\n serve: () => import(\"./serve\").then((m) => m.serve),\n models: () => import(\"./models\").then((m) => m.models),\n \"check-usage\": () => import(\"./check-usage\").then((m) => m.checkUsage),\n debug: () => import(\"./debug\").then((m) => m.debug),\n \"internal-stop-hook\": () =>\n import(\"./internal-stop-hook\").then((m) => m.internalStopHook),\n \"internal-prompt-submit\": () =>\n import(\"./internal-prompt-submit\").then((m) => m.internalPromptSubmit),\n \"internal-stop-review\": () =>\n import(\"./internal-stop-review\").then((m) => m.internalStopReview),\n \"internal-plan-review\": () =>\n import(\"./internal-plan-review\").then((m) => m.internalPlanReview),\n \"internal-session-bind\": () =>\n import(\"./internal-session-bind\").then((m) => m.internalSessionBind),\n \"internal-workspace-header\": () =>\n import(\"./internal-workspace-header\").then(\n (m) => m.internalWorkspaceHeader,\n ),\n \"internal-artifact-open\": () =>\n import(\"./internal-artifact-open\").then((m) => m.internalArtifactOpen),\n \"internal-first-mate-guard\": () =>\n import(\"./internal-first-mate-guard\").then(\n (m) => m.internalFirstMateGuard,\n ),\n \"internal-worker-guard\": () =>\n import(\"./internal-worker-guard\").then((m) => m.internalWorkerGuard),\n \"internal-fast-dispatch-guard\": () =>\n import(\"./internal-fast-dispatch-guard\").then((m) => m.internalFastDispatchGuard),\n \"internal-max-dispatch-guard\": () =>\n import(\"./internal-max-dispatch-guard\").then((m) => m.internalMaxDispatchGuard),\n },\n})\n\n/**\n * Print usage ourselves and let the process DRAIN, instead of letting citty\n * hard-exit while stdout is still queued.\n *\n * citty's `runMain` calls `process.exit(0)` immediately after `showUsage`\n * resolves (citty 0.2.2 dist/index.mjs:389; 0.1.6 does the same, so this is\n * not an upgrade regression). On Windows a PIPE-backed stdout is async, so the\n * usage text is still in a libuv handle when the hard exit tears the loop down\n * and node aborts:\n * `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\\win\\async.c`\n * — after printing the help the user asked for.\n *\n * `--version` is unaffected: citty's version branch has no `process.exit()`\n * and drains naturally. That asymmetry is why this survived so long — the\n * cheapest smoke test passes while every `--help` path aborts. Node 22 on\n * Windows tolerated it; node 24 does not, and release.yml publishes with\n * node 24.\n *\n * Forcing `_handle.setBlocking(true)` first was tried and does NOT fix it —\n * the abort still reproduces through a pipe. The only reliable fix is to not\n * hard-exit at all, which is the same resolution the internal hooks already\n * use (`process.exitCode = 0`, never `process.exit()`), guarded by the tests\n * in this repo's internal-hooks-crash suite.\n *\n * Only the help path is intercepted; everything else still goes through citty\n * so argument parsing, subcommand dispatch and error handling stay citty's.\n */\nconst HELP_FLAGS = new Set([\"--help\", \"-h\"])\n\nasync function resolveHelpTarget(): Promise<{\n cmd: Parameters<typeof showUsage>[0]\n parent?: Parameters<typeof showUsage>[1]\n}> {\n // First non-flag token is the subcommand, matching citty's own resolution\n // for our single-level command tree.\n const name = argv.find((arg) => !arg.startsWith(\"-\"))\n const table = main.subCommands as Record<string, unknown> | undefined\n const entry = name ? table?.[name] : undefined\n if (!entry) return { cmd: main }\n // citty allows a subcommand to be a value, a promise, or a thunk.\n const resolved = typeof entry === \"function\" ? await entry() : await entry\n return {\n cmd: resolved as Parameters<typeof showUsage>[0],\n parent: main as Parameters<typeof showUsage>[1],\n }\n}\n\nif (argv.some((arg) => HELP_FLAGS.has(arg))) {\n const { cmd, parent } = await resolveHelpTarget()\n await showUsage(cmd, parent)\n // Signal success WITHOUT tearing the loop down — node drains stdout and\n // exits 0 on its own.\n process.exitCode = 0\n} else {\n await runMain(main)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { t as state } from "./state-B8adscCv.js";
|
|
2
|
+
//#region src/lib/model-suffix.ts
|
|
3
|
+
/** Strip every trailing `[1m]` marker, case-insensitively. */
|
|
4
|
+
function stripTrailingOneMSuffix(id) {
|
|
5
|
+
const match = id.match(/(?:\[1m\])+$/i);
|
|
6
|
+
if (!match || match.index === void 0) return {
|
|
7
|
+
base: id,
|
|
8
|
+
hadSuffix: false
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
base: id.slice(0, match.index),
|
|
12
|
+
hadSuffix: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Reapply at most one canonical `[1m]` marker when the input had one. */
|
|
16
|
+
function normalizeTrailingOneMSuffix(id) {
|
|
17
|
+
const { base, hadSuffix } = stripTrailingOneMSuffix(id);
|
|
18
|
+
return hadSuffix ? `${base}[1m]` : base;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/lib/fast-endpoint.ts
|
|
22
|
+
const ENDPOINTS = {
|
|
23
|
+
chat: /* @__PURE__ */ new Set(["/chat/completions", "/v1/chat/completions"]),
|
|
24
|
+
responses: /* @__PURE__ */ new Set(["/responses", "/v1/responses"]),
|
|
25
|
+
messages: /* @__PURE__ */ new Set(["/messages", "/v1/messages"])
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Return the endpoint mandated by a fast-profile roster model.
|
|
29
|
+
*
|
|
30
|
+
* This is intentionally independent of the standard `pickEndpoint`: that
|
|
31
|
+
* resolver prefers chat when a catalog entry advertises both clients, while
|
|
32
|
+
* fast roles have a fixed provider contract. Gemini stays on chat; Luna, Sol,
|
|
33
|
+
* and Grok stay on Responses; the Opus Oracle stays on native Messages.
|
|
34
|
+
* Unknown ids return undefined so a fast caller cannot guess a transport.
|
|
35
|
+
*/
|
|
36
|
+
function fastEndpointRequirement(modelId) {
|
|
37
|
+
const id = modelId.replace(/(?:\[1m\])+$/i, "");
|
|
38
|
+
if (id === "gemini-3.7-flash") return "chat";
|
|
39
|
+
if (id === "gpt-5.6-luna" || id === "gpt-5.6-sol" || id === "grok-4.6") return "responses";
|
|
40
|
+
if (id === "claude-opus-5") return "messages";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the fixed fast transport only when the catalog explicitly advertises
|
|
44
|
+
* the required endpoint. An entry that advertises both still resolves to the
|
|
45
|
+
* policy endpoint, regardless of advertisement order.
|
|
46
|
+
*/
|
|
47
|
+
function fastEndpointForModel(model) {
|
|
48
|
+
const required = fastEndpointRequirement(model.id);
|
|
49
|
+
if (!required) return void 0;
|
|
50
|
+
const advertised = model.supported_endpoints;
|
|
51
|
+
if (!Array.isArray(advertised) || advertised.length === 0) return void 0;
|
|
52
|
+
return advertised.some((endpoint) => ENDPOINTS[required].has(endpoint)) ? required : void 0;
|
|
53
|
+
}
|
|
54
|
+
/** Resolve a fast endpoint from a catalog id, without selecting a fallback. */
|
|
55
|
+
function fastEndpointForCatalogId(modelId, catalog) {
|
|
56
|
+
const found = catalog?.find((model) => model.id === modelId);
|
|
57
|
+
return found ? fastEndpointForModel(found) : void 0;
|
|
58
|
+
}
|
|
59
|
+
const ONE_M_TOKENS = 1e6;
|
|
60
|
+
function catalogModels() {
|
|
61
|
+
return state.models?.data ?? [];
|
|
62
|
+
}
|
|
63
|
+
function catalogModel(id) {
|
|
64
|
+
return catalogModels().find((model) => model.id === id);
|
|
65
|
+
}
|
|
66
|
+
const MAX_PROFILE_MODELS = Object.freeze({
|
|
67
|
+
sol: "gpt-5.6-sol",
|
|
68
|
+
luna: "gpt-5.6-luna",
|
|
69
|
+
gemini: "gemini-3.7-flash",
|
|
70
|
+
grok: "grok-4.6",
|
|
71
|
+
opus: "claude-opus-5"
|
|
72
|
+
});
|
|
73
|
+
const MAX_PROFILE_NATIVE_AGENT_NAMES = [
|
|
74
|
+
"Explore",
|
|
75
|
+
"Plan",
|
|
76
|
+
"general-purpose",
|
|
77
|
+
"implementer",
|
|
78
|
+
"reviewer",
|
|
79
|
+
"brainstorm",
|
|
80
|
+
"peer-review-coordinator"
|
|
81
|
+
];
|
|
82
|
+
const MAX_PROFILE_NATIVE_EFFORTS = Object.freeze({
|
|
83
|
+
Explore: "high",
|
|
84
|
+
Plan: "high",
|
|
85
|
+
"general-purpose": "max",
|
|
86
|
+
implementer: "high",
|
|
87
|
+
reviewer: "high",
|
|
88
|
+
brainstorm: "medium",
|
|
89
|
+
"peer-review-coordinator": "max"
|
|
90
|
+
});
|
|
91
|
+
/** Default model family behind each native role's frontmatter. The optional
|
|
92
|
+
* Grok brainstorm path may fall back to Gemini, whose allowed effort ladder is
|
|
93
|
+
* identical for the values accepted by the max dispatch guard. */
|
|
94
|
+
const MAX_PROFILE_NATIVE_MODELS = Object.freeze({
|
|
95
|
+
Explore: MAX_PROFILE_MODELS.luna,
|
|
96
|
+
Plan: MAX_PROFILE_MODELS.sol,
|
|
97
|
+
"general-purpose": MAX_PROFILE_MODELS.luna,
|
|
98
|
+
implementer: MAX_PROFILE_MODELS.gemini,
|
|
99
|
+
reviewer: MAX_PROFILE_MODELS.grok,
|
|
100
|
+
brainstorm: MAX_PROFILE_MODELS.grok,
|
|
101
|
+
"peer-review-coordinator": MAX_PROFILE_MODELS.luna
|
|
102
|
+
});
|
|
103
|
+
const MAX_PROFILE_LEAD_MODEL = MAX_PROFILE_MODELS.sol;
|
|
104
|
+
MAX_PROFILE_MODELS.opus;
|
|
105
|
+
const MAX_PROFILE_ADVISOR_EFFORT = "high";
|
|
106
|
+
const MAX_PROFILE_ADVISOR_INSTRUCTIONS = `# Advisor Tool
|
|
107
|
+
|
|
108
|
+
You have access to an optional, lead-only, transcript-aware Max Advisor. It offers a focused second opinion; it is not a supervisor, approver, workflow gate, or substitute for your own reasoning. You keep decision ownership.
|
|
109
|
+
|
|
110
|
+
Use it only when a consequential uncertainty remains after direct investigation and cannot be settled by repository evidence, a focused command or test, Plan, reviewer, or a fresh-context peer: conflicting evidence, a materially changed assumption, a genuinely non-converging approach, or a hard-to-reverse trade-off. State the precise unresolved question immediately before calling it.
|
|
111
|
+
|
|
112
|
+
Do not call it automatically before substantive work or completion, for routine progress, while waiting, after ordinary tool output, for directly verifiable facts, planner approval, reviewer verification, or as reassurance for a decision the evidence already supports.
|
|
113
|
+
|
|
114
|
+
Treat the result as advice, not authority. Weigh it against the user's intent and verified evidence. Consult again only when materially new evidence creates a different question or directly conflicts with the earlier advice.`;
|
|
115
|
+
function maxAdvisorModelFromPin(pinned, opusModel) {
|
|
116
|
+
const trimmed = pinned?.trim();
|
|
117
|
+
const base = trimmed ? stripTrailingOneMSuffix(trimmed).base : void 0;
|
|
118
|
+
if (base === MAX_PROFILE_MODELS.sol || base === MAX_PROFILE_MODELS.opus) return base;
|
|
119
|
+
return opusModel ?? MAX_PROFILE_MODELS.opus;
|
|
120
|
+
}
|
|
121
|
+
function maxAdvisorPinIsValid(pinned) {
|
|
122
|
+
const trimmed = pinned?.trim();
|
|
123
|
+
const base = trimmed ? stripTrailingOneMSuffix(trimmed).base : trimmed;
|
|
124
|
+
return base === void 0 || base === "" || base === MAX_PROFILE_MODELS.sol || base === MAX_PROFILE_MODELS.opus;
|
|
125
|
+
}
|
|
126
|
+
function maxAdvisorEffortForModel(_model) {
|
|
127
|
+
return MAX_PROFILE_ADVISOR_EFFORT;
|
|
128
|
+
}
|
|
129
|
+
Object.freeze([
|
|
130
|
+
MAX_PROFILE_MODELS.luna,
|
|
131
|
+
MAX_PROFILE_MODELS.gemini,
|
|
132
|
+
MAX_PROFILE_MODELS.grok
|
|
133
|
+
]);
|
|
134
|
+
const MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS = Object.freeze([
|
|
135
|
+
MAX_PROFILE_MODELS.sol,
|
|
136
|
+
MAX_PROFILE_MODELS.luna,
|
|
137
|
+
MAX_PROFILE_MODELS.gemini,
|
|
138
|
+
MAX_PROFILE_MODELS.opus
|
|
139
|
+
]);
|
|
140
|
+
function findModel(catalog, id) {
|
|
141
|
+
return catalog?.data?.find((model) => model.id === id);
|
|
142
|
+
}
|
|
143
|
+
function hasToolCalls(model) {
|
|
144
|
+
return model?.capabilities?.supports?.tool_calls === true;
|
|
145
|
+
}
|
|
146
|
+
function hasContextAtLeast(model, tokens) {
|
|
147
|
+
return (model?.capabilities?.limits?.max_context_window_tokens ?? 0) >= tokens;
|
|
148
|
+
}
|
|
149
|
+
function supportsEffort(model, effort) {
|
|
150
|
+
const ladder = model?.capabilities?.supports?.reasoning_effort;
|
|
151
|
+
return Array.isArray(ladder) && ladder.includes(effort);
|
|
152
|
+
}
|
|
153
|
+
function hasUsableLimits(model) {
|
|
154
|
+
const limits = model?.capabilities?.limits;
|
|
155
|
+
const prompt = limits?.max_prompt_tokens;
|
|
156
|
+
const output = limits?.max_output_tokens;
|
|
157
|
+
return typeof prompt === "number" && Number.isFinite(prompt) && prompt > 0 && typeof output === "number" && Number.isFinite(output) && output > 0 && (limits?.max_context_window_tokens ?? 0) >= prompt;
|
|
158
|
+
}
|
|
159
|
+
function supportsEndpoint(model, endpoint) {
|
|
160
|
+
return model !== void 0 && fastEndpointForModel(model) === endpoint;
|
|
161
|
+
}
|
|
162
|
+
function validateRequiredModel(model, id, requirements, missing) {
|
|
163
|
+
if (!model) {
|
|
164
|
+
missing.push(`${id}: absent from the live catalog`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const [message, ok] of requirements) if (!ok) missing.push(`${id}: ${message}`);
|
|
168
|
+
}
|
|
169
|
+
function usableGemini(model) {
|
|
170
|
+
return Boolean(model && hasToolCalls(model) && hasContextAtLeast(model, ONE_M_TOKENS) && supportsEffort(model, "high") && supportsEndpoint(model, "chat") && hasUsableLimits(model));
|
|
171
|
+
}
|
|
172
|
+
function usableGrok(model) {
|
|
173
|
+
return Boolean(model && hasToolCalls(model) && supportsEffort(model, "medium") && supportsEndpoint(model, "responses") && hasUsableLimits(model));
|
|
174
|
+
}
|
|
175
|
+
function usableGrokReviewer(model) {
|
|
176
|
+
return Boolean(usableGrok(model) && supportsEffort(model, "high") && (model?.capabilities?.limits?.max_prompt_tokens ?? 0) >= 2e5);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate the fixed prerequisites for the raw `-m max` launch.
|
|
180
|
+
*
|
|
181
|
+
* Max is a deliberately strong, closed profile. Its lead, planning, reviewer,
|
|
182
|
+
* and transcript-aware Advisor paths must all be present before any runtime
|
|
183
|
+
* artifact is written: Sol, Luna, Gemini 3.7 Flash, and Opus 5 are mandatory.
|
|
184
|
+
* Grok 4.6 is the only optional model. When present it supplies the preferred
|
|
185
|
+
* medium-effort brainstorm/critic path, but it is never a max lead and its
|
|
186
|
+
* sub-1M context must remain undecorated.
|
|
187
|
+
*/
|
|
188
|
+
function validateMaxProfilePrerequisites(catalog) {
|
|
189
|
+
const missing = [];
|
|
190
|
+
const sol = findModel(catalog, MAX_PROFILE_MODELS.sol);
|
|
191
|
+
const luna = findModel(catalog, MAX_PROFILE_MODELS.luna);
|
|
192
|
+
const gemini = findModel(catalog, MAX_PROFILE_MODELS.gemini);
|
|
193
|
+
const grok = findModel(catalog, MAX_PROFILE_MODELS.grok);
|
|
194
|
+
const opus = findModel(catalog, MAX_PROFILE_MODELS.opus);
|
|
195
|
+
validateRequiredModel(sol, MAX_PROFILE_MODELS.sol, [
|
|
196
|
+
["does not advertise tool_calls", hasToolCalls(sol)],
|
|
197
|
+
["advertised context window is below 1M", hasContextAtLeast(sol, ONE_M_TOKENS)],
|
|
198
|
+
["does not advertise a high reasoning effort", supportsEffort(sol, "high")],
|
|
199
|
+
["does not advertise a supported Responses endpoint", supportsEndpoint(sol, "responses")],
|
|
200
|
+
["has no usable max_prompt_tokens/max_output_tokens limits", hasUsableLimits(sol)]
|
|
201
|
+
], missing);
|
|
202
|
+
validateRequiredModel(luna, MAX_PROFILE_MODELS.luna, [
|
|
203
|
+
["does not advertise tool_calls", hasToolCalls(luna)],
|
|
204
|
+
["advertised context window is below 1M", hasContextAtLeast(luna, ONE_M_TOKENS)],
|
|
205
|
+
["does not advertise a high reasoning effort", supportsEffort(luna, "high")],
|
|
206
|
+
["does not advertise a max reasoning effort", supportsEffort(luna, "max")],
|
|
207
|
+
["does not advertise a supported Responses endpoint", supportsEndpoint(luna, "responses")],
|
|
208
|
+
["has no usable max_prompt_tokens/max_output_tokens limits", hasUsableLimits(luna)]
|
|
209
|
+
], missing);
|
|
210
|
+
validateRequiredModel(gemini, MAX_PROFILE_MODELS.gemini, [
|
|
211
|
+
["does not advertise tool_calls", hasToolCalls(gemini)],
|
|
212
|
+
["advertised context window is below 1M", hasContextAtLeast(gemini, ONE_M_TOKENS)],
|
|
213
|
+
["does not advertise a high reasoning effort", supportsEffort(gemini, "high")],
|
|
214
|
+
["does not advertise a supported Chat Completions endpoint", supportsEndpoint(gemini, "chat")],
|
|
215
|
+
["has no usable max_prompt_tokens/max_output_tokens limits", hasUsableLimits(gemini)]
|
|
216
|
+
], missing);
|
|
217
|
+
validateRequiredModel(opus, MAX_PROFILE_MODELS.opus, [
|
|
218
|
+
["advertised context window is below 1M", hasContextAtLeast(opus, ONE_M_TOKENS)],
|
|
219
|
+
["does not advertise adaptive_thinking", opus?.capabilities?.supports?.adaptive_thinking === true],
|
|
220
|
+
["does not advertise a high reasoning effort", supportsEffort(opus, "high")],
|
|
221
|
+
["does not advertise a supported Messages endpoint", supportsEndpoint(opus, "messages")],
|
|
222
|
+
["has no usable max_prompt_tokens/max_output_tokens limits", hasUsableLimits(opus)]
|
|
223
|
+
], missing);
|
|
224
|
+
const geminiUsable = usableGemini(gemini);
|
|
225
|
+
const grokUsable = usableGrok(grok);
|
|
226
|
+
return {
|
|
227
|
+
ok: missing.length === 0,
|
|
228
|
+
missing,
|
|
229
|
+
models: {
|
|
230
|
+
sol: sol && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.sol}:`)) ? sol : void 0,
|
|
231
|
+
luna: luna && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.luna}:`)) ? luna : void 0,
|
|
232
|
+
gemini: geminiUsable ? gemini : void 0,
|
|
233
|
+
grok: grokUsable ? grok : void 0,
|
|
234
|
+
opus: opus && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.opus}:`)) ? opus : void 0,
|
|
235
|
+
thirdLab: grokUsable && supportsEffort(grok, "high") ? "grok" : geminiUsable ? "gemini" : void 0
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function formatMaxPrerequisiteFailure(missing) {
|
|
240
|
+
return "github-router claude -m max requires the following live-catalog capabilities, which this account's catalog does not fully provide:\n" + missing.map((entry) => ` - ${entry}`).join("\n") + "\n\nMax launches do not fall back to arbitrary older models. Use plain `github-router claude` for the standard profile.";
|
|
241
|
+
}
|
|
242
|
+
function maxGeminiModel() {
|
|
243
|
+
const model = catalogModel(MAX_PROFILE_MODELS.gemini);
|
|
244
|
+
return model && usableGemini(model) ? model.id : void 0;
|
|
245
|
+
}
|
|
246
|
+
function maxGrokModel() {
|
|
247
|
+
const model = catalogModel(MAX_PROFILE_MODELS.grok);
|
|
248
|
+
return model && usableGrok(model) ? model.id : void 0;
|
|
249
|
+
}
|
|
250
|
+
/** Grok replacement for max-profile surfaces that require high effort. */
|
|
251
|
+
function maxGrokHighModel() {
|
|
252
|
+
const model = catalogModel(MAX_PROFILE_MODELS.grok);
|
|
253
|
+
return model && usableGrokReviewer(model) ? model.id : void 0;
|
|
254
|
+
}
|
|
255
|
+
/** Max-only replacement for any standard Gemini Pro assignment. */
|
|
256
|
+
function maxProReplacementModel() {
|
|
257
|
+
if (maxGrokHighModel()) return MAX_PROFILE_MODELS.grok;
|
|
258
|
+
if (maxGeminiModel()) return MAX_PROFILE_MODELS.gemini;
|
|
259
|
+
}
|
|
260
|
+
function maxOpusModel() {
|
|
261
|
+
const model = catalogModel(MAX_PROFILE_MODELS.opus);
|
|
262
|
+
return model && hasContextAtLeast(model, ONE_M_TOKENS) && model.capabilities?.supports?.adaptive_thinking === true && supportsEffort(model, "high") && supportsEndpoint(model, "messages") && hasUsableLimits(model) ? model.id : void 0;
|
|
263
|
+
}
|
|
264
|
+
function maxLunaMaxModel() {
|
|
265
|
+
const model = catalogModel(MAX_PROFILE_MODELS.luna);
|
|
266
|
+
return model && hasToolCalls(model) && hasContextAtLeast(model, ONE_M_TOKENS) && supportsEffort(model, "max") && supportsEndpoint(model, "responses") && hasUsableLimits(model) ? model.id : void 0;
|
|
267
|
+
}
|
|
268
|
+
function maxReviewerModel() {
|
|
269
|
+
const grok = maxGrokHighModel();
|
|
270
|
+
if (grok) return grok;
|
|
271
|
+
return maxLunaMaxModel();
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
export { validateMaxProfilePrerequisites as _, MAX_PROFILE_NATIVE_AGENT_NAMES as a, normalizeTrailingOneMSuffix as b, formatMaxPrerequisiteFailure as c, maxAdvisorPinIsValid as d, maxGeminiModel as f, maxReviewerModel as g, maxProReplacementModel as h, MAX_PROFILE_MODELS as i, maxAdvisorEffortForModel as l, maxOpusModel as m, MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS as n, MAX_PROFILE_NATIVE_EFFORTS as o, maxGrokModel as p, MAX_PROFILE_LEAD_MODEL as r, MAX_PROFILE_NATIVE_MODELS as s, MAX_PROFILE_ADVISOR_INSTRUCTIONS as t, maxAdvisorModelFromPin as u, fastEndpointForCatalogId as v, stripTrailingOneMSuffix as x, fastEndpointForModel as y };
|
|
275
|
+
|
|
276
|
+
//# sourceMappingURL=max-profile-contract-Ct62mtAR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-profile-contract-Ct62mtAR.js","names":[],"sources":["../src/lib/model-suffix.ts","../src/lib/fast-endpoint.ts","../src/lib/max-profile-contract.ts"],"sourcesContent":["/**\n * Normalize Claude Code's local 1M-context decoration at model boundaries.\n *\n * `[1m]` is a client-side accounting marker, not part of a Copilot catalog id.\n * Claude Code can append its own marker to a picker value that is already\n * decorated by the router, producing a reachable doubled suffix. Keep the\n * helper dependency-free so launch/profile code and request preprocessing use\n * exactly the same trailing-suffix rule without an import cycle.\n */\nexport interface TrailingOneMSuffix {\n base: string\n hadSuffix: boolean\n}\n\n/** Strip every trailing `[1m]` marker, case-insensitively. */\nexport function stripTrailingOneMSuffix(id: string): TrailingOneMSuffix {\n const match = id.match(/(?:\\[1m\\])+$/i)\n if (!match || match.index === undefined) {\n return { base: id, hadSuffix: false }\n }\n return { base: id.slice(0, match.index), hadSuffix: true }\n}\n\n/** Reapply at most one canonical `[1m]` marker when the input had one. */\nexport function normalizeTrailingOneMSuffix(id: string): string {\n const { base, hadSuffix } = stripTrailingOneMSuffix(id)\n return hadSuffix ? `${base}[1m]` : base\n}\n\n/** Append one marker without ever creating a doubled suffix. */\nexport function withTrailingOneMSuffix(id: string): string {\n const { base } = stripTrailingOneMSuffix(id)\n return `${base}[1m]`\n}\n","import type { Model } from \"~/services/copilot/get-models\"\n\n// Fast endpoint policy is intentionally kept separate from the standard\n// pickEndpoint() resolver. See fastEndpointRequirement below.\n\n/** Endpoint clients used by the fast profile's fixed roster. */\nexport type FastEndpoint = \"chat\" | \"responses\" | \"messages\"\n\nconst ENDPOINTS: Readonly<Record<FastEndpoint, ReadonlySet<string>>> = {\n chat: new Set([\"/chat/completions\", \"/v1/chat/completions\"]),\n responses: new Set([\"/responses\", \"/v1/responses\"]),\n messages: new Set([\"/messages\", \"/v1/messages\"]),\n}\n\n/**\n * Return the endpoint mandated by a fast-profile roster model.\n *\n * This is intentionally independent of the standard `pickEndpoint`: that\n * resolver prefers chat when a catalog entry advertises both clients, while\n * fast roles have a fixed provider contract. Gemini stays on chat; Luna, Sol,\n * and Grok stay on Responses; the Opus Oracle stays on native Messages.\n * Unknown ids return undefined so a fast caller cannot guess a transport.\n */\nexport function fastEndpointRequirement(modelId: string): FastEndpoint | undefined {\n const id = modelId.replace(/(?:\\[1m\\])+$/i, \"\")\n if (id === \"gemini-3.7-flash\") return \"chat\"\n if (id === \"gpt-5.6-luna\" || id === \"gpt-5.6-sol\" || id === \"grok-4.6\") {\n return \"responses\"\n }\n if (id === \"claude-opus-5\") return \"messages\"\n return undefined\n}\n\n/**\n * Resolve the fixed fast transport only when the catalog explicitly advertises\n * the required endpoint. An entry that advertises both still resolves to the\n * policy endpoint, regardless of advertisement order.\n */\nexport function fastEndpointForModel(model: Model): FastEndpoint | undefined {\n const required = fastEndpointRequirement(model.id)\n if (!required) return undefined\n const advertised = model.supported_endpoints\n if (!Array.isArray(advertised) || advertised.length === 0) return undefined\n return advertised.some((endpoint) => ENDPOINTS[required].has(endpoint))\n ? required\n : undefined\n}\n\n/** Resolve a fast endpoint from a catalog id, without selecting a fallback. */\nexport function fastEndpointForCatalogId(\n modelId: string,\n catalog: ReadonlyArray<Model> | undefined,\n): FastEndpoint | undefined {\n const found = catalog?.find((model) => model.id === modelId)\n return found ? fastEndpointForModel(found) : undefined\n}\n\n/** Human-readable endpoint name for prerequisite diagnostics. */\nexport function fastEndpointLabel(endpoint: FastEndpoint): string {\n return endpoint === \"chat\"\n ? \"Chat Completions\"\n : endpoint === \"responses\"\n ? \"Responses\"\n : \"Messages\"\n}\n","import type { Model, ModelsResponse } from \"~/services/copilot/get-models\"\nimport type { Effort } from \"./reasoning-effort\"\nimport { fastEndpointForModel } from \"./fast-endpoint\"\nimport { stripTrailingOneMSuffix } from \"./model-suffix\"\nimport { state } from \"./state\"\n\n// Max uses the same context threshold as Claude Code's `[1m]` accounting.\nexport const MAX_REQUIRED_CONTEXT_TOKENS = 1_000_000\n\nconst ONE_M_TOKENS = MAX_REQUIRED_CONTEXT_TOKENS\n\nfunction catalogModels(): Array<Model> {\n return state.models?.data ?? []\n}\n\nfunction catalogModel(id: string): Model | undefined {\n return catalogModels().find((model) => model.id === id)\n}\n\n\nexport const MAX_PROFILE_MODELS = Object.freeze({\n sol: \"gpt-5.6-sol\",\n luna: \"gpt-5.6-luna\",\n gemini: \"gemini-3.7-flash\",\n grok: \"grok-4.6\",\n opus: \"claude-opus-5\",\n} as const)\n\nexport const MAX_PROFILE_NATIVE_AGENT_NAMES = [\n \"Explore\",\n \"Plan\",\n \"general-purpose\",\n \"implementer\",\n \"reviewer\",\n \"brainstorm\",\n \"peer-review-coordinator\",\n] as const\n\nexport type MaxProfileNativeAgentName =\n (typeof MAX_PROFILE_NATIVE_AGENT_NAMES)[number]\n\nexport const MAX_PROFILE_NATIVE_EFFORTS = Object.freeze({\n Explore: \"high\",\n Plan: \"high\",\n \"general-purpose\": \"max\",\n implementer: \"high\",\n reviewer: \"high\",\n brainstorm: \"medium\",\n \"peer-review-coordinator\": \"max\",\n} as const satisfies Record<MaxProfileNativeAgentName, Effort>)\n\n/** Default model family behind each native role's frontmatter. The optional\n * Grok brainstorm path may fall back to Gemini, whose allowed effort ladder is\n * identical for the values accepted by the max dispatch guard. */\nexport const MAX_PROFILE_NATIVE_MODELS = Object.freeze({\n Explore: MAX_PROFILE_MODELS.luna,\n Plan: MAX_PROFILE_MODELS.sol,\n \"general-purpose\": MAX_PROFILE_MODELS.luna,\n implementer: MAX_PROFILE_MODELS.gemini,\n reviewer: MAX_PROFILE_MODELS.grok,\n brainstorm: MAX_PROFILE_MODELS.grok,\n \"peer-review-coordinator\": MAX_PROFILE_MODELS.luna,\n} as const satisfies Record<MaxProfileNativeAgentName, string>)\n\nexport const MAX_PROFILE_LEAD_MODEL = MAX_PROFILE_MODELS.sol\nexport const MAX_PROFILE_ADVISOR_MODEL = MAX_PROFILE_MODELS.opus\nexport const MAX_PROFILE_ADVISOR_EFFORT = \"high\" as const\nexport const MAX_PROFILE_ADVISOR_INSTRUCTIONS = `# Advisor Tool\n\nYou have access to an optional, lead-only, transcript-aware Max Advisor. It offers a focused second opinion; it is not a supervisor, approver, workflow gate, or substitute for your own reasoning. You keep decision ownership.\n\nUse it only when a consequential uncertainty remains after direct investigation and cannot be settled by repository evidence, a focused command or test, Plan, reviewer, or a fresh-context peer: conflicting evidence, a materially changed assumption, a genuinely non-converging approach, or a hard-to-reverse trade-off. State the precise unresolved question immediately before calling it.\n\nDo not call it automatically before substantive work or completion, for routine progress, while waiting, after ordinary tool output, for directly verifiable facts, planner approval, reviewer verification, or as reassurance for a decision the evidence already supports.\n\nTreat the result as advice, not authority. Weigh it against the user's intent and verified evidence. Consult again only when materially new evidence creates a different question or directly conflicts with the earlier advice.`\n\nexport function maxAdvisorModelFromPin(pinned: string | undefined, opusModel?: string): string {\n const trimmed = pinned?.trim()\n const base = trimmed ? stripTrailingOneMSuffix(trimmed).base : undefined\n if (base === MAX_PROFILE_MODELS.sol || base === MAX_PROFILE_MODELS.opus) {\n return base\n }\n return opusModel ?? MAX_PROFILE_MODELS.opus\n}\n\nexport function maxAdvisorPinIsValid(pinned: string | undefined): boolean {\n const trimmed = pinned?.trim()\n const base = trimmed ? stripTrailingOneMSuffix(trimmed).base : trimmed\n return base === undefined\n || base === \"\"\n || base === MAX_PROFILE_MODELS.sol\n || base === MAX_PROFILE_MODELS.opus\n}\n\nexport function maxAdvisorModelFromPinStrict(pinned: string | undefined, opusModel?: string): string {\n if (!maxAdvisorPinIsValid(pinned)) return opusModel ?? MAX_PROFILE_MODELS.opus\n return maxAdvisorModelFromPin(pinned, opusModel)\n}\n\nexport function maxStandInAvailable(): boolean {\n return usableSol(catalogModel(MAX_PROFILE_MODELS.sol))\n && maxOpusModel() !== undefined\n && (maxGrokHighModel() !== undefined || maxGeminiModel() !== undefined)\n}\nexport function maxAdvisorEffortForModel(_model: string): Effort {\n return MAX_PROFILE_ADVISOR_EFFORT\n}\n\nexport const MAX_PROFILE_NATIVE_MODEL_IDS = Object.freeze([\n MAX_PROFILE_MODELS.luna,\n MAX_PROFILE_MODELS.gemini,\n MAX_PROFILE_MODELS.grok,\n] as const)\n\nexport const MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS = Object.freeze([\n MAX_PROFILE_MODELS.sol,\n MAX_PROFILE_MODELS.luna,\n MAX_PROFILE_MODELS.gemini,\n MAX_PROFILE_MODELS.opus,\n] as const)\n\nexport type MaxProfileAllowedLeadModel =\n (typeof MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS)[number]\n\nexport interface MaxProfilePrerequisiteCheck {\n ok: boolean\n missing: ReadonlyArray<string>\n /** Resolved optional/third-lab assignments for the launch. */\n models: {\n sol?: Model\n luna?: Model\n gemini?: Model\n grok?: Model\n opus?: Model\n thirdLab?: \"gemini\" | \"grok\"\n }\n}\n\nfunction findModel(\n catalog: ModelsResponse | undefined,\n id: string,\n): Model | undefined {\n return catalog?.data?.find((model) => model.id === id)\n}\n\nfunction hasToolCalls(model: Model | undefined): boolean {\n return model?.capabilities?.supports?.tool_calls === true\n}\n\nfunction hasContextAtLeast(model: Model | undefined, tokens: number): boolean {\n return (model?.capabilities?.limits?.max_context_window_tokens ?? 0) >= tokens\n}\n\nfunction supportsEffort(model: Model | undefined, effort: Effort): boolean {\n const ladder = model?.capabilities?.supports?.reasoning_effort\n return Array.isArray(ladder) && ladder.includes(effort)\n}\n\nfunction hasUsableLimits(model: Model | undefined): boolean {\n const limits = model?.capabilities?.limits\n const prompt = limits?.max_prompt_tokens\n const output = limits?.max_output_tokens\n return (\n typeof prompt === \"number\"\n && Number.isFinite(prompt)\n && prompt > 0\n && typeof output === \"number\"\n && Number.isFinite(output)\n && output > 0\n && (limits?.max_context_window_tokens ?? 0) >= prompt\n )\n}\n\nfunction supportsEndpoint(\n model: Model | undefined,\n endpoint: \"chat\" | \"responses\" | \"messages\",\n): boolean {\n return model !== undefined && fastEndpointForModel(model) === endpoint\n}\n\nfunction validateRequiredModel(\n model: Model | undefined,\n id: string,\n requirements: ReadonlyArray<[string, boolean]>,\n missing: string[],\n): void {\n if (!model) {\n missing.push(`${id}: absent from the live catalog`)\n return\n }\n for (const [message, ok] of requirements) {\n if (!ok) missing.push(`${id}: ${message}`)\n }\n}\n\nfunction usableSol(model: Model | undefined): boolean {\n return Boolean(\n model\n && hasToolCalls(model)\n && hasContextAtLeast(model, ONE_M_TOKENS)\n && supportsEffort(model, \"high\")\n && supportsEndpoint(model, \"responses\")\n && hasUsableLimits(model)\n )\n}\n\nfunction usableGemini(model: Model | undefined): boolean {\n return Boolean(\n model\n && hasToolCalls(model)\n && hasContextAtLeast(model, ONE_M_TOKENS)\n && supportsEffort(model, \"high\")\n && supportsEndpoint(model, \"chat\")\n && hasUsableLimits(model),\n )\n}\n\nfunction usableGrok(model: Model | undefined): boolean {\n return Boolean(\n model\n && hasToolCalls(model)\n && supportsEffort(model, \"medium\")\n && supportsEndpoint(model, \"responses\")\n && hasUsableLimits(model),\n )\n}\n\nfunction usableGrokReviewer(model: Model | undefined): boolean {\n return Boolean(\n usableGrok(model)\n && supportsEffort(model, \"high\")\n && (model?.capabilities?.limits?.max_prompt_tokens ?? 0) >= 200_000,\n )\n}\n\n/**\n * Validate the fixed prerequisites for the raw `-m max` launch.\n *\n * Max is a deliberately strong, closed profile. Its lead, planning, reviewer,\n * and transcript-aware Advisor paths must all be present before any runtime\n * artifact is written: Sol, Luna, Gemini 3.7 Flash, and Opus 5 are mandatory.\n * Grok 4.6 is the only optional model. When present it supplies the preferred\n * medium-effort brainstorm/critic path, but it is never a max lead and its\n * sub-1M context must remain undecorated.\n */\nexport function validateMaxProfilePrerequisites(\n catalog: ModelsResponse | undefined,\n): MaxProfilePrerequisiteCheck {\n const missing: string[] = []\n const sol = findModel(catalog, MAX_PROFILE_MODELS.sol)\n const luna = findModel(catalog, MAX_PROFILE_MODELS.luna)\n const gemini = findModel(catalog, MAX_PROFILE_MODELS.gemini)\n const grok = findModel(catalog, MAX_PROFILE_MODELS.grok)\n const opus = findModel(catalog, MAX_PROFILE_MODELS.opus)\n\n validateRequiredModel(\n sol,\n MAX_PROFILE_MODELS.sol,\n [\n [\"does not advertise tool_calls\", hasToolCalls(sol)],\n [\"advertised context window is below 1M\", hasContextAtLeast(sol, ONE_M_TOKENS)],\n [\"does not advertise a high reasoning effort\", supportsEffort(sol, \"high\")],\n [\"does not advertise a supported Responses endpoint\", supportsEndpoint(sol, \"responses\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(sol)],\n ],\n missing,\n )\n validateRequiredModel(\n luna,\n MAX_PROFILE_MODELS.luna,\n [\n [\"does not advertise tool_calls\", hasToolCalls(luna)],\n [\"advertised context window is below 1M\", hasContextAtLeast(luna, ONE_M_TOKENS)],\n [\"does not advertise a high reasoning effort\", supportsEffort(luna, \"high\")],\n [\"does not advertise a max reasoning effort\", supportsEffort(luna, \"max\")],\n [\"does not advertise a supported Responses endpoint\", supportsEndpoint(luna, \"responses\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(luna)],\n ],\n missing,\n )\n validateRequiredModel(\n gemini,\n MAX_PROFILE_MODELS.gemini,\n [\n [\"does not advertise tool_calls\", hasToolCalls(gemini)],\n [\"advertised context window is below 1M\", hasContextAtLeast(gemini, ONE_M_TOKENS)],\n [\"does not advertise a high reasoning effort\", supportsEffort(gemini, \"high\")],\n [\"does not advertise a supported Chat Completions endpoint\", supportsEndpoint(gemini, \"chat\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(gemini)],\n ],\n missing,\n )\n validateRequiredModel(\n opus,\n MAX_PROFILE_MODELS.opus,\n [\n [\"advertised context window is below 1M\", hasContextAtLeast(opus, ONE_M_TOKENS)],\n [\"does not advertise adaptive_thinking\", opus?.capabilities?.supports?.adaptive_thinking === true],\n [\"does not advertise a high reasoning effort\", supportsEffort(opus, \"high\")],\n [\"does not advertise a supported Messages endpoint\", supportsEndpoint(opus, \"messages\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(opus)],\n ],\n missing,\n )\n\n // Grok is intentionally optional. A malformed optional entry is simply\n // omitted from the max projection; it must never make launch fail or be\n // decorated as a 1M model.\n const geminiUsable = usableGemini(gemini)\n const grokUsable = usableGrok(grok)\n\n return {\n ok: missing.length === 0,\n missing,\n models: {\n sol: sol && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.sol}:`)) ? sol : undefined,\n luna: luna && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.luna}:`)) ? luna : undefined,\n gemini: geminiUsable ? gemini : undefined,\n grok: grokUsable ? grok : undefined,\n opus: opus && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.opus}:`)) ? opus : undefined,\n thirdLab: grokUsable && supportsEffort(grok, \"high\")\n ? \"grok\"\n : geminiUsable\n ? \"gemini\"\n : undefined,\n },\n }\n}\n\nexport function formatMaxPrerequisiteFailure(\n missing: ReadonlyArray<string>,\n): string {\n return (\n \"github-router claude -m max requires the following live-catalog capabilities, \"\n + \"which this account's catalog does not fully provide:\\n\"\n + missing.map((entry) => ` - ${entry}`).join(\"\\n\")\n + \"\\n\\nMax launches do not fall back to arbitrary older models. \"\n + \"Use plain `github-router claude` for the standard profile.\"\n )\n}\n\nexport function maxModelIsOneM(\n model: Model | undefined,\n): boolean {\n return (\n (model?.capabilities?.limits?.max_context_window_tokens ?? 0) >= ONE_M_TOKENS\n )\n}\n\nexport function maxModelSupportsEffort(\n model: Model | undefined,\n effort: Effort,\n): boolean {\n return supportsEffort(model, effort)\n}\n\nexport function maxModelHasUsableLimits(model: Model | undefined): boolean {\n return hasUsableLimits(model)\n}\n\nexport function maxThirdLabModel(): string | undefined {\n const gemini = catalogModel(MAX_PROFILE_MODELS.gemini)\n if (usableGemini(gemini)) return MAX_PROFILE_MODELS.gemini\n const grok = catalogModel(MAX_PROFILE_MODELS.grok)\n return usableGrok(grok) ? MAX_PROFILE_MODELS.grok : undefined\n}\n\nexport function maxGeminiModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.gemini)\n return model && usableGemini(model) ? model.id : undefined\n}\n\nexport function maxGrokModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.grok)\n return model && usableGrok(model) ? model.id : undefined\n}\n\n/** Grok replacement for max-profile surfaces that require high effort. */\nexport function maxGrokHighModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.grok)\n return model && usableGrokReviewer(model) ? model.id : undefined\n}\n\n/** Max-only replacement for any standard Gemini Pro assignment. */\nexport function maxProReplacementModel():\n | typeof MAX_PROFILE_MODELS.grok\n | typeof MAX_PROFILE_MODELS.gemini\n | undefined {\n if (maxGrokHighModel()) return MAX_PROFILE_MODELS.grok\n if (maxGeminiModel()) return MAX_PROFILE_MODELS.gemini\n return undefined\n}\n\nexport function maxOpusModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.opus)\n return model && hasContextAtLeast(model, ONE_M_TOKENS)\n && model.capabilities?.supports?.adaptive_thinking === true\n && supportsEffort(model, \"high\")\n && supportsEndpoint(model, \"messages\")\n && hasUsableLimits(model)\n ? model.id\n : undefined\n}\n\nexport function maxLunaMaxModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.luna)\n return model\n && hasToolCalls(model)\n && hasContextAtLeast(model, ONE_M_TOKENS)\n && supportsEffort(model, \"max\")\n && supportsEndpoint(model, \"responses\")\n && hasUsableLimits(model)\n ? model.id\n : undefined\n}\n\nexport function maxReviewerModel(): string | undefined {\n const grok = maxGrokHighModel()\n if (grok) return grok\n return maxLunaMaxModel()\n}\n\nexport function maxCatalogModel(id: string): Model | undefined {\n return catalogModel(id)\n}\n\nexport function maxCatalogModels(): ReadonlyArray<Model> {\n return catalogModels()\n}"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as buildSelfCommand } from "./self-invocation-
|
|
1
|
+
import { t as buildSelfCommand } from "./self-invocation-BJWbA8ka.js";
|
|
2
2
|
//#region src/lib/mcp-workspace-header.ts
|
|
3
3
|
/** Header the per-session MCP headersHelper emits, carrying the calling Claude
|
|
4
4
|
* session's working directory. The /mcp handler uses it as the default
|
|
@@ -17,4 +17,4 @@ function buildWorkspaceHeaderHelperCommand(invocation) {
|
|
|
17
17
|
//#endregion
|
|
18
18
|
export { buildWorkspaceHeaderHelperCommand as n, buildWorkspaceHeaderJson as r, MCP_WORKSPACE_HEADER as t };
|
|
19
19
|
|
|
20
|
-
//# sourceMappingURL=mcp-workspace-header-
|
|
20
|
+
//# sourceMappingURL=mcp-workspace-header-NzFD0Wgd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-workspace-header-
|
|
1
|
+
{"version":3,"file":"mcp-workspace-header-NzFD0Wgd.js","names":[],"sources":["../src/lib/mcp-workspace-header.ts"],"sourcesContent":["import { buildSelfCommand, type SelfInvocation } from \"./hook-launcher/self-invocation\"\n\n/** Header the per-session MCP headersHelper emits, carrying the calling Claude\n * session's working directory. The /mcp handler uses it as the default\n * `workspace` for repo-scoped tools so a machine-wide `serve` targets the\n * active repo, not the proxy launch cwd. */\nexport const MCP_WORKSPACE_HEADER = \"X-GH-Workspace\"\n\n/** stdout JSON the headersHelper prints; Claude merges it into the connection\n * headers. `cwd` is the helper's own process cwd = the session's project dir. */\nexport function buildWorkspaceHeaderJson(cwd: string): string {\n return JSON.stringify({ [MCP_WORKSPACE_HEADER]: cwd })\n}\n\n/** Command string Claude runs as the headersHelper. */\nexport function buildWorkspaceHeaderHelperCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-workspace-header\")\n}\n"],"mappings":";;;;;;AAMA,MAAa,uBAAuB;;;AAIpC,SAAgB,yBAAyB,KAAqB;CAC5D,OAAO,KAAK,UAAU,GAAG,uBAAuB,IAAI,CAAC;AACvD;;AAGA,SAAgB,kCAAkC,YAAoC;CACpF,OAAO,iBAAiB,YAAY,2BAA2B;AACjE"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as ensurePaths } from "./paths-
|
|
2
|
-
import { p as getModels, r as setupGitHubToken, t as setupCopilotToken } from "./token-
|
|
1
|
+
import { i as ensurePaths } from "./paths-IcZW8gZx.js";
|
|
2
|
+
import { p as getModels, r as setupGitHubToken, t as setupCopilotToken } from "./token-CbuFyx-B.js";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import consola from "consola";
|
|
5
5
|
//#region src/models.ts
|
|
@@ -160,4 +160,4 @@ function formatTokens(n) {
|
|
|
160
160
|
//#endregion
|
|
161
161
|
export { models };
|
|
162
162
|
|
|
163
|
-
//# sourceMappingURL=models-
|
|
163
|
+
//# sourceMappingURL=models-1KwEUgzd.js.map
|