github-router 0.3.250 → 0.3.255

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/attribution-settings-CegtE0TY.js +2347 -0
  2. package/dist/attribution-settings-CegtE0TY.js.map +1 -0
  3. package/dist/auth-CP4q2CjO.js +44 -0
  4. package/dist/auth-CP4q2CjO.js.map +1 -0
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/check-usage-6PB5IUFb.js +43 -0
  7. package/dist/check-usage-6PB5IUFb.js.map +1 -0
  8. package/dist/claude-BiZxoNWq.js +593 -0
  9. package/dist/claude-BiZxoNWq.js.map +1 -0
  10. package/dist/client-CQMfroGV.js +413 -0
  11. package/dist/client-CQMfroGV.js.map +1 -0
  12. package/dist/codex-ST21X3WF.js +85 -0
  13. package/dist/codex-ST21X3WF.js.map +1 -0
  14. package/dist/debug-CjmE9sgG.js +81 -0
  15. package/dist/debug-CjmE9sgG.js.map +1 -0
  16. package/dist/engine-DNCX0Hfd.js +2 -0
  17. package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
  18. package/dist/exec-DTO2oin0.js.map +1 -0
  19. package/dist/file-log-reporter-IFQcL5IK.js +158 -0
  20. package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
  21. package/dist/gate-discovery-CepmbZnc.js +635 -0
  22. package/dist/gate-discovery-CepmbZnc.js.map +1 -0
  23. package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
  24. package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
  25. package/dist/hook-mcp-client-DBvm9608.js +136 -0
  26. package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
  27. package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
  28. package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
  29. package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
  30. package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
  31. package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
  32. package/dist/internal-plan-review-ipnT3bhj.js +114 -0
  33. package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
  34. package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
  35. package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
  36. package/dist/internal-session-bind-DwK2iGW0.js +133 -0
  37. package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
  38. package/dist/internal-stop-hook-CNPGJm63.js +212 -0
  39. package/dist/internal-stop-hook-CNPGJm63.js.map +1 -0
  40. package/dist/internal-stop-review-AUcFssrW.js +122 -0
  41. package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
  42. package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
  43. package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
  44. package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
  45. package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
  46. package/dist/lifecycle-Byw5s1G8.js +134 -0
  47. package/dist/lifecycle-Byw5s1G8.js.map +1 -0
  48. package/dist/lifecycle-D80CaWie.js +2 -0
  49. package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
  50. package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
  51. package/dist/lifecycle-DzqCqDlM.js +2 -0
  52. package/dist/main.js +43 -12904
  53. package/dist/main.js.map +1 -1
  54. package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
  55. package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
  56. package/dist/models-B6UnGyRo.js +158 -0
  57. package/dist/models-B6UnGyRo.js.map +1 -0
  58. package/dist/orchestration-hRU4TdWr.js +39 -0
  59. package/dist/orchestration-hRU4TdWr.js.map +1 -0
  60. package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
  61. package/dist/paths-C1cLZ7Ga.js.map +1 -0
  62. package/dist/paths-CwKwc2pi.js +2 -0
  63. package/dist/{peer-mcp-personas-BvommiSI.js → peer-mcp-personas-BDet588n.js} +163 -2392
  64. package/dist/peer-mcp-personas-BDet588n.js.map +1 -0
  65. package/dist/plan-review-hook-C8x0uKPf.js +159 -0
  66. package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
  67. package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
  68. package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
  69. package/dist/provision-DSkPa5hY.js +656 -0
  70. package/dist/provision-DSkPa5hY.js.map +1 -0
  71. package/dist/serve-sa_7c-yD.js +1511 -0
  72. package/dist/serve-sa_7c-yD.js.map +1 -0
  73. package/dist/server-setup-DcO9qvN3.js +4776 -0
  74. package/dist/server-setup-DcO9qvN3.js.map +1 -0
  75. package/dist/start-siGxuZhO.js +129 -0
  76. package/dist/start-siGxuZhO.js.map +1 -0
  77. package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
  78. package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
  79. package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
  80. package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
  81. package/dist/token-R2ocFC3M.js +1054 -0
  82. package/dist/token-R2ocFC3M.js.map +1 -0
  83. package/dist/version-_Q1WpsQp.js +32 -0
  84. package/dist/version-_Q1WpsQp.js.map +1 -0
  85. package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
  86. package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
  87. package/package.json +1 -1
  88. package/dist/engine-dOYoMQis.js +0 -2
  89. package/dist/lifecycle-0rXrwjWk.js +0 -2
  90. package/dist/lifecycle-DHZsVN8j.js.map +0 -1
  91. package/dist/lifecycle-hzPwXc3I.js +0 -2
  92. package/dist/paths-BjTMI_xK.js.map +0 -1
  93. package/dist/paths-D3XgmTcQ.js +0 -2
  94. package/dist/peer-mcp-personas-BvommiSI.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-discovery-CepmbZnc.js","names":["fs","nodePath","fs","nodePath"],"sources":["../src/lib/orchestration/harness-parse.ts","../src/lib/orchestration/gate-discovery.ts"],"sourcesContent":["/**\n * Deterministic, language-agnostic harness PARSER for the structural Stop-gate.\n *\n * The gate runs a repo's own checks (typecheck/lint/test/build) when the agent\n * tries to finish. Historically it only auto-enabled for Bun/TS repos via a\n * hard-coded sealed gate. This module generalizes detection by PARSING the\n * project's own authoritative config — `package.json` scripts, CI `run:` steps,\n * Make/just/task targets, and language manifests — and emitting the canonical\n * check commands it finds.\n *\n * Design properties (a cross-lab panel chose a parser over a model for these):\n * - EVIDENCE-PINNED BY CONSTRUCTION: every emitted command is either lifted\n * verbatim from a parsed source file (a package.json script, a CI `run:`\n * line, a Make target invocation) or is a fixed canonical manifest command\n * for a detected ecosystem (`cargo check`, `go vet ./...`). The parser never\n * invents a command, so it has no hallucination / prompt-injection surface.\n * - PURE + DETERMINISTIC: a function of the repo's files + the live PATH. The\n * runtime Stop hook re-derives it at each stop (no cache), so it always\n * reflects the current tree and fails open for free when a tool vanished.\n * - SHELL-SAFE: `liveExec` runs `command.trim().split(/\\s+/)` (naive argv\n * split, no shell), so every emitted command is validated to be plain\n * space-separated tokens with no shell operators and no `%` (cmd.exe quoting\n * throws on `%`). See `isSafeCommand`.\n *\n * This is consumed ONLY by the local Stop hook. The sealed-gate kernel\n * (`run_workflow`) is never fed a parsed command — `GateDescriptor.kind` is\n * `\"parsed\"`, never a sealed id, and `sealedGateIds()` is unchanged.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { existsSync, promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { resolveExecutable } from \"~/lib/exec\"\n\nimport { resolveSealedGate } from \"./gate-registry\"\nimport { type CheckSpec } from \"./gate-runner\"\n\n/** Canonical check ids — stable across ecosystems so baseline isolation and the\n * selector key on the same names regardless of language. `build` is deliberately\n * NOT a check: build scripts are too variable in cost to auto-run on every stop\n * (a full bundle/SEA build is the slow-command / false-red footgun the design\n * avoids). Compile-checking is covered under `typecheck` (`go vet`, `cargo\n * check`, `tsc`). */\nexport type CheckId = \"typecheck\" | \"lint\" | \"test\"\n\n/** The fast static checks that are always-on; `test` is opt-in (it runs project\n * code and can be slow on every stop). */\nconst STATIC_IDS: ReadonlySet<CheckId> = new Set([\"typecheck\", \"lint\"])\n\n/**\n * The resolved gate source for a repo.\n * - `sealed` — the bun/TS fast-path (a sealed gate id, byte-identical to\n * the legacy behavior; the runtime hook resolves it via the\n * sealed registry).\n * - `parsed` — deterministic parser output (this module).\n * - `discovered` — the evidence-pinned model fallback (see `gate-discovery`).\n * `workdir` is the directory the checks run in (the repo/package root where the\n * evidence was found), NOT the Stop payload's cwd — so a monorepo stop from a\n * nested dir still runs the root's checks.\n */\nexport type GateDescriptor =\n | { kind: \"sealed\"; gateId: string; workdir: string }\n | { kind: \"parsed\"; checks: CheckSpec[]; ecosystem: string; workdir: string; evidence: string[] }\n | { kind: \"discovered\"; checks: CheckSpec[]; ecosystem: string; workdir: string; evidence: string[] }\n\ninterface Candidate {\n id: CheckId\n command: string\n /** The source file the command was lifted from (for evidence + messages). */\n source: string\n}\n\n/** Reject anything that is not a plain, shell-free, `%`-free argv line. This is\n * the load-bearing guard for `liveExec`'s naive whitespace split. */\nexport function isSafeCommand(command: string): boolean {\n const c = command.trim()\n if (c.length === 0 || c.length > 200) return false\n if (/[\\r\\n]/.test(c)) return false\n // Each token may contain only these chars: letters/digits and a small set of\n // path/flag punctuation. This rejects shell metacharacters (; & | < > ( ) $\n // backtick \" ' * ? ! ^ \\ %), env expansion, and quoting in one shot.\n const tokens = c.split(/\\s+/).filter(Boolean)\n if (tokens.length === 0) return false\n return tokens.every((t) => /^[A-Za-z0-9._/:@=+-]+$/.test(t))\n}\n\n/** First token of a command (the executable), for a PATH-presence probe. */\nfunction firstToken(command: string): string {\n return command.trim().split(/\\s+/)[0] ?? \"\"\n}\n\n/** True when a command would MUTATE the tree and so must never be a gate: a\n * `--fix`/`--write` flag or a `:fix`/`:write` script-name suffix (e.g.\n * `npm run lint:fix`, `eslint --fix`). Whitespace is normalized first so a\n * tab/double-space can't slip a flag past the scan. Exported + shared with the\n * discovered-command sanitizer. */\nexport function isMutatingCommand(command: string): boolean {\n const c = command.trim().replace(/\\s+/g, \" \")\n if (/(^| )(--fix|--write|-w|--watch|--serve|--interactive|-i)( |=|$)/i.test(c)) return true\n if (/(^| |:|-)(fix|write)\\b/i.test(c)) return true\n return false\n}\n\n/** True when the command is safe AND its executable resolves on PATH (a missing\n * tool → drop the check rather than guarantee a false-red). */\nfunction commandRunnable(command: string): boolean {\n if (!isSafeCommand(command)) return false\n return resolveExecutable(firstToken(command), { env: process.env }) !== null\n}\n\nasync function readJsonFile(file: string): Promise<unknown | undefined> {\n try {\n return JSON.parse(await fs.readFile(file, \"utf8\")) as unknown\n } catch {\n return undefined\n }\n}\n\nasync function readTextFile(file: string): Promise<string | undefined> {\n try {\n return await fs.readFile(file, \"utf8\")\n } catch {\n return undefined\n }\n}\n\n/** package.json `scripts` as a plain string→string map (best-effort). */\nasync function readScripts(root: string): Promise<Record<string, string>> {\n const pkg = await readJsonFile(nodePath.join(root, \"package.json\"))\n const scripts = pkg && typeof pkg === \"object\" ? (pkg as { scripts?: unknown }).scripts : undefined\n const out: Record<string, string> = {}\n if (scripts && typeof scripts === \"object\") {\n for (const [k, v] of Object.entries(scripts as Record<string, unknown>)) {\n if (typeof v === \"string\") out[k] = v\n }\n }\n return out\n}\n\n/** Pick the JS package runner from the lockfile present at the root. */\nfunction nodeRunner(root: string): \"bun\" | \"pnpm\" | \"yarn\" | \"npm\" {\n if (existsSync(nodePath.join(root, \"bun.lockb\")) || existsSync(nodePath.join(root, \"bun.lock\"))) return \"bun\"\n if (existsSync(nodePath.join(root, \"pnpm-lock.yaml\"))) return \"pnpm\"\n if (existsSync(nodePath.join(root, \"yarn.lock\"))) return \"yarn\"\n return \"npm\"\n}\n\n/** Map a package.json script NAME to a canonical id (read-only checks only — a\n * `:fix`/`:write` variant mutates the tree and is never a gate). */\nfunction scriptNameToId(name: string): CheckId | null {\n if (/:(fix|write)$/i.test(name)) return null\n if (/^(typecheck|type-check|check-types|tsc|types)$/i.test(name)) return \"typecheck\"\n if (/^(lint|lint:check|eslint)$/i.test(name)) return \"lint\"\n if (/^test$/i.test(name)) return \"test\"\n return null\n}\n\nasync function collectNodeChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"package.json\"))) return []\n const runner = nodeRunner(root)\n if (resolveExecutable(runner, { env: process.env }) === null) return []\n const scripts = await readScripts(root)\n const out: Candidate[] = []\n for (const [name] of Object.entries(scripts)) {\n const id = scriptNameToId(name)\n if (!id) continue\n if (id === \"test\" && !includeTests) continue\n const command = `${runner} run ${name}`\n if (isSafeCommand(command)) out.push({ id, command, source: \"package.json\" })\n }\n return out\n}\n\n/**\n * Best-effort CI gap-filler: scan GitHub Actions / GitLab CI YAML for single-line\n * `run:` static checks (typecheck/lint/build) the project actually runs. We do\n * NOT lift `test` from CI — CI test jobs frequently need services/secrets and\n * would false-red on every stop. Multi-line / scripted `run:` blocks are skipped\n * (they can't be a single argv). Any parse failure yields nothing.\n */\nasync function collectCiChecks(root: string): Promise<Candidate[]> {\n const files: string[] = []\n const wfDir = nodePath.join(root, \".github\", \"workflows\")\n try {\n for (const name of await fs.readdir(wfDir)) {\n if (/\\.ya?ml$/i.test(name)) files.push(nodePath.join(wfDir, name))\n }\n } catch {\n /* no workflows dir */\n }\n const gitlab = nodePath.join(root, \".gitlab-ci.yml\")\n if (existsSync(gitlab)) files.push(gitlab)\n if (files.length === 0) return []\n\n let parseYaml: (s: string) => unknown\n try {\n // Lazy import so the parser module has no hard dep when CI files are absent.\n ;({ parse: parseYaml } = await import(\"yaml\"))\n } catch {\n return []\n }\n\n const out: Candidate[] = []\n const seen = new Set<CheckId>()\n const classify = (cmd: string): CheckId | null => {\n if (/\\b(typecheck|type-check|tsc|mypy|pyright)\\b/i.test(cmd)) return \"typecheck\"\n if (/\\b(lint|eslint|ruff|clippy|golangci|vet)\\b/i.test(cmd)) return \"lint\"\n return null\n }\n const consider = (run: unknown, source: string): void => {\n if (typeof run !== \"string\") return\n const cmd = run.trim()\n if (cmd.includes(\"\\n\")) return // multi-line script block — not a single argv.\n if (!isSafeCommand(cmd) || isMutatingCommand(cmd) || !commandRunnable(cmd)) return\n const id = classify(cmd)\n if (!id || seen.has(id)) return\n seen.add(id)\n out.push({ id, command: cmd, source })\n }\n // Walk the parsed YAML for any `run:` string anywhere (GH `steps[].run`, GitLab\n // `<job>.script[]`). A generic deep walk tolerates both schemas + future shapes.\n const walk = (node: unknown, source: string): void => {\n if (!node || typeof node !== \"object\") return\n if (Array.isArray(node)) {\n for (const v of node) walk(v, source)\n return\n }\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n if (k === \"run\" && typeof v === \"string\") consider(v, source)\n else if (k === \"script\") {\n // GitLab `script:` is a string or string[].\n if (typeof v === \"string\") consider(v, source)\n else if (Array.isArray(v)) for (const s of v) consider(s, source)\n } else walk(v, source)\n }\n }\n for (const f of files) {\n const text = await readTextFile(f)\n if (text === undefined) continue\n try {\n walk(parseYaml(text), nodePath.relative(root, f) || nodePath.basename(f))\n } catch {\n /* unparseable workflow → skip */\n }\n }\n return out\n}\n\n/** Make / just / Taskfile targets matching canonical ids → `<tool> <target>`. */\nasync function collectTaskChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n const out: Candidate[] = []\n const wantId = (target: string): CheckId | null => scriptNameToId(target)\n // Makefile / justfile: a line `target:` (Make) or `target:` (just recipe head).\n for (const [file, tool] of [\n [\"Makefile\", \"make\"],\n [\"makefile\", \"make\"],\n [\"justfile\", \"just\"],\n [\".justfile\", \"just\"],\n ] as const) {\n const text = await readTextFile(nodePath.join(root, file))\n if (text === undefined) continue\n if (resolveExecutable(tool, { env: process.env }) === null) continue\n for (const line of text.split(/\\r?\\n/)) {\n const m = /^([A-Za-z0-9._-]+)\\s*:/.exec(line)\n if (!m) continue\n const id = wantId(m[1])\n if (!id || (id === \"test\" && !includeTests)) continue\n const command = `${tool} ${m[1]}`\n if (isSafeCommand(command)) out.push({ id, command, source: file })\n }\n }\n // Taskfile.yml: top-level `tasks:` keys.\n const taskfile = [\"Taskfile.yml\", \"Taskfile.yaml\"].map((n) => nodePath.join(root, n)).find((p) => existsSync(p))\n if (taskfile && resolveExecutable(\"task\", { env: process.env }) !== null) {\n const text = await readTextFile(taskfile)\n if (text !== undefined) {\n try {\n const { parse } = await import(\"yaml\")\n const doc = parse(text) as { tasks?: Record<string, unknown> } | undefined\n for (const name of Object.keys(doc?.tasks ?? {})) {\n const id = wantId(name)\n if (!id || (id === \"test\" && !includeTests)) continue\n const command = `task ${name}`\n if (isSafeCommand(command)) out.push({ id, command, source: nodePath.basename(taskfile) })\n }\n } catch {\n /* skip */\n }\n }\n }\n return out\n}\n\nasync function collectRustChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"Cargo.toml\"))) return []\n if (resolveExecutable(\"cargo\", { env: process.env }) === null) return []\n const out: Candidate[] = [{ id: \"typecheck\", command: \"cargo check\", source: \"Cargo.toml\" }]\n // clippy is a separate component; only emit it if `cargo-clippy` resolves.\n if (resolveExecutable(\"cargo-clippy\", { env: process.env }) !== null) {\n out.push({ id: \"lint\", command: \"cargo clippy\", source: \"Cargo.toml\" })\n }\n if (includeTests) out.push({ id: \"test\", command: \"cargo test\", source: \"Cargo.toml\" })\n return out\n}\n\nasync function collectGoChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"go.mod\"))) return []\n if (resolveExecutable(\"go\", { env: process.env }) === null) return []\n // `go vet` compiles + reports suspect constructs — the typecheck-equivalent.\n const out: Candidate[] = [{ id: \"typecheck\", command: \"go vet ./...\", source: \"go.mod\" }]\n if (includeTests) out.push({ id: \"test\", command: \"go test ./...\", source: \"go.mod\" })\n return out\n}\n\nasync function collectPythonChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n const configFiles = [\"pyproject.toml\", \"setup.cfg\", \"pytest.ini\", \"tox.ini\", \"ruff.toml\", \"mypy.ini\"]\n const present = configFiles.filter((f) => existsSync(nodePath.join(root, f)))\n if (present.length === 0) return []\n const evidence = present[0]\n const out: Candidate[] = []\n const configText = (await readTextFile(nodePath.join(root, present[0]))) ?? \"\"\n const mentions = (tool: string): boolean => present.some((f) => f.startsWith(tool)) || configText.includes(tool)\n // Only emit a Python tool when BOTH config evidence AND the tool resolve.\n if (mentions(\"mypy\") && resolveExecutable(\"mypy\", { env: process.env }) !== null) {\n out.push({ id: \"typecheck\", command: \"mypy .\", source: evidence })\n }\n if (mentions(\"ruff\") && resolveExecutable(\"ruff\", { env: process.env }) !== null) {\n out.push({ id: \"lint\", command: \"ruff check .\", source: evidence })\n }\n if (includeTests && (mentions(\"pytest\") || existsSync(nodePath.join(root, \"pytest.ini\")))) {\n if (resolveExecutable(\"pytest\", { env: process.env }) !== null) {\n out.push({ id: \"test\", command: \"pytest -q\", source: evidence })\n }\n }\n return out\n}\n\n/** Pick the first runnable candidate per id, in source-priority order. */\nfunction pickByPriority(candidates: Candidate[]): { checks: CheckSpec[]; evidence: string[] } {\n const byId = new Map<CheckId, Candidate>()\n for (const c of candidates) {\n if (!byId.has(c.id) && commandRunnable(c.command)) byId.set(c.id, c)\n }\n const order: CheckId[] = [\"typecheck\", \"lint\", \"test\"]\n const checks: CheckSpec[] = []\n const evidence = new Set<string>()\n for (const id of order) {\n const c = byId.get(id)\n if (c) {\n checks.push({ id: c.id, command: c.command })\n evidence.add(c.source)\n }\n }\n return { checks, evidence: [...evidence] }\n}\n\n/**\n * Resolve the gate descriptor for an already-resolved repo `root`.\n * 1. bun/TS sealed fast-path — byte-identical to the legacy `detectHarnessGateId`\n * (bun on PATH + a `typecheck` script → sealed `default-ci`/`typecheck-test`).\n * 2. else the deterministic parser: collect candidates from package.json\n * scripts (primary, self-contained), Make/just/task, language manifests, and\n * a CI gap-filler for static checks; pick one per id by priority. A `parsed`\n * descriptor is returned only when at least one STATIC check survives (a\n * test-only set would either be off-by-default or risk false-reds).\n * 3. else null (the launcher prints why and the gate stays off).\n */\nexport async function parseGateDescriptor(\n root: string,\n opts: { includeTests: boolean },\n): Promise<GateDescriptor | null> {\n // (1) bun/TS sealed parity.\n if (resolveExecutable(\"bun\", { env: process.env }) !== null) {\n const scripts = await readScripts(root)\n if (typeof scripts.typecheck === \"string\") {\n const gateId = typeof scripts.lint === \"string\" ? \"default-ci\" : \"typecheck-test\"\n return { kind: \"sealed\", gateId, workdir: root }\n }\n }\n\n // (2) deterministic parser. package.json scripts first (self-contained), then\n // task runners + manifests, then a CI gap-filler for static checks.\n const groups = await Promise.all([\n collectNodeChecks(root, opts.includeTests),\n collectTaskChecks(root, opts.includeTests),\n collectRustChecks(root, opts.includeTests),\n collectGoChecks(root, opts.includeTests),\n collectPythonChecks(root, opts.includeTests),\n collectCiChecks(root),\n ])\n const candidates = groups.flat()\n if (candidates.length === 0) return null\n\n const ecosystem =\n candidates.find((c) => c.source === \"package.json\") ? \"node\"\n : candidates.find((c) => c.source === \"Cargo.toml\") ? \"rust\"\n : candidates.find((c) => c.source === \"go.mod\") ? \"go\"\n : candidates.find((c) => /^(pyproject|setup|pytest|tox|ruff|mypy)/.test(c.source)) ? \"python\"\n : candidates.find((c) => /^(Makefile|makefile|justfile|\\.justfile|Taskfile)/.test(c.source)) ? \"make\"\n : \"ci\"\n\n const { checks, evidence } = pickByPriority(candidates)\n if (checks.length === 0) return null\n // Require at least one STATIC check (typecheck/lint/build) unless the caller\n // opted into running the full test suite — a test-only set is otherwise either\n // off-by-default or a slow/false-red risk on every stop.\n const hasStatic = checks.some((c) => STATIC_IDS.has(c.id as CheckId))\n if (!hasStatic && !opts.includeTests) return null\n return { kind: \"parsed\", checks, ecosystem, workdir: root, evidence }\n}\n\n/** The checks a descriptor runs: a sealed descriptor resolves its sealed command\n * set from the registry; parsed/discovered carry their own. Fresh array. */\nexport function checksForDescriptor(d: GateDescriptor): CheckSpec[] {\n if (d.kind === \"sealed\") {\n const sealed = resolveSealedGate(d.gateId)\n return sealed ? sealed.checks.map((c) => ({ id: c.id, command: c.command })) : []\n }\n return d.checks.map((c) => ({ id: c.id, command: c.command }))\n}\n\n/** A stable key over a descriptor's effective check set, for baseline isolation.\n * Sealed descriptors key on their gate id (preserving legacy baseline keys);\n * parsed/discovered key on the canonicalized (id,command) set, so a changed\n * command set yields a fresh baseline instead of masking/inventing regressions. */\nexport function descriptorHash(d: GateDescriptor): string {\n if (d.kind === \"sealed\") return `sealed:${d.gateId}`\n const canon = [...d.checks]\n .map((c) => `${c.id}\u0000${c.command.trim().replace(/\\s+/g, \" \")}`)\n .sort()\n .join(\"\u0001\")\n return `${d.kind}:${createHash(\"sha256\").update(canon).digest(\"hex\").slice(0, 32)}`\n}\n","/**\n * Evidence-pinned model FALLBACK for the structural Stop-gate — the last resort\n * when the deterministic parser (`harness-parse`) finds no runnable checks.\n *\n * A read-only worker reads the repo's own config/docs and proposes the canonical\n * check commands. Two guards keep this safe despite being model-authored:\n * 1. SANITIZE — `sanitizeDiscoveredCheck` rejects shell metacharacters (so a\n * command can never chain/redirect/expand), destructive/stateful verbs,\n * interactive/watch shapes, and an executable that isn't on PATH. What\n * survives is a single plain argv line, safe for `liveExec`'s naive split.\n * 2. EVIDENCE-PIN — a surviving command must appear (whitespace-normalized)\n * VERBATIM in one of the collected source files. The model cannot invent a\n * command or be prompt-injected into emitting one that isn't already a real\n * command in the repo (and a real command in a user-trusted repo is the\n * same authority the existing gate already runs).\n *\n * Discovery runs ONCE at launch and the result is cached per (repoFingerprint,\n * sourcesHash) in a human-readable record. The runtime Stop hook only READS the\n * cached record (no model call at stop). This is consumed ONLY by the local Stop\n * hook; the sealed-gate kernel never sees a discovered command.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { existsSync, promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { resolveExecutable } from \"~/lib/exec\"\nimport { PATHS } from \"~/lib/paths\"\n\nimport { type CheckSpec } from \"./gate-runner\"\nimport { isMutatingCommand, isSafeCommand, type CheckId } from \"./harness-parse\"\nimport { repoFingerprint, repoRoot } from \"./stop-gate-policy\"\n\n/** The allowlist of files the discovery worker is steered to read — config +\n * docs that legitimately describe how to check a project. Secret files are\n * additionally blocked at the worker IO layer (`.env*`/`*.pem`/`id_*`/…). */\nconst SIGNAL_FILES: ReadonlyArray<string> = [\n \"package.json\",\n \"Makefile\",\n \"makefile\",\n \"justfile\",\n \".justfile\",\n \"Taskfile.yml\",\n \"Taskfile.yaml\",\n \"Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"setup.cfg\",\n \"tox.ini\",\n \"pytest.ini\",\n \"CONTRIBUTING.md\",\n \"CONTRIBUTING\",\n \"README.md\",\n \"README\",\n \"DEVELOPING.md\",\n \"mix.exs\",\n \"build.gradle\",\n \"pom.xml\",\n \"composer.json\",\n]\nconst SIGNAL_DIRS: ReadonlyArray<string> = [\".github/workflows\"]\n\n/** A discovered check command after sanitization + evidence-pinning. */\nexport interface DiscoveredRecord {\n root: string\n fingerprint: string\n sourcesHash: string\n discoveredAt: string\n model: string\n ecosystem: string\n checks: CheckSpec[]\n confidence: string\n evidence: string[]\n}\n\nconst MAX_SIGNAL_BYTES = 64 * 1024\n/** Global caps so a repo with many workflow files can't inflate discovery /\n * hashing cost: at most this many files and this much total text. */\nconst MAX_SIGNAL_FILES = 40\nconst MAX_TOTAL_SIGNAL_BYTES = 512 * 1024\n\n/** Words that mark a command as destructive, stateful, or non-terminating — a\n * \"check\" must never do any of these. (Shell operators are already rejected by\n * `isSafeCommand`, so chaining can't smuggle them past this word scan.) */\nconst DENY_WORD =\n /\\b(rm|rmdir|mv|dd|mkfs|sudo|chmod|chown|publish|push|deploy|migrate|kubectl|terraform|docker|curl|wget|ssh|scp|rsync|nc|eval|npm i|npm install|yarn add|pip install|apt|brew|watch|serve|repl|dev|start)\\b/i\n\n/**\n * True when `command` is safe to auto-run as a check: a plain argv line (no\n * shell metacharacters), no destructive/stateful verb, no mutating/interactive\n * shape, and its executable resolves on PATH. The deny-word scan runs on a\n * whitespace-NORMALIZED copy so a tab / double-space can't split `npm install`\n * past the literal-space patterns.\n */\nexport function sanitizeDiscoveredCheck(command: string): boolean {\n if (!isSafeCommand(command)) return false\n const norm = normalizeWs(command)\n if (DENY_WORD.test(norm)) return false\n if (isMutatingCommand(norm)) return false // --fix/--write/--watch + :fix/:write scripts\n const first = norm.split(\" \")[0] ?? \"\"\n return resolveExecutable(first, { env: process.env }) !== null\n}\n\nfunction normalizeWs(s: string): string {\n return s.replace(/\\s+/g, \" \").trim()\n}\n\n/** Collect the text of the allowlisted signal files (capped), for evidence-pin +\n * the sources hash. Returns the concatenated text and the relative file list. */\nasync function collectSignals(root: string): Promise<{ text: string; files: string[] }> {\n const parts: string[] = []\n const files: string[] = []\n let total = 0\n const readCapped = async (abs: string, rel: string): Promise<void> => {\n if (files.length >= MAX_SIGNAL_FILES || total >= MAX_TOTAL_SIGNAL_BYTES) return\n try {\n const raw = await fs.readFile(abs, \"utf8\")\n const slice = raw.length > MAX_SIGNAL_BYTES ? raw.slice(0, MAX_SIGNAL_BYTES) : raw\n parts.push(slice)\n files.push(rel)\n total += slice.length\n } catch {\n /* unreadable → skip */\n }\n }\n for (const f of SIGNAL_FILES) {\n const abs = nodePath.join(root, f)\n if (existsSync(abs)) await readCapped(abs, f)\n }\n for (const d of SIGNAL_DIRS) {\n const dir = nodePath.join(root, d)\n try {\n for (const name of await fs.readdir(dir)) {\n if (/\\.(ya?ml)$/i.test(name)) await readCapped(nodePath.join(dir, name), nodePath.join(d, name))\n }\n } catch {\n /* no dir */\n }\n }\n return { text: parts.join(\"\\n\"), files }\n}\n\n/** A freshness hash over the signal files' contents + relative paths. A change\n * to how the project checks itself flips this → re-discovery on next launch. */\nexport async function sourcesHash(root: string): Promise<string> {\n const { text, files } = await collectSignals(root)\n return createHash(\"sha256\")\n .update(files.sort().join(\"\\n\"))\n .update(\"\\0\")\n .update(text)\n .digest(\"hex\")\n}\n\nfunction discoveredDir(): string {\n return nodePath.join(PATHS.APP_DIR, \"stop-gate\", \"discovered\")\n}\nfunction recordPathFor(root: string): string {\n return nodePath.join(discoveredDir(), createHash(\"sha256\").update(nodePath.resolve(root)).digest(\"hex\").slice(0, 32))\n}\n\n/** Read the cached discovered record for `root`, verifying it still matches the\n * live repo identity AND the live sources hash. Any mismatch / unreadable /\n * empty-checks record → null (re-discover or stay off; never run a stale set). */\nexport async function readDiscoveredGate(root: string): Promise<DiscoveredRecord | null> {\n let rec: DiscoveredRecord\n try {\n rec = JSON.parse(await fs.readFile(recordPathFor(root), \"utf8\")) as DiscoveredRecord\n } catch {\n return null\n }\n if (!rec || !Array.isArray(rec.checks) || rec.checks.length === 0) return null\n const fp = await repoFingerprint(root).catch(() => \"\")\n if (fp.length === 0 || fp !== rec.fingerprint) return null // identity drift → deny.\n const sh = await sourcesHash(root).catch(() => \"\")\n if (sh.length === 0 || sh !== rec.sourcesHash) return null // config changed → stale.\n // Re-validate id AND re-sanitize the command at read time (defense-in-depth\n // against a tampered/corrupt record).\n const checks = rec.checks.filter(\n (c) => c && typeof c.id === \"string\" && VALID_IDS.has(c.id) && typeof c.command === \"string\" && sanitizeDiscoveredCheck(c.command),\n )\n if (checks.length === 0) return null\n return { ...rec, checks }\n}\n\nexport async function writeDiscoveredGate(rec: DiscoveredRecord): Promise<void> {\n await fs.mkdir(discoveredDir(), { recursive: true })\n const tmp = `${recordPathFor(rec.root)}.${process.pid}.tmp`\n await fs.writeFile(tmp, `${JSON.stringify(rec, null, 2)}\\n`, { mode: 0o600 })\n await fs.rename(tmp, recordPathFor(rec.root))\n}\n\nconst DISCOVERY_PROMPT = (files: string[]): string =>\n `You are configuring an automated pre-finish CHECK gate for this repository. Read ONLY these `\n + `already-present config/doc files to learn how the project checks itself: ${files.join(\", \")}. `\n + `Identify the canonical FAST static check command (typecheck and/or lint) and, if obvious, the test `\n + `command. Rules: (1) return a command ONLY if it appears VERBATIM in one of those files — never invent `\n + `one; (2) commands must be non-interactive, self-terminating, read-only verification (NO install / `\n + `publish / push / deploy / migrate / format-in-place / watch / serve / dev-server / delete); (3) at most `\n + `3 commands; (4) if unsure, return an empty list. Respond with ONLY a fenced \\`\\`\\`json block of the shape `\n + `{\"ecosystem\":\"<label>\",\"checks\":[{\"id\":\"typecheck|lint|test\",\"command\":\"<single-line command>\"}],`\n + `\"confidence\":\"high|low\"} and NOTHING else.`\n\ninterface DiscoverResult {\n ecosystem: string\n checks: CheckSpec[]\n confidence: string\n evidence: string[]\n}\n\n/** Extract the first fenced/bare JSON object from the worker's text. */\nfunction extractJson(text: string): unknown {\n const fenced = /```(?:json)?\\s*([\\s\\S]*?)```/i.exec(text)\n const body = fenced ? fenced[1] : text\n const start = body.indexOf(\"{\")\n const end = body.lastIndexOf(\"}\")\n if (start < 0 || end <= start) return undefined\n try {\n return JSON.parse(body.slice(start, end + 1)) as unknown\n } catch {\n return undefined\n }\n}\n\nconst VALID_IDS: ReadonlySet<string> = new Set<CheckId>([\"typecheck\", \"lint\", \"test\"])\n\n/**\n * Sanitize + EVIDENCE-PIN the model's raw checks against the collected source\n * text. Pure (no IO) so it is unit-testable without the worker. A check survives\n * only if: its id is canonical, it isn't a `test` while tests are off, its id\n * hasn't already been taken, it passes `sanitizeDiscoveredCheck`, AND its command\n * appears (whitespace-normalized) VERBATIM in `evidenceText`. The evidence-pin is\n * the load-bearing guard: the model cannot invent a command or be prompt-injected\n * into one that isn't already a real command in the (user-trusted) repo.\n */\nexport function filterDiscoveredChecks(\n rawChecks: unknown,\n evidenceText: string,\n includeTests: boolean,\n): CheckSpec[] {\n const evidenceNorm = normalizeWs(evidenceText)\n const seen = new Set<string>()\n const checks: CheckSpec[] = []\n for (const c of Array.isArray(rawChecks) ? rawChecks : []) {\n if (!c || typeof c !== \"object\") continue\n const id = (c as { id?: unknown }).id\n const command = (c as { command?: unknown }).command\n if (typeof id !== \"string\" || !VALID_IDS.has(id)) continue\n if (typeof command !== \"string\") continue\n if (id === \"test\" && !includeTests) continue\n if (seen.has(id)) continue\n if (!sanitizeDiscoveredCheck(command)) continue\n if (!evidenceNorm.includes(normalizeWs(command))) continue\n seen.add(id)\n checks.push({ id, command: command.trim() })\n }\n return checks\n}\n\n/**\n * Run the read-only worker to discover check commands for `cwd`. Returns the\n * sanitized + evidence-pinned result, or null (worker unavailable / errored /\n * nothing survived). NEVER throws.\n */\nexport async function discoverGateCommands(\n cwd: string,\n opts: { signal?: AbortSignal; includeTests: boolean },\n): Promise<DiscoverResult | null> {\n const root = await repoRoot(cwd).catch(() => cwd)\n const { text: evidenceText, files } = await collectSignals(root)\n if (files.length === 0) return null\n\n let result: { text: string; isError?: boolean }\n try {\n // Lazy-import the heavy worker-agent engine ONLY when discovery actually\n // runs — keeping it out of the module-load graph of `claude.ts` (which\n // imports this module), so the mock.module-isolated CLI tests don't deadlock\n // pulling the Pi runtime at load time.\n const { runWorkerAgent } = await import(\"~/lib/worker-agent/engine\")\n // Discovery is advisory setup, not a verified workflow producer/checker,\n // so honoring the operator's explore default here is intentional.\n result = await runWorkerAgent({\n mode: \"explore\",\n workspace: root,\n prompt: DISCOVERY_PROMPT(files),\n signal: opts.signal,\n })\n } catch {\n return null\n }\n if (result.isError) return null\n const parsed = extractJson(result.text)\n if (!parsed || typeof parsed !== \"object\") return null\n const obj = parsed as { ecosystem?: unknown; checks?: unknown; confidence?: unknown }\n const checks = filterDiscoveredChecks(obj.checks, evidenceText, opts.includeTests)\n if (checks.length === 0) return null\n const ecosystem = typeof obj.ecosystem === \"string\" && obj.ecosystem.length > 0 ? obj.ecosystem : \"discovered\"\n const confidence = typeof obj.confidence === \"string\" ? obj.confidence : \"low\"\n return { ecosystem, checks, confidence, evidence: files }\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { D as githubHeaders, O as state, b as GITHUB_API_BASE_URL, g as fetchWithTransientRetry, v as HTTPError } from "./token-R2ocFC3M.js";
2
+ //#region src/services/github/get-copilot-usage.ts
3
+ const getCopilotUsage = async () => {
4
+ const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(state) }), { label: "/copilot_internal/user" });
5
+ if (!response.ok) throw new HTTPError("Failed to get Copilot usage", response);
6
+ return await response.json();
7
+ };
8
+ //#endregion
9
+ export { getCopilotUsage as t };
10
+
11
+ //# sourceMappingURL=get-copilot-usage-DD1N0Xlh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-copilot-usage-DD1N0Xlh.js","names":[],"sources":["../src/services/github/get-copilot-usage.ts"],"sourcesContent":["import { GITHUB_API_BASE_URL, githubHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\nimport { fetchWithTransientRetry } from \"~/lib/upstream-retry\"\n\nexport const getCopilotUsage = async (): Promise<CopilotUsageResponse> => {\n // GitHub PAT GET — retry transient 429/5xx/network; a 401 (bad PAT) fails\n // fast (not retried by the helper).\n const response = await fetchWithTransientRetry(\n () =>\n fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, {\n headers: githubHeaders(state),\n }),\n { label: \"/copilot_internal/user\" },\n )\n\n if (!response.ok) {\n throw new HTTPError(\"Failed to get Copilot usage\", response)\n }\n\n return (await response.json()) as CopilotUsageResponse\n}\n\nexport interface QuotaDetail {\n entitlement: number\n overage_count: number\n overage_permitted: boolean\n percent_remaining: number\n quota_id: string\n quota_remaining: number\n remaining: number\n unlimited: boolean\n}\n\ninterface QuotaSnapshots {\n chat: QuotaDetail\n completions: QuotaDetail\n premium_interactions: QuotaDetail\n}\n\ninterface CopilotUsageResponse {\n access_type_sku: string\n analytics_tracking_id: string\n assigned_date: string\n can_signup_for_limited: boolean\n chat_enabled: boolean\n copilot_plan: string\n organization_login_list: Array<unknown>\n organization_list: Array<unknown>\n quota_reset_date: string\n quota_snapshots: QuotaSnapshots\n}\n"],"mappings":";;AAKA,MAAa,kBAAkB,YAA2C;CAGxE,MAAM,WAAW,MAAM,8BAEnB,MAAM,GAAG,oBAAoB,yBAAyB,EACpD,SAAS,cAAc,KAAK,EAC9B,CAAC,GACH,EAAE,OAAO,yBAAyB,CACpC;CAEA,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,UAAU,+BAA+B,QAAQ;CAG7D,OAAQ,MAAM,SAAS,KAAK;AAC9B"}
@@ -0,0 +1,136 @@
1
+ import { clearTimeout, setTimeout } from "node:timers";
2
+ //#region src/lib/orchestration/hook-mcp-client.ts
3
+ /**
4
+ * Tiny MCP-over-HTTP + inference client for the advisory-review hooks (hook V2).
5
+ *
6
+ * The hook subcommands (`internal-stop-review`, `internal-prompt-submit`) run as
7
+ * SEPARATE short-lived processes spawned by Claude Code — they have no in-process
8
+ * proxy `state`. They reach the running github-router proxy over loopback HTTP:
9
+ *
10
+ * - `callMcpTool` POSTs a JSON-RPC `tools/call` to `${serverUrl}/mcp/<group>`
11
+ * with the per-launch `Authorization: Bearer <nonce>` (the same nonce the
12
+ * proxy minted in `writePeerMcpRuntimeFiles` and validates in the /mcp
13
+ * handler). Used for `workers/review` (the background reviewer) and
14
+ * `search/code` (the prompt-hook grounding search). Forces the JSON response
15
+ * path (`Accept: application/json`) — the SSE path's 60s-ceiling workaround
16
+ * is a Claude-Code-MCP-client concern; our own fetch has no such cap, so the
17
+ * simpler single-body JSON path is correct here.
18
+ *
19
+ * - `callInference` POSTs to `${serverUrl}/v1/responses` (no nonce — the /v1/*
20
+ * passthrough authenticates upstream with the proxy's own Copilot token) for
21
+ * the single gpt-5.6-sol scope/goal call the prompt hook makes.
22
+ *
23
+ * Everything here is best-effort and caller-fail-open: a transport error,
24
+ * non-2xx, timeout, or malformed body throws, and every caller wraps the call so
25
+ * the hook degrades gracefully (no findings / the regex-heuristic goal) rather
26
+ * than disrupting the session.
27
+ */
28
+ /**
29
+ * Read the proxy URL + nonce the launcher injected into the spawned child env
30
+ * (`GH_ROUTER_HOOK_MCP_URL` / `GH_ROUTER_HOOK_NONCE`). Returns undefined when
31
+ * either is absent — the hook then skips its LLM layer and falls back to its
32
+ * deterministic / regex behavior.
33
+ */
34
+ function hookMcpRuntimeFromEnv(env = process.env) {
35
+ const serverUrl = (env.GH_ROUTER_HOOK_MCP_URL ?? "").trim();
36
+ const nonce = (env.GH_ROUTER_HOOK_NONCE ?? "").trim();
37
+ if (serverUrl.length === 0 || nonce.length === 0) return void 0;
38
+ return {
39
+ serverUrl,
40
+ nonce
41
+ };
42
+ }
43
+ /**
44
+ * POST a JSON-RPC `tools/call` and return the tool's text + isError. Throws on
45
+ * any transport/HTTP/parse failure (caller fails open). A JSON-RPC `error`
46
+ * envelope is mapped to `{ text: message, isError: true }` (a well-formed
47
+ * negative result, not a transport failure).
48
+ */
49
+ async function callMcpTool(opts) {
50
+ const body = await postJson(`${opts.runtime.serverUrl.replace(/\/+$/, "")}/mcp/${opts.group}`, {
51
+ jsonrpc: "2.0",
52
+ id: 1,
53
+ method: "tools/call",
54
+ params: {
55
+ name: opts.tool,
56
+ arguments: opts.args
57
+ }
58
+ }, {
59
+ timeoutMs: opts.timeoutMs,
60
+ signal: opts.signal,
61
+ headers: { Authorization: `Bearer ${opts.runtime.nonce}` }
62
+ });
63
+ const rpc = body && typeof body === "object" ? body : {};
64
+ if (rpc.error) return {
65
+ text: rpc.error.message ?? "MCP error",
66
+ isError: true
67
+ };
68
+ return {
69
+ text: (Array.isArray(rpc.result?.content) ? rpc.result.content : []).filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join(""),
70
+ isError: rpc.result?.isError === true
71
+ };
72
+ }
73
+ /**
74
+ * One non-streaming gpt-5.6-sol (or any model id) inference via `/v1/responses`.
75
+ * Returns the assistant text (possibly empty). Throws on transport/HTTP/parse
76
+ * failure. `effort` maps to the Responses `reasoning.effort` knob.
77
+ */
78
+ async function callInference(opts) {
79
+ const body = await postJson(`${opts.serverUrl.replace(/\/+$/, "")}/v1/responses`, {
80
+ model: opts.model,
81
+ instructions: opts.instructions,
82
+ input: [{
83
+ role: "user",
84
+ content: [{
85
+ type: "input_text",
86
+ text: opts.input
87
+ }]
88
+ }],
89
+ stream: false,
90
+ reasoning: { effort: opts.effort }
91
+ }, {
92
+ timeoutMs: opts.timeoutMs,
93
+ signal: opts.signal
94
+ });
95
+ const out = [];
96
+ const items = Array.isArray(body?.output) ? body.output : [];
97
+ for (const item of items) {
98
+ if (item?.type !== "message" || item.role !== "assistant") continue;
99
+ const parts = Array.isArray(item.content) ? item.content : [];
100
+ for (const part of parts) if ((part?.type === "output_text" || part?.type === "text") && typeof part.text === "string") out.push(part.text);
101
+ }
102
+ return out.join("");
103
+ }
104
+ /**
105
+ * POST `payload` as JSON with a hard timeout, returning the parsed JSON body.
106
+ * Throws on non-2xx, network error, timeout (AbortController), or non-JSON body.
107
+ * An external `signal` is honored alongside the internal timeout.
108
+ */
109
+ async function postJson(url, payload, opts) {
110
+ const controller = new AbortController();
111
+ const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("hook MCP request timed out")), opts.timeoutMs);
112
+ const onExternalAbort = () => controller.abort(/* @__PURE__ */ new Error("hook MCP request aborted"));
113
+ if (opts.signal) if (opts.signal.aborted) onExternalAbort();
114
+ else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
115
+ try {
116
+ const res = await fetch(url, {
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ Accept: "application/json",
121
+ ...opts.headers
122
+ },
123
+ body: JSON.stringify(payload),
124
+ signal: controller.signal
125
+ });
126
+ if (!res.ok) throw new Error(`hook MCP request failed: HTTP ${res.status}`);
127
+ return await res.json();
128
+ } finally {
129
+ clearTimeout(timer);
130
+ if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
131
+ }
132
+ }
133
+ //#endregion
134
+ export { callMcpTool as n, hookMcpRuntimeFromEnv as r, callInference as t };
135
+
136
+ //# sourceMappingURL=hook-mcp-client-DBvm9608.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-mcp-client-DBvm9608.js","names":["setTimer"],"sources":["../src/lib/orchestration/hook-mcp-client.ts"],"sourcesContent":["/**\n * Tiny MCP-over-HTTP + inference client for the advisory-review hooks (hook V2).\n *\n * The hook subcommands (`internal-stop-review`, `internal-prompt-submit`) run as\n * SEPARATE short-lived processes spawned by Claude Code — they have no in-process\n * proxy `state`. They reach the running github-router proxy over loopback HTTP:\n *\n * - `callMcpTool` POSTs a JSON-RPC `tools/call` to `${serverUrl}/mcp/<group>`\n * with the per-launch `Authorization: Bearer <nonce>` (the same nonce the\n * proxy minted in `writePeerMcpRuntimeFiles` and validates in the /mcp\n * handler). Used for `workers/review` (the background reviewer) and\n * `search/code` (the prompt-hook grounding search). Forces the JSON response\n * path (`Accept: application/json`) — the SSE path's 60s-ceiling workaround\n * is a Claude-Code-MCP-client concern; our own fetch has no such cap, so the\n * simpler single-body JSON path is correct here.\n *\n * - `callInference` POSTs to `${serverUrl}/v1/responses` (no nonce — the /v1/*\n * passthrough authenticates upstream with the proxy's own Copilot token) for\n * the single gpt-5.6-sol scope/goal call the prompt hook makes.\n *\n * Everything here is best-effort and caller-fail-open: a transport error,\n * non-2xx, timeout, or malformed body throws, and every caller wraps the call so\n * the hook degrades gracefully (no findings / the regex-heuristic goal) rather\n * than disrupting the session.\n */\n\nimport { setTimeout as setTimer, clearTimeout as clearTimer } from \"node:timers\"\n\n/** The proxy URL + per-launch nonce, read from the child env the launcher set. */\nexport interface HookMcpRuntime {\n serverUrl: string\n nonce: string\n}\n\n/**\n * Read the proxy URL + nonce the launcher injected into the spawned child env\n * (`GH_ROUTER_HOOK_MCP_URL` / `GH_ROUTER_HOOK_NONCE`). Returns undefined when\n * either is absent — the hook then skips its LLM layer and falls back to its\n * deterministic / regex behavior.\n */\nexport function hookMcpRuntimeFromEnv(env: NodeJS.ProcessEnv = process.env): HookMcpRuntime | undefined {\n const serverUrl = (env.GH_ROUTER_HOOK_MCP_URL ?? \"\").trim()\n const nonce = (env.GH_ROUTER_HOOK_NONCE ?? \"\").trim()\n if (serverUrl.length === 0 || nonce.length === 0) return undefined\n return { serverUrl, nonce }\n}\n\ninterface JsonRpcResponse {\n jsonrpc?: string\n id?: number | string | null\n result?: {\n content?: Array<{ type?: string; text?: string }>\n isError?: boolean\n }\n error?: { code?: number; message?: string }\n}\n\nexport interface McpToolResult {\n /** Concatenated text from `result.content[].text`. */\n text: string\n /** True when the tool reported a failure (`result.isError`) or a JSON-RPC error. */\n isError: boolean\n}\n\n/**\n * POST a JSON-RPC `tools/call` and return the tool's text + isError. Throws on\n * any transport/HTTP/parse failure (caller fails open). A JSON-RPC `error`\n * envelope is mapped to `{ text: message, isError: true }` (a well-formed\n * negative result, not a transport failure).\n */\nexport async function callMcpTool(opts: {\n runtime: HookMcpRuntime\n group: string\n tool: string\n args: Record<string, unknown>\n timeoutMs: number\n signal?: AbortSignal\n}): Promise<McpToolResult> {\n const url = `${opts.runtime.serverUrl.replace(/\\/+$/, \"\")}/mcp/${opts.group}`\n const payload = {\n jsonrpc: \"2.0\",\n id: 1,\n method: \"tools/call\",\n params: { name: opts.tool, arguments: opts.args },\n }\n const body = await postJson(url, payload, {\n timeoutMs: opts.timeoutMs,\n signal: opts.signal,\n headers: { Authorization: `Bearer ${opts.runtime.nonce}` },\n })\n const rpc = (body && typeof body === \"object\" ? body : {}) as JsonRpcResponse\n if (rpc.error) {\n return { text: rpc.error.message ?? \"MCP error\", isError: true }\n }\n const content = Array.isArray(rpc.result?.content) ? rpc.result.content : []\n const text = content\n .filter((p) => p && p.type === \"text\" && typeof p.text === \"string\")\n .map((p) => p.text as string)\n .join(\"\")\n return { text, isError: rpc.result?.isError === true }\n}\n\ninterface ResponsesApiShape {\n output?: Array<{\n type?: string\n role?: string\n content?: Array<{ type?: string; text?: string }>\n }>\n}\n\n/**\n * One non-streaming gpt-5.6-sol (or any model id) inference via `/v1/responses`.\n * Returns the assistant text (possibly empty). Throws on transport/HTTP/parse\n * failure. `effort` maps to the Responses `reasoning.effort` knob.\n */\nexport async function callInference(opts: {\n serverUrl: string\n model: string\n instructions: string\n input: string\n effort: \"low\" | \"medium\" | \"high\" | \"xhigh\"\n timeoutMs: number\n signal?: AbortSignal\n}): Promise<string> {\n const url = `${opts.serverUrl.replace(/\\/+$/, \"\")}/v1/responses`\n const payload = {\n model: opts.model,\n instructions: opts.instructions,\n input: [{ role: \"user\", content: [{ type: \"input_text\", text: opts.input }] }],\n stream: false,\n reasoning: { effort: opts.effort },\n }\n const body = (await postJson(url, payload, {\n timeoutMs: opts.timeoutMs,\n signal: opts.signal,\n })) as ResponsesApiShape\n const out: string[] = []\n const items = Array.isArray(body?.output) ? body.output : []\n for (const item of items) {\n if (item?.type !== \"message\" || item.role !== \"assistant\") continue\n const parts = Array.isArray(item.content) ? item.content : []\n for (const part of parts) {\n if ((part?.type === \"output_text\" || part?.type === \"text\") && typeof part.text === \"string\") {\n out.push(part.text)\n }\n }\n }\n return out.join(\"\")\n}\n\n/**\n * POST `payload` as JSON with a hard timeout, returning the parsed JSON body.\n * Throws on non-2xx, network error, timeout (AbortController), or non-JSON body.\n * An external `signal` is honored alongside the internal timeout.\n */\nasync function postJson(\n url: string,\n payload: unknown,\n opts: { timeoutMs: number; signal?: AbortSignal; headers?: Record<string, string> },\n): Promise<unknown> {\n const controller = new AbortController()\n const timer = setTimer(() => controller.abort(new Error(\"hook MCP request timed out\")), opts.timeoutMs)\n const onExternalAbort = (): void => controller.abort(new Error(\"hook MCP request aborted\"))\n if (opts.signal) {\n if (opts.signal.aborted) onExternalAbort()\n else opts.signal.addEventListener(\"abort\", onExternalAbort, { once: true })\n }\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\", ...opts.headers },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n if (!res.ok) {\n throw new Error(`hook MCP request failed: HTTP ${res.status}`)\n }\n return (await res.json()) as unknown\n } finally {\n clearTimer(timer)\n if (opts.signal) opts.signal.removeEventListener(\"abort\", onExternalAbort)\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,319 @@
1
+ import { t as ArtifactClient } from "./client-CQMfroGV.js";
2
+ import { a as isSubagentContext } from "./stop-gate-policy-CHwWbuF_.js";
3
+ import { defineCommand } from "citty";
4
+ import { readFileSync, realpathSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { Marked } from "marked";
8
+ //#region src/lib/artifact/plan-html.ts
9
+ /**
10
+ * Render a plan's markdown into a self-contained, styled HTML document for the
11
+ * ai-or-die Artifact panel. HTML is the canonical review artifact (lavish's
12
+ * model); converting here means the hands-off auto-open shows a formatted,
13
+ * annotatable page instead of raw markdown.
14
+ *
15
+ * Security: the iframe ai-or-die serves this into runs with
16
+ * `allow-scripts allow-same-origin`, so a `<script>` smuggled into the plan
17
+ * markdown could read the per-session asset token. Two defenses:
18
+ * 1. marked is configured to ESCAPE raw HTML tokens (render as visible text).
19
+ * 2. `link`/`image` hrefs with script-bearing schemes (javascript:, vbscript:,
20
+ * non-image data:) are neutralised before rendering.
21
+ * No separate sanitizer/dep needed.
22
+ */
23
+ function escapeHtml(s) {
24
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
25
+ }
26
+ /** Decode the HTML character references a browser would resolve in an attribute
27
+ * value, so an entity-obfuscated scheme (`javascript&colon;`, `j&#97;vascript:`)
28
+ * cannot slip past the scheme check below. Numeric refs cover any letter; the
29
+ * small named map covers the scheme-relevant punctuation. */
30
+ const NAMED_REFS = {
31
+ colon: ":",
32
+ tab: " ",
33
+ newline: "\n",
34
+ sol: "/",
35
+ semi: ";",
36
+ amp: "&",
37
+ lpar: "(",
38
+ rpar: ")"
39
+ };
40
+ function decodeEntities(s) {
41
+ return s.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);?/g, (m, body) => {
42
+ if (body[0] === "#") {
43
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
44
+ return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : m;
45
+ }
46
+ return NAMED_REFS[body.toLowerCase()] ?? m;
47
+ });
48
+ }
49
+ /** Block script-bearing URL schemes (defense-in-depth on top of HTML escaping). */
50
+ function isDangerousUrl(url, isImage) {
51
+ const v = Array.from(decodeEntities(url)).filter((c) => c.charCodeAt(0) > 32).join("").toLowerCase();
52
+ if (v.startsWith("javascript:") || v.startsWith("vbscript:")) return true;
53
+ if (v.startsWith("data:")) return !(isImage && v.startsWith("data:image/") && !v.startsWith("data:image/svg"));
54
+ return false;
55
+ }
56
+ /** A marked instance that escapes raw HTML (raw markup never becomes live). */
57
+ function makeMarked() {
58
+ const m = new Marked({
59
+ gfm: true,
60
+ breaks: false
61
+ });
62
+ m.use({ renderer: { html(token) {
63
+ return escapeHtml(typeof token === "string" ? token : token.text ?? "");
64
+ } } });
65
+ return m;
66
+ }
67
+ /**
68
+ * Recursively neutralise script-bearing link/image hrefs in the lexed token tree.
69
+ * Done as an explicit walk (not marked's `walkTokens` hook) because we render via
70
+ * the low-level `lexer`+`parser` split, which does not run registered walkTokens.
71
+ */
72
+ function neutralizeUrls(tokens) {
73
+ for (const t of tokens) {
74
+ if ((t.type === "link" || t.type === "image") && typeof t.href === "string") {
75
+ if (isDangerousUrl(t.href, t.type === "image")) t.href = "#";
76
+ }
77
+ if (t.tokens) neutralizeUrls(t.tokens);
78
+ if (t.items) neutralizeUrls(t.items);
79
+ if (t.header) {
80
+ for (const cell of t.header) if (cell.tokens) neutralizeUrls(cell.tokens);
81
+ }
82
+ if (t.rows) {
83
+ for (const row of t.rows) for (const cell of row) if (cell.tokens) neutralizeUrls(cell.tokens);
84
+ }
85
+ }
86
+ }
87
+ /** Add `data-source-line="N"` to the first opening tag of a rendered block. */
88
+ function tagSourceLine(html, line) {
89
+ return html.replace(/^(\s*)<([a-zA-Z][\w-]*)/, `$1<$2 data-source-line="${line}"`);
90
+ }
91
+ /**
92
+ * Render markdown to inner HTML, tagging each top-level block with its 1-based
93
+ * source line so an annotation in the panel can map back to a plan line.
94
+ */
95
+ function renderMarkdownBody(source) {
96
+ const m = makeMarked();
97
+ let tokens;
98
+ try {
99
+ tokens = m.lexer(source);
100
+ } catch {
101
+ return `<pre>${escapeHtml(source)}</pre>`;
102
+ }
103
+ neutralizeUrls(tokens);
104
+ let line = 1;
105
+ let out = "";
106
+ for (const tok of tokens) {
107
+ let html;
108
+ try {
109
+ html = m.parser([tok]);
110
+ } catch {
111
+ html = "";
112
+ }
113
+ if (html.trim()) out += tagSourceLine(html, line);
114
+ line += tok.raw?.match(/\n/g)?.length ?? 0;
115
+ }
116
+ return out || `<pre>${escapeHtml(source)}</pre>`;
117
+ }
118
+ /**
119
+ * Wrap rendered markdown in a complete, self-contained HTML document with
120
+ * readable typography (works even before ai-or-die injects its annotation SDK).
121
+ */
122
+ function renderPlanHtml(source, title = "Plan") {
123
+ return wrapArtifactDocument(renderMarkdownBody(source), title);
124
+ }
125
+ const BASE_STYLE = ` :root {
126
+ --bg: #0f1115; --fg: #f7f3ea; --muted: #aeb6c6; --border: #2a2f3a;
127
+ --accent: #f4c95d; --code-bg: #171a21;
128
+ --serif: 'Iowan Old Style', Georgia, 'Times New Roman', serif;
129
+ --sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
130
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
131
+ }
132
+ * { box-sizing: border-box; }
133
+ html, body { margin: 0; background: var(--bg); color: var(--fg); }
134
+ body { font-family: var(--serif); font-size: 16px; line-height: 1.6;
135
+ padding: 32px clamp(16px, 5vw, 56px); }
136
+ main { max-width: 78ch; margin: 0 auto; }
137
+ h1, h2, h3, h4 { font-family: var(--sans); line-height: 1.25; margin: 1.6em 0 0.5em; }
138
+ h1 { font-size: 1.9em; } h2 { font-size: 1.45em; padding-bottom: 0.2em;
139
+ border-bottom: 1px solid var(--border); } h3 { font-size: 1.2em; }
140
+ p, li { color: var(--fg); }
141
+ a { color: var(--accent); }
142
+ code { font-family: var(--mono); font-size: 0.9em; background: var(--code-bg);
143
+ padding: 0.12em 0.35em; border-radius: 5px; }
144
+ pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px;
145
+ padding: 12px 14px; overflow: auto; }
146
+ pre code { background: none; padding: 0; }
147
+ blockquote { margin: 1em 0; padding: 0.2em 1em; border-left: 3px solid var(--accent);
148
+ color: var(--muted); }
149
+ table { border-collapse: collapse; width: 100%; margin: 1em 0; font-family: var(--sans);
150
+ font-size: 0.95em; }
151
+ th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
152
+ th { background: var(--code-bg); }
153
+ hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }
154
+ ul, ol { padding-left: 1.4em; }`;
155
+ /**
156
+ * Wrap rendered inner HTML in the self-contained document shell. `extraStyle` is
157
+ * appended to the inline stylesheet so the interactive variant can add its own
158
+ * rules without a second `<style>` block. Static callers pass "" (default), so
159
+ * the static document is byte-identical to the pre-refactor output.
160
+ */
161
+ function wrapArtifactDocument(body, title, extraStyle = "") {
162
+ return `<!doctype html>
163
+ <html lang="en">
164
+ <head>
165
+ <meta charset="utf-8">
166
+ <meta name="viewport" content="width=device-width, initial-scale=1">
167
+ <title>${escapeHtml(title)}</title>
168
+ <style>
169
+ ${BASE_STYLE}${extraStyle}
170
+ </style>
171
+ </head>
172
+ <body>
173
+ <main>
174
+ ${body}
175
+ </main>
176
+ </body>
177
+ </html>
178
+ `;
179
+ }
180
+ //#endregion
181
+ //#region src/internal-artifact-open.ts
182
+ /**
183
+ * The internal `internal-artifact-open` subcommand: invoked by a spawned Claude
184
+ * Code session's `PostToolUse` hook (matcher `ExitPlanMode`), registered into the
185
+ * mirrored settings.json by the launcher when running inside an ai-or-die tab
186
+ * (`AIORDIE_SESSION_ID` set). It auto-opens the just-finalized plan in the
187
+ * ai-or-die Artifact review panel so the human reviews it WITHOUT the model
188
+ * having to call artifact_open itself.
189
+ *
190
+ * Auth: AIORDIE_TOKEN is stripped from the child env, so the token can't come via
191
+ * env or argv (argv leaks to `ps`). The launcher writes a mode-600
192
+ * `.aiordie-artifact.json` into CLAUDE_CONFIG_DIR; this hook reads it.
193
+ *
194
+ * Plan: the EXACT plan for THIS session comes from the ExitPlanMode payload —
195
+ * `tool_input.planFilePath` (per-session; never the globally-newest file in the
196
+ * shared plans dir) with `tool_input.plan` as the markdown fallback. The plan
197
+ * markdown is rendered to a self-contained styled HTML file (HTML is the
198
+ * canonical artifact, lavish's model — formatted + annotatable, not raw md), and
199
+ * THAT `.html` is opened.
200
+ *
201
+ * Side-effect only: never writes stdout, never throws, always exits 0. Skips
202
+ * subagent payloads (agent_id/agent_type) like the other hooks.
203
+ */
204
+ function readStdin() {
205
+ try {
206
+ if (process.stdin.isTTY) return "";
207
+ return readFileSync(0, "utf8");
208
+ } catch {
209
+ return "";
210
+ }
211
+ }
212
+ function credsPath() {
213
+ const dir = process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude");
214
+ return path.join(dir, ".aiordie-artifact.json");
215
+ }
216
+ function isSubagent(payload) {
217
+ try {
218
+ return isSubagentContext(payload);
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+ /** Pull the per-session plan path + markdown content out of the hook payload. */
224
+ function parseExitPlanPayload(raw) {
225
+ try {
226
+ const ti = JSON.parse(raw).tool_input ?? {};
227
+ return {
228
+ planFilePath: typeof ti.planFilePath === "string" && ti.planFilePath.trim() ? ti.planFilePath : void 0,
229
+ planMarkdown: typeof ti.plan === "string" && ti.plan.trim() ? ti.plan : void 0
230
+ };
231
+ } catch {
232
+ return {};
233
+ }
234
+ }
235
+ function readCreds() {
236
+ try {
237
+ const parsed = JSON.parse(readFileSync(credsPath(), "utf8"));
238
+ if (!parsed.baseUrl || !parsed.token || !parsed.sessionId) return void 0;
239
+ return {
240
+ baseUrl: parsed.baseUrl,
241
+ token: parsed.token,
242
+ sessionId: parsed.sessionId,
243
+ insecureTLS: parsed.insecureTLS === true
244
+ };
245
+ } catch {
246
+ return;
247
+ }
248
+ }
249
+ /**
250
+ * Resolve the plan markdown + a title, preferring the file at `planFilePath`
251
+ * (the source of truth, may be newer than the payload's snapshot), falling back
252
+ * to the inline `tool_input.plan` content.
253
+ */
254
+ function resolvePlanMarkdown(p) {
255
+ if (p.planFilePath) try {
256
+ const md = readFileSync(p.planFilePath, "utf8");
257
+ if (md.trim()) return {
258
+ markdown: md,
259
+ title: path.basename(p.planFilePath, path.extname(p.planFilePath)),
260
+ nearPath: p.planFilePath
261
+ };
262
+ } catch {}
263
+ if (p.planMarkdown) return {
264
+ markdown: p.planMarkdown,
265
+ title: "Plan"
266
+ };
267
+ }
268
+ /**
269
+ * Write the rendered HTML next to the plan (an allowed root) when we have a real
270
+ * path, else into the plans dir under CLAUDE_CONFIG_DIR. Returns the realpath of
271
+ * the written `.html`, or undefined on failure.
272
+ */
273
+ function writePlanHtml(html, nearPath) {
274
+ let target;
275
+ if (nearPath) target = `${nearPath.replace(/\.(md|markdown)$/i, "")}.aiordie.html`;
276
+ else {
277
+ const dir = path.join(process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude"), "plans");
278
+ target = path.join(dir, `aiordie-plan-${process.pid}.html`);
279
+ }
280
+ try {
281
+ writeFileSync(target, html, "utf8");
282
+ try {
283
+ return realpathSync.native(target);
284
+ } catch {
285
+ return target;
286
+ }
287
+ } catch {
288
+ return;
289
+ }
290
+ }
291
+ const internalArtifactOpen = defineCommand({
292
+ meta: {
293
+ name: "internal-artifact-open",
294
+ description: "Open the finalized plan in the ai-or-die panel"
295
+ },
296
+ async run() {
297
+ try {
298
+ const raw = readStdin();
299
+ let payload;
300
+ try {
301
+ payload = JSON.parse(raw);
302
+ } catch {
303
+ payload = void 0;
304
+ }
305
+ if (isSubagent(payload)) return;
306
+ const creds = readCreds();
307
+ if (!creds) return;
308
+ const plan = resolvePlanMarkdown(parseExitPlanPayload(raw));
309
+ if (!plan) return;
310
+ const htmlPath = writePlanHtml(renderPlanHtml(plan.markdown, plan.title), plan.nearPath);
311
+ if (!htmlPath) return;
312
+ await new ArtifactClient(creds).open(htmlPath);
313
+ } catch {}
314
+ }
315
+ });
316
+ //#endregion
317
+ export { internalArtifactOpen };
318
+
319
+ //# sourceMappingURL=internal-artifact-open-D2cQ2QCw.js.map