github-router 0.3.314 → 0.3.316

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/{aic-ledger-Cx_Ubon3.js → aic-ledger-BS47yb_C.js} +66 -6
  2. package/dist/aic-ledger-BS47yb_C.js.map +1 -0
  3. package/dist/{attribution-settings-Btc5EZTj.js → attribution-settings-jcsSeVur.js} +120 -21
  4. package/dist/attribution-settings-jcsSeVur.js.map +1 -0
  5. package/dist/{auth-O_UG1ABO.js → auth-JfxL9q7z.js} +4 -4
  6. package/dist/{auth-O_UG1ABO.js.map → auth-JfxL9q7z.js.map} +1 -1
  7. package/dist/browser-ext/manifest.json +1 -1
  8. package/dist/{check-usage-dicJOWML.js → check-usage-DXUEQzt_.js} +4 -4
  9. package/dist/{check-usage-dicJOWML.js.map → check-usage-DXUEQzt_.js.map} +1 -1
  10. package/dist/{claude-D0vuAqTV.js → claude-CJ4Nd2_3.js} +116 -45
  11. package/dist/claude-CJ4Nd2_3.js.map +1 -0
  12. package/dist/{codex-BNm9D0iJ.js → codex-CwuLqNZF.js} +6 -6
  13. package/dist/codex-CwuLqNZF.js.map +1 -0
  14. package/dist/copilot-discount-IbXR5A28.js +220 -0
  15. package/dist/copilot-discount-IbXR5A28.js.map +1 -0
  16. package/dist/{debug-2PgImRnr.js → debug-CA7pg8V9.js} +2 -2
  17. package/dist/{debug-2PgImRnr.js.map → debug-CA7pg8V9.js.map} +1 -1
  18. package/dist/engine-DwrxceOS.js +2 -0
  19. package/dist/{exec-y8C_MU8A.js → exec-COUT4QsS.js} +2 -2
  20. package/dist/{exec-y8C_MU8A.js.map → exec-COUT4QsS.js.map} +1 -1
  21. package/dist/{gate-discovery-CO2njGBB.js → gate-discovery-z04ywOzB.js} +6 -6
  22. package/dist/{gate-discovery-CO2njGBB.js.map → gate-discovery-z04ywOzB.js.map} +1 -1
  23. package/dist/{get-copilot-usage-Cjy8TiFf.js → get-copilot-usage-BMD_cJNT.js} +3 -3
  24. package/dist/{get-copilot-usage-Cjy8TiFf.js.map → get-copilot-usage-BMD_cJNT.js.map} +1 -1
  25. package/dist/hooks.mjs +490 -28
  26. package/dist/hooks.sha256 +1 -1
  27. package/dist/internal-aic-status-BN0xRLSF.js +432 -0
  28. package/dist/internal-aic-status-BN0xRLSF.js.map +1 -0
  29. package/dist/{internal-artifact-open-CxeIfsW_.js → internal-artifact-open-B3stUu74.js} +2 -2
  30. package/dist/{internal-artifact-open-CxeIfsW_.js.map → internal-artifact-open-B3stUu74.js.map} +1 -1
  31. package/dist/{internal-fast-dispatch-guard-D8YNlnuQ.js → internal-fast-dispatch-guard-2QjUlhqO.js} +1 -1
  32. package/dist/{internal-fast-dispatch-guard-19xQEouu.js → internal-fast-dispatch-guard-CAwYj_Yz.js} +2 -2
  33. package/dist/{internal-fast-dispatch-guard-19xQEouu.js.map → internal-fast-dispatch-guard-CAwYj_Yz.js.map} +1 -1
  34. package/dist/{internal-first-mate-guard-BuweUgjb.js → internal-first-mate-guard-B0rbLb0L.js} +3 -3
  35. package/dist/{internal-first-mate-guard-BuweUgjb.js.map → internal-first-mate-guard-B0rbLb0L.js.map} +1 -1
  36. package/dist/{internal-first-mate-guard-Nf1H0mCK.js → internal-first-mate-guard-VgnHBp0s.js} +1 -1
  37. package/dist/{internal-max-dispatch-guard-DZ7KLWxP.js → internal-max-dispatch-guard-BNqQ2wMc.js} +3 -3
  38. package/dist/{internal-max-dispatch-guard-DZ7KLWxP.js.map → internal-max-dispatch-guard-BNqQ2wMc.js.map} +1 -1
  39. package/dist/{internal-max-dispatch-guard-_Pp4LltM.js → internal-max-dispatch-guard-Bz7a2Ntf.js} +1 -1
  40. package/dist/{internal-plan-review-DLduDz41.js → internal-plan-review-QUvNZmMR.js} +3 -3
  41. package/dist/{internal-plan-review-DLduDz41.js.map → internal-plan-review-QUvNZmMR.js.map} +1 -1
  42. package/dist/{internal-prompt-submit-DG94f8E-.js → internal-prompt-submit-fj5ccft_.js} +5 -5
  43. package/dist/{internal-prompt-submit-DG94f8E-.js.map → internal-prompt-submit-fj5ccft_.js.map} +1 -1
  44. package/dist/{internal-session-bind-D6Hpl-84.js → internal-session-bind-0Hmak8PR.js} +2 -2
  45. package/dist/{internal-session-bind-D6Hpl-84.js.map → internal-session-bind-0Hmak8PR.js.map} +1 -1
  46. package/dist/{internal-stop-hook-D56b6iVj.js → internal-stop-hook-UN1JHtjj.js} +6 -6
  47. package/dist/{internal-stop-hook-D56b6iVj.js.map → internal-stop-hook-UN1JHtjj.js.map} +1 -1
  48. package/dist/{internal-stop-review-DlKzZgFm.js → internal-stop-review-8v2llYDB.js} +2 -2
  49. package/dist/{internal-stop-review-DlKzZgFm.js.map → internal-stop-review-8v2llYDB.js.map} +1 -1
  50. package/dist/{internal-worker-guard-D7sfByyo.js → internal-worker-guard-BI4Pl0Ew.js} +2 -2
  51. package/dist/{internal-worker-guard-D7sfByyo.js.map → internal-worker-guard-BI4Pl0Ew.js.map} +1 -1
  52. package/dist/{internal-workspace-header-CU5OjLCB.js → internal-workspace-header-kg9FoNXK.js} +2 -2
  53. package/dist/{internal-workspace-header-CU5OjLCB.js.map → internal-workspace-header-kg9FoNXK.js.map} +1 -1
  54. package/dist/lifecycle-BpqAyaAj.js +2 -0
  55. package/dist/lifecycle-CL2WiyeQ.js +2 -0
  56. package/dist/{lifecycle-CtRA3Y7p.js → lifecycle-Cx1wVrLb.js} +3 -3
  57. package/dist/{lifecycle-CtRA3Y7p.js.map → lifecycle-Cx1wVrLb.js.map} +1 -1
  58. package/dist/{lifecycle-BCnarXJV.js → lifecycle-IwotwedG.js} +2 -2
  59. package/dist/{lifecycle-BCnarXJV.js.map → lifecycle-IwotwedG.js.map} +1 -1
  60. package/dist/main.js +20 -20
  61. package/dist/{max-profile-contract-BcrMlrmo.js → max-profile-contract-Bl7I_EOC.js} +2 -2
  62. package/dist/{max-profile-contract-BcrMlrmo.js.map → max-profile-contract-Bl7I_EOC.js.map} +1 -1
  63. package/dist/{mcp-workspace-header-B60WSR1Y.js → mcp-workspace-header-BYgWioLH.js} +2 -2
  64. package/dist/{mcp-workspace-header-B60WSR1Y.js.map → mcp-workspace-header-BYgWioLH.js.map} +1 -1
  65. package/dist/{models-COuMOfM6.js → models-CVzyU5mJ.js} +3 -3
  66. package/dist/{models-COuMOfM6.js.map → models-CVzyU5mJ.js.map} +1 -1
  67. package/dist/{orchestration-DjLA-lxB.js → orchestration-Dc3mO0G3.js} +3 -3
  68. package/dist/{orchestration-DjLA-lxB.js.map → orchestration-Dc3mO0G3.js.map} +1 -1
  69. package/dist/paths-BXXCVFvA.js +2 -0
  70. package/dist/{paths-De1Qh9Q9.js → paths-CMxlQEfQ.js} +3 -3
  71. package/dist/{paths-De1Qh9Q9.js.map → paths-CMxlQEfQ.js.map} +1 -1
  72. package/dist/{peer-mcp-personas-0_XU6nm9.js → peer-mcp-personas-CQ-0Piyr.js} +175 -54
  73. package/dist/peer-mcp-personas-CQ-0Piyr.js.map +1 -0
  74. package/dist/{plan-review-hook-vL6BYmA8.js → plan-review-hook-BIMwWkh6.js} +4 -4
  75. package/dist/{plan-review-hook-vL6BYmA8.js.map → plan-review-hook-BIMwWkh6.js.map} +1 -1
  76. package/dist/{prompt-submit-hook-DfqufWC1.js → prompt-submit-hook-BcFpjHXg.js} +3 -3
  77. package/dist/{prompt-submit-hook-DfqufWC1.js.map → prompt-submit-hook-BcFpjHXg.js.map} +1 -1
  78. package/dist/{provision-DuCnZJj_.js → provision-_R7ZMygj.js} +5 -5
  79. package/dist/{provision-DuCnZJj_.js.map → provision-_R7ZMygj.js.map} +1 -1
  80. package/dist/{self-invocation-B84JCHps.js → self-invocation-cRl4RqfW.js} +2 -2
  81. package/dist/{self-invocation-B84JCHps.js.map → self-invocation-cRl4RqfW.js.map} +1 -1
  82. package/dist/{serve-DXFVR6A0.js → serve-DU66K5kv.js} +14 -14
  83. package/dist/{serve-DXFVR6A0.js.map → serve-DU66K5kv.js.map} +1 -1
  84. package/dist/{server-setup-CP8nGop3.js → server-setup-iLG8LrfL.js} +168 -30
  85. package/dist/server-setup-iLG8LrfL.js.map +1 -0
  86. package/dist/{start-BHEjESCB.js → start-DQTcm0o_.js} +3 -3
  87. package/dist/{start-BHEjESCB.js.map → start-DQTcm0o_.js.map} +1 -1
  88. package/dist/{state-B8adscCv.js → state-BlMpEFAt.js} +2 -1
  89. package/dist/{state-B8adscCv.js.map → state-BlMpEFAt.js.map} +1 -1
  90. package/dist/{stop-gate-hook-BEU8qQrC.js → stop-gate-hook-BUCxa5Kc.js} +4 -4
  91. package/dist/{stop-gate-hook-BEU8qQrC.js.map → stop-gate-hook-BUCxa5Kc.js.map} +1 -1
  92. package/dist/{stop-gate-policy-qzqXfRxE.js → stop-gate-policy--bpyxxXU.js} +3 -3
  93. package/dist/{stop-gate-policy-qzqXfRxE.js.map → stop-gate-policy--bpyxxXU.js.map} +1 -1
  94. package/dist/{token-CCg-Ov5q.js → token-C1gthnbV.js} +3 -3
  95. package/dist/{token-CCg-Ov5q.js.map → token-C1gthnbV.js.map} +1 -1
  96. package/dist/{worker-dispatch-CH-NN5FW.js → worker-dispatch-DMIdrlsF.js} +2 -2
  97. package/dist/{worker-dispatch-CH-NN5FW.js.map → worker-dispatch-DMIdrlsF.js.map} +1 -1
  98. package/package.json +1 -1
  99. package/dist/aic-ledger-Cx_Ubon3.js.map +0 -1
  100. package/dist/attribution-settings-Btc5EZTj.js.map +0 -1
  101. package/dist/claude-D0vuAqTV.js.map +0 -1
  102. package/dist/codex-BNm9D0iJ.js.map +0 -1
  103. package/dist/engine-DsVZuCrD.js +0 -2
  104. package/dist/internal-aic-status-C_swX3_7.js +0 -95
  105. package/dist/internal-aic-status-C_swX3_7.js.map +0 -1
  106. package/dist/lifecycle-B0USSJvd.js +0 -2
  107. package/dist/lifecycle-CQFjaQc0.js +0 -2
  108. package/dist/paths-DyX6SEnd.js +0 -2
  109. package/dist/peer-mcp-personas-0_XU6nm9.js.map +0 -1
  110. package/dist/server-setup-CP8nGop3.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copilot-discount-IbXR5A28.js","names":[],"sources":["../src/lib/copilot-discount.ts"],"sourcesContent":["/**\n * Approximate discounted USD for a session, from static per-model discount\n * factors applied to the AIC ledger's billed nano-AIU.\n *\n * Background: GitHub's portal lists per-token rates, but the spend dashboard\n * bills several models well below portal (gpt-5.6-sol ≈ 0.28x, the Opus /\n * Sonnet / Codex cluster ≈ 0.6x, gemini-3.7-flash ≈ 0.29x since Sep 2026),\n * while Luna bills long-context portal tiers and Grok / 3.8-flash bill\n * portal-default exactly. No uniform per-lab factor exists (verified across\n * 28d / MTD / 7d / daily windows), so each model carries its own factor.\n *\n * The factors scale NANO-AIU, not tokens × portal rates: nano is already\n * tier-correct upstream (long-context tiers, cache writes), so no tier\n * engine or portal-rate table is needed here. Displayed total is capped:\n * `min(factored, totalCredits × $0.01)` — the cap can only ever lower the\n * number (all factors ≤ 1), so a typo like 2.8-instead-of-0.28 degrades to\n * the unfactored total rather than overstating spend.\n *\n * Staleness: rates move (3.7-flash flipped 1.00x → 0.29x overnight once\n * already). Re-derive monthly from the portal + spend dashboard; drift\n * degrades to cap-bound display, never to overstatement.\n */\n\nimport type { AicSnapshot } from \"./aic-ledger\"\nimport { aicTotalCredits } from \"./aic-ledger\"\n\n/** USD value of one AI credit (GitHub's published conversion). */\nexport const USD_PER_AIC_CREDIT = 0.01\n\nexport interface DiscountFactorRow {\n /** Multiply nano-implied USD by this. */\n factor: number\n /** Why this value: which snapshot window(s) it was derived from. */\n source: string\n}\n\nfunction row(factor: number, source: string): DiscountFactorRow {\n return { factor, source }\n}\n\n/**\n * Static per-model discount factors, keyed by resolved (Copilot-side) model\n * id — the same key the ledger's `perModel` uses. Derived Sep 2026 from the\n * 28d / MTD / 7d / daily spend-dashboard windows vs portal rates.\n */\nexport const DISCOUNT_FACTORS: Readonly<Record<string, DiscountFactorRow>> =\n Object.freeze({\n \"gpt-5.6-sol\": row(0.28, \"0.27-0.29 across 28d/MTD/7d/pre/9-9; write-mix refuted\"),\n \"claude-opus-5\": row(0.62, \"0.61-0.64 across windows; 9/7 1.02 excluded as single-day artifact\"),\n \"claude-sonnet-5\": row(0.66, \"0.62-0.71 across windows\"),\n \"gpt-5.3-codex\": row(0.60, \"0.59-0.62 across windows\"),\n \"gpt-5.4\": row(0.60, \"single snapshot 0.598\"),\n // Regime break: billed 1.00x pre-Sep, 0.29x since. Recheck FIRST at\n // every refresh — likeliest row to have moved again.\n \"gemini-3.7-flash\": row(0.29, \"September regime; pre-Sep 1.00x — volatile\"),\n // No discount: Luna's heat is long-context tiers + ~2% cache writes\n // (both already in nano); Grok / 3.8-flash bill portal-default exactly.\n \"gpt-5.6-luna\": row(1.0, \"long-tier portal + writes, no discount\"),\n \"grok-4.6\": row(1.0, \"portal default, six windows\"),\n \"gemini-3.8-flash\": row(1.0, \"portal exact, six windows to the dollar\"),\n })\n\n/** Discount factor for a model id; 1.0 (no discount) when unlisted. */\nexport function discountFactorFor(modelId: string): number {\n return sanitizeFactor(DISCOUNT_FACTORS[modelId]?.factor)\n}\n\n/**\n * Clamp a factor to usable range. Non-finite or negative factors (corrupt\n * table edit, bad injection) degrade to 1.0 — unfactored but never NaN,\n * never negative on screen.\n */\nfunction sanitizeFactor(raw: unknown): number {\n return typeof raw === \"number\" && Number.isFinite(raw) && raw >= 0 ? raw : 1.0\n}\n\nexport interface DiscountedUsd {\n /** Capped total actually displayed (`~$`). */\n total: number\n /** Per-model factored USD, scaled to the capped total when the cap binds. */\n byModel: Record<string, number>\n /** True when the cap (not the factors) decided the total. */\n capped: boolean\n}\n\n/**\n * Session discounted USD from a ledger snapshot. Total function: never\n * throws (guards non-finite/negative ledger values defensively — a corrupt\n * snapshot must degrade the status line, not crash the hook).\n */\nexport function discountedUsdForSnapshot(snapshot: AicSnapshot): DiscountedUsd {\n return discountedUsdWithFactors(snapshot, DISCOUNT_FACTORS)\n}\n\n/**\n * Factor-injectable core (exported for unit tests — e.g. proving a\n * fat-finger factor can never overstate spend). `factors` falls back to\n * 1.0 per model exactly like the table lookup.\n */\nexport function discountedUsdWithFactors(\n snapshot: AicSnapshot,\n factors: Readonly<Record<string, DiscountFactorRow>>,\n): DiscountedUsd {\n const byModel: Record<string, number> = {}\n let sum = 0\n for (const [model, entry] of Object.entries(snapshot.perModel)) {\n const nano =\n typeof entry.nanoAiu === \"number\" && Number.isFinite(entry.nanoAiu)\n ? Math.max(0, entry.nanoAiu)\n : 0\n const factor = sanitizeFactor(factors[model]?.factor)\n const usd = (nano / 1_000_000_000) * USD_PER_AIC_CREDIT * factor\n byModel[model] = usd\n sum += usd\n }\n const cap = aicTotalCredits(snapshot) * USD_PER_AIC_CREDIT\n // Relative epsilon: per-model float summation can exceed the cap by a\n // final-ulp hair without any factor being wrong — only bind on a real gap.\n if (!(sum > cap * (1 + 1e-9)) || sum <= 0) {\n return { total: sum, byModel, capped: false }\n }\n // Cap binds (fat-finger factor, volatile row, unknown-model overstatement):\n // scale rows down proportionally so the table still sums to its Total.\n const scale = cap / sum\n const scaled: Record<string, number> = {}\n for (const [model, usd] of Object.entries(byModel)) {\n scaled[model] = usd * scale\n }\n return { total: cap, byModel: scaled, capped: true }\n}\n\n/** Format table dollars: always cents (`~$` added by callers that want it). */\nexport function formatTableUsd(usd: number): string {\n return usd.toFixed(2)\n}\n\ninterface CostTableRow {\n model: string\n uncached: number\n cached: number\n writes: number\n output: number\n usd: number\n}\n\n/**\n * Beautiful per-model session cost table (plain text, no ANSI — it lands on\n * stderr / logs). Columns mirror the spend dashboard: Uncached / Cached /\n * [Write] / Cache% / Output / $. The Write column appears only when the\n * session actually wrote cache. `Cache% = cache_read / (cache_read + input)`;\n * writes are excluded (they are new tokens, not hits).\n */\nexport function formatDiscountedCostTable(\n snapshot: AicSnapshot,\n discounted: DiscountedUsd,\n): string {\n const tokens = snapshot.tokensByModel ?? {}\n const models = new Set([...Object.keys(snapshot.perModel), ...Object.keys(tokens)])\n const rows: Array<CostTableRow> = []\n for (const model of models) {\n const t = tokens[model]\n rows.push({\n model,\n uncached: t?.input ?? 0,\n cached: t?.cache_read ?? 0,\n writes: t?.cache_write ?? 0,\n output: t?.output ?? 0,\n usd: discounted.byModel[model] ?? 0,\n })\n }\n if (rows.length === 0) return \"\"\n rows.sort((a, b) => b.usd - a.usd || a.model.localeCompare(b.model))\n\n const showWrites = rows.some((r) => r.writes > 0)\n const cachePct = (cached: number, uncached: number): string => {\n const denom = cached + uncached\n if (denom <= 0) return \"—\"\n return `${((cached / denom) * 100).toFixed(1)}%`\n }\n\n type Cell = { text: string; align: \"left\" | \"right\" }\n const header: Array<Cell> = [\n { text: \"Model\", align: \"left\" },\n { text: \"Uncached\", align: \"right\" },\n { text: \"Cached\", align: \"right\" },\n ]\n if (showWrites) header.push({ text: \"Write\", align: \"right\" })\n header.push(\n { text: \"Cache%\", align: \"right\" },\n { text: \"Output\", align: \"right\" },\n { text: \"$\", align: \"right\" },\n )\n\n const body: Array<Array<Cell>> = rows.map((r) => {\n const cells: Array<Cell> = [\n { text: r.model, align: \"left\" },\n { text: formatCount(r.uncached), align: \"right\" },\n { text: formatCount(r.cached), align: \"right\" },\n ]\n if (showWrites) cells.push({ text: formatCount(r.writes), align: \"right\" })\n cells.push(\n { text: cachePct(r.cached, r.uncached), align: \"right\" },\n { text: formatCount(r.output), align: \"right\" },\n { text: `~$${formatTableUsd(r.usd)}`, align: \"right\" },\n )\n return cells\n })\n\n const totalUncached = rows.reduce((n, r) => n + r.uncached, 0)\n const totalCached = rows.reduce((n, r) => n + r.cached, 0)\n const totalWrites = rows.reduce((n, r) => n + r.writes, 0)\n const totalOutput = rows.reduce((n, r) => n + r.output, 0)\n const totalCells: Array<Cell> = [\n { text: \"Total\", align: \"left\" },\n { text: formatCount(totalUncached), align: \"right\" },\n { text: formatCount(totalCached), align: \"right\" },\n ]\n if (showWrites) {\n totalCells.push({ text: formatCount(totalWrites), align: \"right\" })\n }\n totalCells.push(\n { text: cachePct(totalCached, totalUncached), align: \"right\" },\n { text: formatCount(totalOutput), align: \"right\" },\n { text: `~$${formatTableUsd(discounted.total)}`, align: \"right\" },\n )\n\n const widths = header.map((h, i) => {\n let w = h.text.length\n for (const row of [...body, totalCells]) {\n w = Math.max(w, row[i].text.length)\n }\n return w\n })\n const render = (cells: Array<Cell>): string =>\n cells\n .map((c, i) =>\n c.align === \"left\" ? c.text.padEnd(widths[i]) : c.text.padStart(widths[i]),\n )\n .join(\" \")\n const lines = [\n \"Session cost (discounted actuals, ~$):\",\n render(header),\n ...body.map(render),\n render(totalCells),\n ]\n return lines.join(\"\\n\")\n}\n\n/** Compact count for table cells: `999` → `999`, `15234` → `15.2k`. */\nfunction formatCount(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`\n if (n >= 1000) return `${(n / 1000).toFixed(1)}k`\n return `${n}`\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as PATHS } from "./paths-De1Qh9Q9.js";
1
+ import { t as PATHS } from "./paths-CMxlQEfQ.js";
2
2
  import { defineCommand } from "citty";
3
3
  import consola from "consola";
4
4
  import fs from "node:fs/promises";
@@ -78,4 +78,4 @@ const debug = defineCommand({
78
78
  //#endregion
79
79
  export { debug };
80
80
 
81
- //# sourceMappingURL=debug-2PgImRnr.js.map
81
+ //# sourceMappingURL=debug-CA7pg8V9.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"debug-2PgImRnr.js","names":[],"sources":["../src/debug.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport consola from \"consola\"\nimport fs from \"node:fs/promises\"\nimport os from \"node:os\"\n\nimport { PATHS } from \"./lib/paths\"\n\ninterface DebugInfo {\n version: string\n runtime: {\n name: string\n version: string\n platform: string\n arch: string\n }\n paths: {\n APP_DIR: string\n GITHUB_TOKEN_PATH: string\n }\n tokenExists: boolean\n}\n\ninterface RunDebugOptions {\n json: boolean\n}\n\nasync function getPackageVersion(): Promise<string> {\n try {\n const packageJsonPath = new URL(\"../package.json\", import.meta.url).pathname\n // @ts-expect-error https://github.com/sindresorhus/eslint-plugin-unicorn/blob/v59.0.1/docs/rules/prefer-json-parse-buffer.md\n // JSON.parse() can actually parse buffers\n const packageJson = JSON.parse(await fs.readFile(packageJsonPath)) as {\n version: string\n }\n return packageJson.version\n } catch {\n return \"unknown\"\n }\n}\n\nfunction getRuntimeInfo() {\n const isBun = typeof Bun !== \"undefined\"\n\n return {\n name: isBun ? \"bun\" : \"node\",\n version: isBun ? Bun.version : process.version.slice(1),\n platform: os.platform(),\n arch: os.arch(),\n }\n}\n\nasync function checkTokenExists(): Promise<boolean> {\n try {\n const stats = await fs.stat(PATHS.GITHUB_TOKEN_PATH)\n if (!stats.isFile()) return false\n\n const content = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, \"utf8\")\n return content.trim().length > 0\n } catch {\n return false\n }\n}\n\nasync function getDebugInfo(): Promise<DebugInfo> {\n const [version, tokenExists] = await Promise.all([\n getPackageVersion(),\n checkTokenExists(),\n ])\n\n return {\n version,\n runtime: getRuntimeInfo(),\n paths: {\n APP_DIR: PATHS.APP_DIR,\n GITHUB_TOKEN_PATH: PATHS.GITHUB_TOKEN_PATH,\n },\n tokenExists,\n }\n}\n\nfunction printDebugInfoPlain(info: DebugInfo): void {\n consola.info(`github-router debug\n\nVersion: ${info.version}\nRuntime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})\n\nPaths:\n- APP_DIR: ${info.paths.APP_DIR}\n- GITHUB_TOKEN_PATH: ${info.paths.GITHUB_TOKEN_PATH}\n\nToken exists: ${info.tokenExists ? \"Yes\" : \"No\"}`)\n}\n\nfunction printDebugInfoJson(info: DebugInfo): void {\n console.log(JSON.stringify(info, null, 2))\n}\n\nexport async function runDebug(options: RunDebugOptions): Promise<void> {\n const debugInfo = await getDebugInfo()\n\n if (options.json) {\n printDebugInfoJson(debugInfo)\n } else {\n printDebugInfoPlain(debugInfo)\n }\n}\n\nexport const debug = defineCommand({\n meta: {\n name: \"debug\",\n description: \"Print debug information about the application\",\n },\n args: {\n json: {\n type: \"boolean\",\n default: false,\n description: \"Output debug information as JSON\",\n },\n },\n run({ args }) {\n return runDebug({\n json: args.json,\n })\n },\n})\n"],"mappings":";;;;;;AA4BA,eAAe,oBAAqC;CAClD,IAAI;EACF,MAAM,kBAAkB,IAAI,IAAI,mBAAmB,YAAY,GAAG,CAAC,CAAC;EAMpE,OAHoB,KAAK,MAAM,MAAM,GAAG,SAAS,eAAe,CAG/C,CAAC,CAAC;CACrB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,iBAAiB;CACxB,MAAM,QAAQ,OAAO,QAAQ;CAE7B,OAAO;EACL,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,IAAI,UAAU,QAAQ,QAAQ,MAAM,CAAC;EACtD,UAAU,GAAG,SAAS;EACtB,MAAM,GAAG,KAAK;CAChB;AACF;AAEA,eAAe,mBAAqC;CAClD,IAAI;EAEF,IAAI,EAAC,MADe,GAAG,KAAK,MAAM,iBAAiB,EAAA,CACxC,OAAO,GAAG,OAAO;EAG5B,QAAO,MADe,GAAG,SAAS,MAAM,mBAAmB,MAAM,EAAA,CAClD,KAAK,CAAC,CAAC,SAAS;CACjC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,eAAmC;CAChD,MAAM,CAAC,SAAS,eAAe,MAAM,QAAQ,IAAI,CAC/C,kBAAkB,GAClB,iBAAiB,CACnB,CAAC;CAED,OAAO;EACL;EACA,SAAS,eAAe;EACxB,OAAO;GACL,SAAS,MAAM;GACf,mBAAmB,MAAM;EAC3B;EACA;CACF;AACF;AAEA,SAAS,oBAAoB,MAAuB;CAClD,QAAQ,KAAK;;WAEJ,KAAK,QAAQ;WACb,KAAK,QAAQ,KAAK,GAAG,KAAK,QAAQ,QAAQ,IAAI,KAAK,QAAQ,SAAS,GAAG,KAAK,QAAQ,KAAK;;;aAGvF,KAAK,MAAM,QAAQ;uBACT,KAAK,MAAM,kBAAkB;;gBAEpC,KAAK,cAAc,QAAQ,MAAM;AACjD;AAEA,SAAS,mBAAmB,MAAuB;CACjD,QAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAC3C;AAEA,eAAsB,SAAS,SAAyC;CACtE,MAAM,YAAY,MAAM,aAAa;CAErC,IAAI,QAAQ,MACV,mBAAmB,SAAS;MAE5B,oBAAoB,SAAS;AAEjC;AAEA,MAAa,QAAQ,cAAc;CACjC,MAAM;EACJ,MAAM;EACN,aAAa;CACf;CACA,MAAM,EACJ,MAAM;EACJ,MAAM;EACN,SAAS;EACT,aAAa;CACf,EACF;CACA,IAAI,EAAE,QAAQ;EACZ,OAAO,SAAS,EACd,MAAM,KAAK,KACb,CAAC;CACH;AACF,CAAC"}
1
+ {"version":3,"file":"debug-CA7pg8V9.js","names":[],"sources":["../src/debug.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { defineCommand } from \"citty\"\nimport consola from \"consola\"\nimport fs from \"node:fs/promises\"\nimport os from \"node:os\"\n\nimport { PATHS } from \"./lib/paths\"\n\ninterface DebugInfo {\n version: string\n runtime: {\n name: string\n version: string\n platform: string\n arch: string\n }\n paths: {\n APP_DIR: string\n GITHUB_TOKEN_PATH: string\n }\n tokenExists: boolean\n}\n\ninterface RunDebugOptions {\n json: boolean\n}\n\nasync function getPackageVersion(): Promise<string> {\n try {\n const packageJsonPath = new URL(\"../package.json\", import.meta.url).pathname\n // @ts-expect-error https://github.com/sindresorhus/eslint-plugin-unicorn/blob/v59.0.1/docs/rules/prefer-json-parse-buffer.md\n // JSON.parse() can actually parse buffers\n const packageJson = JSON.parse(await fs.readFile(packageJsonPath)) as {\n version: string\n }\n return packageJson.version\n } catch {\n return \"unknown\"\n }\n}\n\nfunction getRuntimeInfo() {\n const isBun = typeof Bun !== \"undefined\"\n\n return {\n name: isBun ? \"bun\" : \"node\",\n version: isBun ? Bun.version : process.version.slice(1),\n platform: os.platform(),\n arch: os.arch(),\n }\n}\n\nasync function checkTokenExists(): Promise<boolean> {\n try {\n const stats = await fs.stat(PATHS.GITHUB_TOKEN_PATH)\n if (!stats.isFile()) return false\n\n const content = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, \"utf8\")\n return content.trim().length > 0\n } catch {\n return false\n }\n}\n\nasync function getDebugInfo(): Promise<DebugInfo> {\n const [version, tokenExists] = await Promise.all([\n getPackageVersion(),\n checkTokenExists(),\n ])\n\n return {\n version,\n runtime: getRuntimeInfo(),\n paths: {\n APP_DIR: PATHS.APP_DIR,\n GITHUB_TOKEN_PATH: PATHS.GITHUB_TOKEN_PATH,\n },\n tokenExists,\n }\n}\n\nfunction printDebugInfoPlain(info: DebugInfo): void {\n consola.info(`github-router debug\n\nVersion: ${info.version}\nRuntime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})\n\nPaths:\n- APP_DIR: ${info.paths.APP_DIR}\n- GITHUB_TOKEN_PATH: ${info.paths.GITHUB_TOKEN_PATH}\n\nToken exists: ${info.tokenExists ? \"Yes\" : \"No\"}`)\n}\n\nfunction printDebugInfoJson(info: DebugInfo): void {\n console.log(JSON.stringify(info, null, 2))\n}\n\nexport async function runDebug(options: RunDebugOptions): Promise<void> {\n const debugInfo = await getDebugInfo()\n\n if (options.json) {\n printDebugInfoJson(debugInfo)\n } else {\n printDebugInfoPlain(debugInfo)\n }\n}\n\nexport const debug = defineCommand({\n meta: {\n name: \"debug\",\n description: \"Print debug information about the application\",\n },\n args: {\n json: {\n type: \"boolean\",\n default: false,\n description: \"Output debug information as JSON\",\n },\n },\n run({ args }) {\n return runDebug({\n json: args.json,\n })\n },\n})\n"],"mappings":";;;;;;AA4BA,eAAe,oBAAqC;CAClD,IAAI;EACF,MAAM,kBAAkB,IAAI,IAAI,mBAAmB,YAAY,GAAG,CAAC,CAAC;EAMpE,OAHoB,KAAK,MAAM,MAAM,GAAG,SAAS,eAAe,CAG/C,CAAC,CAAC;CACrB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,iBAAiB;CACxB,MAAM,QAAQ,OAAO,QAAQ;CAE7B,OAAO;EACL,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,IAAI,UAAU,QAAQ,QAAQ,MAAM,CAAC;EACtD,UAAU,GAAG,SAAS;EACtB,MAAM,GAAG,KAAK;CAChB;AACF;AAEA,eAAe,mBAAqC;CAClD,IAAI;EAEF,IAAI,EAAC,MADe,GAAG,KAAK,MAAM,iBAAiB,EAAA,CACxC,OAAO,GAAG,OAAO;EAG5B,QAAO,MADe,GAAG,SAAS,MAAM,mBAAmB,MAAM,EAAA,CAClD,KAAK,CAAC,CAAC,SAAS;CACjC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,eAAmC;CAChD,MAAM,CAAC,SAAS,eAAe,MAAM,QAAQ,IAAI,CAC/C,kBAAkB,GAClB,iBAAiB,CACnB,CAAC;CAED,OAAO;EACL;EACA,SAAS,eAAe;EACxB,OAAO;GACL,SAAS,MAAM;GACf,mBAAmB,MAAM;EAC3B;EACA;CACF;AACF;AAEA,SAAS,oBAAoB,MAAuB;CAClD,QAAQ,KAAK;;WAEJ,KAAK,QAAQ;WACb,KAAK,QAAQ,KAAK,GAAG,KAAK,QAAQ,QAAQ,IAAI,KAAK,QAAQ,SAAS,GAAG,KAAK,QAAQ,KAAK;;;aAGvF,KAAK,MAAM,QAAQ;uBACT,KAAK,MAAM,kBAAkB;;gBAEpC,KAAK,cAAc,QAAQ,MAAM;AACjD;AAEA,SAAS,mBAAmB,MAAuB;CACjD,QAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAC3C;AAEA,eAAsB,SAAS,SAAyC;CACtE,MAAM,YAAY,MAAM,aAAa;CAErC,IAAI,QAAQ,MACV,mBAAmB,SAAS;MAE5B,oBAAoB,SAAS;AAEjC;AAEA,MAAa,QAAQ,cAAc;CACjC,MAAM;EACJ,MAAM;EACN,aAAa;CACf;CACA,MAAM,EACJ,MAAM;EACJ,MAAM;EACN,SAAS;EACT,aAAa;CACf,EACF;CACA,IAAI,EAAE,QAAQ;EACZ,OAAO,SAAS,EACd,MAAM,KAAK,KACb,CAAC;CACH;AACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { C as runWorkerAgent } from "./peer-mcp-personas-CQ-0Piyr.js";
2
+ export { runWorkerAgent };
@@ -545,6 +545,6 @@ function killChildProcessTree(child, opts = { detachedGroup: false }) {
545
545
  }, graceMs).unref?.();
546
546
  }
547
547
  //#endregion
548
- export { resolveExecutable as a, runManagedExeCapture as c, parseIntEnv as i, spawnTaskkillBestEffort as l, killManagedTree as n, runCommandCapture as o, parseBoolEnv as r, runCommandVoid as s, killChildProcessTree as t };
548
+ export { parseIntEnv as a, runCommandVoid as c, parseBoolEnv as i, runManagedExeCapture as l, killChildProcessTree as n, resolveExecutable as o, killManagedTree as r, runCommandCapture as s, buildExecInvocation as t, spawnTaskkillBestEffort as u };
549
549
 
550
- //# sourceMappingURL=exec-y8C_MU8A.js.map
550
+ //# sourceMappingURL=exec-COUT4QsS.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"exec-y8C_MU8A.js","names":[],"sources":["../src/lib/exec.ts"],"sourcesContent":["/**\n * Windows-safe, injection-safe command execution helpers.\n *\n * Why this module exists: on Windows, npm-installed CLIs (`claude`,\n * `npm`, `codex`) are `.cmd`/`.bat` shims. Node's `execFile`/`spawn`\n * with `shell:false` cannot launch them (CreateProcess only resolves\n * `.exe`), so callers must go through `cmd.exe` (`shell:true`). That in\n * turn opens two hazards this module closes:\n *\n * 1. **Metacharacter injection** (`& | < > ^ ( ) ! %`). A naive\n * \"quote only tokens with spaces\" scheme lets `pkg@latest&calc`\n * run `calc` as a second command. `buildExecInvocation` applies\n * real cmd.exe quoting (argv-quote + caret-escape) and fails\n * closed on `%` (which cannot be reliably escaped on the cmd\n * command line).\n *\n * 2. **CWD shadowing.** `cmd.exe` resolves a bare `npm` from the\n * current directory before PATH, so an untrusted repo can plant\n * `npm.cmd`. `resolveExecutable` resolves to an absolute path\n * against PATH only (honoring PATHEXT), never the cwd, so callers\n * spawn the real binary by absolute path.\n *\n * All runners are best-effort and timeout-bounded; callers wrap in\n * try/catch and never let an update/probe failure block launch.\n */\n\nimport { spawn, spawnSync, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\n/**\n * Parse a boolean-ish env value. Returns `undefined` when unset or\n * unrecognized so callers can apply their own default. Accepts\n * `1|true|yes|on` (true) and `0|false|no|off|<empty>` (false),\n * case-insensitive. The single shared parser for all new `GH_ROUTER_*`\n * flags so on/off semantics don't drift per call site.\n */\nexport function parseBoolEnv(value: string | undefined): boolean | undefined {\n if (value === undefined) return undefined\n const v = value.trim().toLowerCase()\n if (v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\") return true\n if (v === \"0\" || v === \"false\" || v === \"no\" || v === \"off\" || v === \"\") {\n return false\n }\n return undefined\n}\n\n/**\n * Parse a positive-integer env value. Returns `undefined` when unset, empty, or\n * not a clean positive integer, so callers apply their own default.\n *\n * The single shared parser for numeric `GH_ROUTER_*` knobs, and it deliberately\n * uses `Number()` rather than `parseInt()`. `parseInt` stops at the first\n * character it cannot consume, so it silently truncates instead of rejecting:\n * `\"3e5\"` becomes 3, `\"300_000\"` becomes 300, `\"60000ms\"` becomes 60000. For a\n * timeout or a concurrency cap that is the worst possible failure — a user who\n * writes `GH_ROUTER_STOP_GATE_TIMEOUT_MS=3e5` intending five minutes gets three\n * MILLISECONDS, and the resulting instant failure looks like a product bug\n * rather than a typo. Falling back to the documented default is strictly safer\n * than honouring a number the user never meant.\n *\n * Found by a cross-lab reviewer during a capability audit, at three shipped call\n * sites, while three separate local helpers in this repo already parsed the same\n * shape correctly and one of them documented this exact hazard. That is the\n * argument for one shared parser rather than a fourth copy.\n */\nexport function parseIntEnv(value: string | undefined): number | undefined {\n if (value === undefined) return undefined\n const raw = value.trim()\n if (raw === \"\") return undefined\n const n = Number(raw)\n if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) return undefined\n return n\n}\n\n/** Read the PATH value from an env object, case-insensitively. */\nfunction pathValueOf(env: NodeJS.ProcessEnv): string {\n for (const key of Object.keys(env)) {\n if (key.toLowerCase() === \"path\") return env[key] ?? \"\"\n }\n return \"\"\n}\n\nexport interface ResolveExecutableOpts {\n env?: NodeJS.ProcessEnv\n platform?: NodeJS.Platform\n /** Directory to treat as \"current dir\" and exclude from resolution. */\n cwd?: string\n}\n\n/**\n * Resolve an executable name to an absolute path against PATH, honoring\n * `PATHEXT` on Windows and **excluding the current working directory**.\n *\n * Returns `null` when unresolved — callers treat that as \"tool absent\"\n * and skip (best-effort). Spawning the returned absolute path means\n * `cmd.exe`'s implicit cwd-first lookup never applies, closing the\n * planted-`npm.cmd` vector.\n */\nexport function resolveExecutable(\n name: string,\n opts: ResolveExecutableOpts = {},\n): string | null {\n const platform = opts.platform ?? process.platform\n const env = opts.env ?? process.env\n // Defensive: some test harnesses mock `node:process` without `cwd`.\n // Absent a usable cwd we simply skip the cwd-exclusion guard (the\n // returned absolute path already bypasses cmd.exe's cwd-first lookup).\n const cwdRaw =\n opts.cwd ?? (typeof process.cwd === \"function\" ? process.cwd() : undefined)\n const resolvedCwd = cwdRaw ? path.resolve(cwdRaw) : null\n\n const dirs = pathValueOf(env)\n .split(path.delimiter)\n // Drop empty entries and a literal \".\" — both denote the cwd on\n // Windows, which we explicitly refuse to resolve against.\n .filter((d) => d.length > 0 && d !== \".\")\n\n const isWin = platform === \"win32\"\n\n // POSIX: an explicit path component → resolve directly.\n if (!isWin && name.includes(\"/\")) {\n return existsSync(name) ? path.resolve(name) : null\n }\n // Windows: an explicit path component (with a separator) → direct.\n if (isWin && (name.includes(\"\\\\\") || name.includes(\"/\"))) {\n return existsSync(name) ? path.resolve(name) : null\n }\n\n const exts =\n isWin && path.extname(name) === \"\"\n ? (env.PATHEXT ?? \".COM;.EXE;.BAT;.CMD\")\n .split(\";\")\n .map((e) => e.trim())\n .filter(Boolean)\n : [\"\"]\n\n for (const dir of dirs) {\n // Belt-and-suspenders: never resolve against the cwd even if it is\n // listed explicitly in PATH.\n if (resolvedCwd && path.resolve(dir) === resolvedCwd) continue\n for (const ext of exts) {\n const candidate = path.join(dir, name + ext)\n if (existsSync(candidate)) return candidate\n }\n }\n return null\n}\n\n/**\n * Quote one argument for a `cmd.exe /c \"<line>\"` command line so the\n * target program receives it verbatim and no `cmd.exe` metacharacter\n * retains shell meaning.\n *\n * Two phases (the canonical Windows approach — Colascione / Rust std):\n * 1. **argv quoting** so `CommandLineToArgvW` in the target parses\n * the token as one argument (double-quote, backslash-escape).\n * 2. **caret-escaping** every `cmd.exe` metacharacter — including the\n * quotes from phase 1 — so `cmd.exe` is never in quote-mode, strips\n * the carets, and hands the argv-quoted string to the program.\n *\n * `%` is special: it cannot be reliably escaped on the `cmd.exe`\n * *command line* (caret does not stop `%VAR%` expansion there). Rather\n * than mis-escape, we **throw** — our callers never pass `%`, so this\n * fails closed on the one unescapable injection vector.\n */\nexport function quoteWinArg(arg: string): string {\n if (arg.includes(\"%\")) {\n throw new Error(\n \"buildExecInvocation: argument contains '%', which cannot be safely \" +\n \"escaped on the Windows command line; refusing to build the command.\",\n )\n }\n\n // Phase 1 — argv quoting.\n let quoted: string\n if (arg.length > 0 && !/[ \\t\\n\\v\"&|<>()^!]/.test(arg)) {\n quoted = arg\n } else {\n let s = '\"'\n let backslashes = 0\n for (const ch of arg) {\n if (ch === \"\\\\\") {\n backslashes++\n } else if (ch === '\"') {\n s += \"\\\\\".repeat(backslashes * 2 + 1) + '\"'\n backslashes = 0\n } else {\n s += \"\\\\\".repeat(backslashes) + ch\n backslashes = 0\n }\n }\n s += \"\\\\\".repeat(backslashes * 2) + '\"'\n quoted = s\n }\n\n // Phase 2 — caret-escape all cmd.exe metacharacters (and the carets\n // themselves). cmd strips these; the program sees the phase-1 string.\n return quoted.replace(/[()!^\"<>&|]/g, \"^$&\")\n}\n\nexport interface ExecInvocation {\n command: string\n args: string[]\n shell: boolean\n}\n\n/**\n * Build the platform-correct `spawn` invocation for a command given as\n * an argv array. Pure / unit-testable (no spawn).\n *\n * - win32 → a single caret/argv-quoted command string + `shell:true`\n * + empty args array (the empty array avoids the DEP0190 warning\n * that fires when args and `shell:true` are combined). `cmd[0]`\n * should already be an absolute path from `resolveExecutable`.\n * - posix → `(cmd[0], cmd.slice(1))` with `shell:false` — no shell,\n * no injection surface.\n */\nexport function buildExecInvocation(\n cmd: string[],\n platform: NodeJS.Platform = process.platform,\n): ExecInvocation {\n if (cmd.length === 0) throw new Error(\"buildExecInvocation: empty command\")\n if (platform === \"win32\") {\n return { command: cmd.map(quoteWinArg).join(\" \"), args: [], shell: true }\n }\n return { command: cmd[0], args: cmd.slice(1), shell: false }\n}\n\nexport interface RunOpts {\n cwd?: string\n /** Hard timeout in ms; the process tree is killed on expiry. */\n timeoutMs?: number\n /** Extra env to merge over the parent env for the child. */\n env?: NodeJS.ProcessEnv\n /**\n * Byte ceiling on captured stdout. Defaults to {@link DEFAULT_CAPTURE_CAP}.\n *\n * On overflow we STOP BUFFERING but deliberately do NOT kill the child, and\n * the result is reported as a normal completion with `truncated: true`. That\n * asymmetry against `runManagedExeCapture` (which tree-kills on overflow) is\n * load-bearing: callers here read the exit CODE as a verdict —\n * `~/lib/orchestration/live-exec` maps `code ?? 1` onto a FAILED GATE — so\n * killing the child would turn \"this command printed a lot\" into \"typecheck\n * failed\". Truncation is a capture-side limit, never a command outcome.\n *\n * The data handler keeps firing and discarding after the cap, so the pipe\n * drains and the child never blocks writing into a full buffer.\n */\n maxStdoutBytes?: number\n}\n\n/**\n * Default stdout capture ceiling.\n *\n * A `timeoutMs` is NOT a memory bound: `git diff HEAD` on a repo with a large\n * generated file happily emits hundreds of MB inside a 5s window, and every\n * byte was being concatenated onto one JS string. 16 MiB matches the worker\n * layer's `GH_ROUTER_WORKER_MAX_TOOL_BYTES` so the two capture paths agree on\n * what \"too much output to be useful\" means.\n */\nexport const DEFAULT_CAPTURE_CAP = 16 * 1024 * 1024\n\n/** Same ceiling shape for stderr, which only ever feeds diagnostics. */\nconst CAPTURE_STDERR_CAP = 64 * 1024\n\nexport interface RunResult {\n stdout: string\n stderr: string\n /** Exit code; `null` when killed by signal/timeout. */\n code: number | null\n timedOut: boolean\n /**\n * True when stdout exceeded the cap and was cut short. The command itself\n * still ran to completion — check this, never `code`, to detect truncation.\n */\n truncated: boolean\n /**\n * True when stderr exceeded its 64 KiB ceiling and was cut short.\n *\n * Reported separately from `truncated` because it means something different\n * to a caller: stdout truncation loses DATA, stderr truncation loses a\n * DIAGNOSTIC. A failing compiler often puts the root error at the tail of a\n * long stderr, so a caller that surfaces stderr to a human needs to know it\n * is looking at a prefix rather than the whole story.\n */\n stderrTruncated: boolean\n}\n\nfunction runInternal(\n cmd: string[],\n stdoutMode: \"pipe\" | \"inherit\" | \"ignore\",\n opts: RunOpts,\n): Promise<RunResult> {\n const { command, args, shell } = buildExecInvocation(cmd)\n return new Promise<RunResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, args, {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell,\n windowsHide: true,\n stdio: [\n \"ignore\",\n stdoutMode,\n stdoutMode === \"inherit\" ? \"inherit\" : \"pipe\",\n ],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n // Buffer BYTES and decode once at the end (or incrementally through a\n // streaming decoder). The previous `stdout += chunk.toString(\"utf8\")` per\n // chunk corrupted any multi-byte character that straddled a chunk\n // boundary — a 3-byte `→` split 1/2 decoded as two replacement chars, so\n // captured diffs and command output silently mangled non-ASCII.\n const stdoutDecoder = new TextDecoder()\n const stderrDecoder = new TextDecoder()\n let stdout = \"\"\n let stderr = \"\"\n let stdoutBytes = 0\n let stderrBytes = 0\n let truncated = false\n let stderrTruncated = false\n let timedOut = false\n let settled = false\n const maxStdoutBytes = opts.maxStdoutBytes ?? DEFAULT_CAPTURE_CAP\n\n const timer = opts.timeoutMs\n ? setTimeout(() => {\n timedOut = true\n killTree(child.pid)\n }, opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n // Past the cap: keep draining the pipe, stop growing the string. The\n // child is intentionally left alive (see `maxStdoutBytes` docs).\n if (truncated) return\n const room = maxStdoutBytes - stdoutBytes\n if (c.length > room) {\n // Append only the slice that fits, then stop. `stream: true` lets the\n // decoder hold a partial code point back rather than emit U+FFFD, so a\n // cap landing mid-character truncates cleanly instead of corrupting.\n if (room > 0) {\n stdout += stdoutDecoder.decode(c.subarray(0, room), { stream: true })\n stdoutBytes += room\n }\n truncated = true\n return\n }\n stdout += stdoutDecoder.decode(c, { stream: true })\n stdoutBytes += c.length\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n // Already full: this chunk is dropped entirely, which is truncation just\n // as much as a partial slice is. Flagging only the slicing chunk missed\n // the common case where the cap lands on a chunk boundary and every\n // subsequent chunk returns here.\n if (stderrBytes >= CAPTURE_STDERR_CAP) {\n stderrTruncated = true\n return\n }\n const room = CAPTURE_STDERR_CAP - stderrBytes\n const slice = c.length > room ? c.subarray(0, room) : c\n if (slice.length < c.length) stderrTruncated = true\n stderr += stderrDecoder.decode(slice, { stream: true })\n stderrBytes += slice.length\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n // Flush the decoders' held-back bytes.\n //\n // NOT flushed on the truncated path: the cap deliberately cuts mid-byte-\n // sequence, so the decoder is holding a partial code point we chose to\n // drop. Flushing it would emit U+FFFD — turning a clean truncation into\n // apparent corruption, which is the exact failure this fix removes.\n // Un-truncated output has no such intent, so a trailing partial there IS\n // malformed input and should surface as U+FFFD rather than vanish.\n if (!truncated) stdout += stdoutDecoder.decode()\n if (!stderrTruncated) stderr += stderrDecoder.decode()\n resolve({ stdout, stderr, code, timedOut, truncated, stderrTruncated })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/**\n * Spawn `taskkill` best-effort, without any chance of taking the proxy down.\n *\n * Two things a bare `try { spawn(\"taskkill\", ...) } catch {}` gets wrong, both\n * of which this helper is the single place that fixes:\n *\n * 1. **The catch cannot catch.** `spawn` reports ENOENT / EPERM / EACCES by\n * emitting `'error'` ASYNCHRONOUSLY, so the surrounding `try` (which only\n * guards the synchronous throw) never sees it. An EventEmitter `'error'`\n * with no listener THROWS — which lands in the `uncaughtException` handler\n * in `~/main` and calls `process.exit(1)`. A best-effort child kill on a\n * timeout path could therefore kill the whole proxy. The listener must be\n * attached synchronously at the spawn site, which is why this is a helper\n * and not a convention.\n * 2. **The bare name is hijackable.** `CreateProcess`'s search order can\n * include the cwd, so `\"taskkill\"` may resolve to a planted binary;\n * `taskkillExe()` pins `System32\\taskkill.exe`.\n */\nexport function spawnTaskkillBestEffort(pid: number): boolean {\n try {\n const child = spawn(taskkillExe(), [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n })\n // Load-bearing: see (1) above. Without this an async spawn failure is an\n // uncaught exception, not a no-op. It is NOT a silent swallow, either —\n // the fallback below is what keeps a caller from hanging.\n //\n // (No logging here on purpose: this module has no logger import, and\n // taking one would pull a dependency into the lowest-level exec helper\n // that every other module builds on. The fallback is the observable\n // behavior that matters.)\n child.on(\"error\", () => {\n fallbackKill(pid)\n })\n return true\n } catch {\n // Synchronous spawn failure (bad argv shape).\n fallbackKill(pid)\n return false\n }\n}\n\n/**\n * Last-resort single-process kill when `taskkill` itself cannot run.\n *\n * Not a tree kill — grandchildren survive — but it is the difference between\n * \"the timed-out child dies and its `close` event settles the caller\" and \"the\n * caller waits forever\". `runInternal` resolves ONLY from `close`, so a timeout\n * whose kill never lands leaves that promise pending for the life of the\n * process, which is a worse failure than the crash `spawnTaskkillBestEffort`\n * was written to prevent. Best-effort by construction: ESRCH means the child\n * was already gone, which is the success case.\n */\nfunction fallbackKill(pid: number): void {\n try {\n process.kill(pid)\n } catch {\n // Already exited, or we lack permission — nothing further to try.\n }\n}\n\n/** Kill a process tree best-effort (taskkill /T on Windows). */\nfunction killTree(pid: number | undefined): void {\n if (!pid) return\n try {\n if (process.platform === \"win32\") {\n spawnTaskkillBestEffort(pid)\n } else {\n process.kill(pid, \"SIGTERM\")\n }\n } catch {\n // already gone\n }\n}\n\n/** Run a command and capture stdout/stderr. Rejects on spawn error. */\nexport function runCommandCapture(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command discarding output (still captures stderr for errors). */\nexport function runCommandVoid(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command with the child's stdout/stderr inherited to the user. */\nexport function runCommandInherit(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"inherit\", opts)\n}\n\nexport interface ManagedExeOpts extends RunOpts {\n /**\n * Hard cap on captured stdout bytes. On overflow the child is\n * tree-killed and the result carries `stdoutTruncated: true`. Defends\n * against a full-`CodeUnit` colgrep `--json` payload (source + 5\n * analysis layers per hit) bloating memory.\n */\n maxStdoutBytes?: number\n /**\n * When true, exceeding `maxStdoutBytes` does NOT tree-kill the child — it\n * sets `stdoutTruncated` and stops BUFFERING further stdout while still\n * draining the pipe (no backpressure). Use for a child whose kill is unsafe\n * (e.g. colgrep, which writes a non-atomic index — a byte-cap kill during\n * its result output could interrupt a write). The child runs to completion\n * (bounded by `timeoutMs` / `inactivityTimeoutMs`). Default false (kill).\n */\n truncateInsteadOfKill?: boolean\n /**\n * Called synchronously with the spawned child right after spawn\n * succeeds, BEFORE any output arrives. The colbert lifecycle ledger\n * uses this to register the child so a session-exit sweep can\n * tree-kill an orphan. Never throws into the runner.\n */\n onSpawn?: (child: ReturnType<typeof spawn>) => void\n /**\n * Inactivity (stall) watchdog. When set, a timer is armed for\n * `inactivityTimeoutMs` and RESET on every stdout/stderr data chunk. On\n * expiry (no output for the window) it consults `onInactivityCheck`: a\n * `true` return means \"still making progress via an out-of-band signal\"\n * (e.g. the colgrep index dir is still growing on disk even though the\n * process is silent on a non-TTY pipe) and the watchdog re-arms; a\n * `false`/absent return tree-kills the child and sets `stalled: true`.\n * This is the progress-based \"stuck\" detector that lets a long-but-\n * progressing build run to completion while still killing a hung one —\n * independent of the coarse total `timeoutMs` backstop.\n */\n inactivityTimeoutMs?: number\n /** External progress probe consulted when the inactivity timer fires.\n * Return `true` to re-arm (still progressing), `false`/throw to kill.\n * MUST be cheap + synchronous (called on a timer, not awaited). */\n onInactivityCheck?: () => boolean\n}\n\nexport interface ManagedExeResult extends RunResult {\n /** True iff stdout was truncated at `maxStdoutBytes` (child was killed). */\n stdoutTruncated: boolean\n /** True iff the inactivity watchdog killed the child (no progress). */\n stalled: boolean\n}\n\n/**\n * Run a **native executable** (a real `.exe`/Mach-O/ELF, NOT a `.cmd`\n * shim) capturing stdout/stderr, with `shell:false` on EVERY platform.\n *\n * Why a separate runner from `runCommandCapture`: that path routes\n * through `buildExecInvocation`, which on Windows builds a\n * `cmd.exe`-quoted command string and **throws on `%`** (`quoteWinArg`).\n * A workspace path can legally contain `%` (and `&`, `(`, `)`, `!`, …),\n * so the managed colgrep binary — which IS a native `.exe`, not a shim —\n * must bypass cmd.exe entirely. `spawn(absExe, args, {shell:false})`\n * resolves the `.exe` via CreateProcess directly: no cmd.exe, no\n * metacharacter hazard, no `%` refusal. POSIX was already `shell:false`.\n * This is what makes \"ANY absolute workspace\" hold on Windows.\n *\n * Lifecycle:\n * - `timeoutMs` → tree-kill on expiry (`taskkill /T /F` on Windows,\n * POSIX process-group `kill(-pgid)` so colgrep's rayon worker\n * children die too). `timedOut: true` in the result.\n * - `maxStdoutBytes` → tree-kill + `stdoutTruncated: true` once the\n * captured stdout exceeds the cap.\n * - `onSpawn(child)` → register the child with the caller's ledger.\n *\n * `command` MUST be an absolute path to the executable (the caller\n * resolves it; we never search PATH here — there is nothing to inject).\n */\nexport function runManagedExeCapture(\n command: string,\n args: ReadonlyArray<string>,\n opts: ManagedExeOpts = {},\n): Promise<ManagedExeResult> {\n const isWin = process.platform === \"win32\"\n return new Promise<ManagedExeResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, [...args], {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell: false,\n windowsHide: true,\n // POSIX: new process group so we can kill the whole tree\n // (colgrep + rayon workers) with kill(-pgid). Windows uses\n // taskkill /T instead; `detached` there has no group semantics.\n detached: !isWin,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n try {\n opts.onSpawn?.(child)\n } catch {\n // ledger registration must never break the spawn\n }\n\n const chunks: Array<Buffer> = []\n let stdoutBytes = 0\n const stderrChunks: Array<Buffer> = []\n let stderrBytes = 0\n const STDERR_CAP = 64 * 1024\n let timedOut = false\n let stdoutTruncated = false\n let stalled = false\n let settled = false\n\n // Exactly one terminator wins: the first to fire records its reason and\n // tree-kills; later ones no-op. Keeps timedOut / stalled / stdoutTruncated\n // mutually exclusive and avoids a double tree-kill.\n let terminated = false\n const terminate = (reason: \"timeout\" | \"stall\" | \"truncate\"): void => {\n if (terminated) return\n terminated = true\n if (reason === \"timeout\") timedOut = true\n else if (reason === \"stall\") stalled = true\n else stdoutTruncated = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n killManagedTree(child, isWin)\n }\n\n const timer = opts.timeoutMs\n ? setTimeout(() => terminate(\"timeout\"), opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n // Inactivity (stall) watchdog. Re-arms itself: when the window elapses\n // with no output, consult the external progress probe — re-arm if it\n // says \"still progressing\" (e.g. the index dir grew), else kill + mark\n // stalled. Reset on every data chunk (a chatty process never stalls).\n let inactivityTimer: ReturnType<typeof setTimeout> | undefined\n const armInactivity = (): void => {\n if (opts.inactivityTimeoutMs === undefined || settled || terminated) return\n inactivityTimer = setTimeout(() => {\n if (settled) return\n // No probe → pure output-inactivity kill. A probe that THROWS is\n // inconclusive → don't kill (re-arm); the absolute timeoutMs backstop\n // still catches a genuinely wedged process.\n let progressing = false\n if (opts.onInactivityCheck) {\n try {\n progressing = opts.onInactivityCheck() === true\n } catch {\n progressing = true\n }\n }\n if (progressing) {\n armInactivity()\n return\n }\n terminate(\"stall\")\n }, opts.inactivityTimeoutMs)\n inactivityTimer?.unref?.()\n }\n const resetInactivity = (): void => {\n if (inactivityTimer) clearTimeout(inactivityTimer)\n armInactivity()\n }\n armInactivity()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n if (stdoutTruncated) return\n stdoutBytes += c.length\n if (\n opts.maxStdoutBytes !== undefined &&\n stdoutBytes > opts.maxStdoutBytes\n ) {\n stdoutTruncated = true\n // Default: tree-kill on overflow. Opt-in: keep the child alive and\n // just stop buffering (the data handler keeps firing + discarding via\n // the `if (stdoutTruncated) return` guard above, so the pipe drains\n // and the child never blocks on a full buffer).\n if (!opts.truncateInsteadOfKill) terminate(\"truncate\")\n return\n }\n chunks.push(c)\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n // Hard byte cap on stderr — append only the slice that fits so a\n // single huge chunk can't overshoot. Never logged raw (it can\n // embed source code from colgrep).\n if (stderrBytes >= STDERR_CAP) return\n const remaining = STDERR_CAP - stderrBytes\n const slice = c.length > remaining ? c.subarray(0, remaining) : c\n stderrChunks.push(slice)\n stderrBytes += slice.length\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n resolve({\n stdout: Buffer.concat(chunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n code,\n timedOut,\n stdoutTruncated,\n // `RunResult.truncated` is the shared, runner-agnostic signal. Kept in\n // lockstep with `stdoutTruncated` (this runner's older, more specific\n // name, which callers already branch on) so a caller holding either\n // shape reads the same fact.\n truncated: stdoutTruncated,\n // This runner slices stderr to fit its own 64 KiB cap without tracking\n // whether it did, so report the honest thing rather than guess.\n stderrTruncated: stderrBytes >= STDERR_CAP,\n stalled,\n })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/**\n * Tree-kill a managed child. Windows: `taskkill /T /F /PID` (whole\n * tree). POSIX: kill the process GROUP (`-pgid`) so colgrep's rayon\n * worker children die with the parent.\n *\n * `runManagedExeCapture` always spawns POSIX children `detached:true`\n * (their own process group), so the group kill is the correct and\n * sufficient primitive. We deliberately do NOT fall back to a positive-\n * pid `process.kill(pid)` when the group kill fails: by the time a kill\n * fires (timeout / byte-cap race), the child may have already exited and\n * its PID been recycled by an unrelated process — a positive-pid kill\n * would then target the wrong process. `ESRCH` (group already gone) is\n * the success case for our purposes; any other error is swallowed.\n */\nexport function killManagedTree(\n child: ReturnType<typeof spawn>,\n isWin: boolean = process.platform === \"win32\",\n): void {\n const pid = child.pid\n if (!pid) return\n try {\n if (isWin) {\n spawnTaskkillBestEffort(pid)\n } else {\n // Negative pid → the process group (we spawned detached). No\n // positive-pid fallback (PID-reuse hazard — see doc comment).\n process.kill(-pid, \"SIGKILL\")\n }\n } catch {\n // ESRCH (already gone) / EPERM — best-effort, nothing more to do.\n }\n}\n\n/**\n * Absolute path to Windows `taskkill.exe`. Bare `\"taskkill\"` would let\n * `CreateProcess`'s search order (which can include the cwd) run a planted\n * `taskkill.exe`; pinning the System32 path closes that hijack.\n */\nfunction taskkillExe(): string {\n const root =\n process.env.SystemRoot || process.env.windir || \"C:\\\\Windows\"\n return path.join(root, \"System32\", \"taskkill.exe\")\n}\n\n/**\n * Tree-kill a child process on a SHUTDOWN/exit path, deterministically.\n *\n * Distinct from `killManagedTree` in two ways that matter when the proxy\n * is about to call `process.exit`:\n *\n * 1. **Windows uses `spawnSync`, not async `spawn`.** An async taskkill\n * may never issue before `process.exit` tears the event loop down;\n * the synchronous form guarantees the kill command has run. It is\n * time-bounded so a wedged taskkill can't hang shutdown.\n * 2. **POSIX honors `detachedGroup`.** The launched CLI is spawned\n * `detached:true` (its own process group) so `kill(-pid)` reaps the\n * whole group; other callers (e.g. ripgrep) are NOT detached, so\n * `child.kill()` on the positive pid is correct. A negative-pid kill\n * on a non-detached child would throw or hit the wrong target.\n *\n * Both branches escalate SIGTERM → SIGKILL after `graceMs` so a CLI that\n * traps SIGTERM still dies. The escalation timer is `.unref()`'d so it\n * never holds the event loop open. Errors are swallowed (already-gone /\n * EBUSY / ESRCH are the success case for our purposes).\n */\nexport function killChildProcessTree(\n child: ChildProcess,\n opts: { detachedGroup: boolean; graceMs?: number } = { detachedGroup: false },\n): void {\n const pid = child.pid\n if (!pid) return\n const graceMs = opts.graceMs ?? 500\n\n if (process.platform === \"win32\") {\n // /T = whole tree, /F = force. taskkill walks the live parent-PID\n // chain, so it reaps grandchildren regardless of the cmd.exe-vs-direct\n // spawn shape. Sync (completes before an imminent process.exit) and\n // time-bounded (a wedged taskkill must not hang shutdown).\n try {\n spawnSync(taskkillExe(), [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n timeout: 5000,\n })\n } catch {\n // Already gone / EBUSY race — best-effort.\n }\n return\n }\n\n // POSIX: signal the group (detached) or the lone child (non-detached).\n const target = opts.detachedGroup ? -pid : pid\n try {\n process.kill(target, \"SIGTERM\")\n } catch {\n // ESRCH — already gone.\n }\n const t = setTimeout(() => {\n // For a lone (non-detached) child, skip escalation once it has exited\n // — its positive PID could have been recycled. For a process GROUP the\n // leader's exit does NOT mean the group is empty (a SIGTERM-ignoring\n // grandchild may survive), and the group id is only reused once the\n // group is fully gone, so always attempt the group SIGKILL (ESRCH when\n // already empty is the success case).\n if (\n !opts.detachedGroup &&\n (child.exitCode !== null || child.signalCode !== null)\n ) {\n return\n }\n try {\n process.kill(target, \"SIGKILL\")\n } catch {\n // Already dead.\n }\n }, graceMs)\n t.unref?.()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"exec-COUT4QsS.js","names":[],"sources":["../src/lib/exec.ts"],"sourcesContent":["/**\n * Windows-safe, injection-safe command execution helpers.\n *\n * Why this module exists: on Windows, npm-installed CLIs (`claude`,\n * `npm`, `codex`) are `.cmd`/`.bat` shims. Node's `execFile`/`spawn`\n * with `shell:false` cannot launch them (CreateProcess only resolves\n * `.exe`), so callers must go through `cmd.exe` (`shell:true`). That in\n * turn opens two hazards this module closes:\n *\n * 1. **Metacharacter injection** (`& | < > ^ ( ) ! %`). A naive\n * \"quote only tokens with spaces\" scheme lets `pkg@latest&calc`\n * run `calc` as a second command. `buildExecInvocation` applies\n * real cmd.exe quoting (argv-quote + caret-escape) and fails\n * closed on `%` (which cannot be reliably escaped on the cmd\n * command line).\n *\n * 2. **CWD shadowing.** `cmd.exe` resolves a bare `npm` from the\n * current directory before PATH, so an untrusted repo can plant\n * `npm.cmd`. `resolveExecutable` resolves to an absolute path\n * against PATH only (honoring PATHEXT), never the cwd, so callers\n * spawn the real binary by absolute path.\n *\n * All runners are best-effort and timeout-bounded; callers wrap in\n * try/catch and never let an update/probe failure block launch.\n */\n\nimport { spawn, spawnSync, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\n/**\n * Parse a boolean-ish env value. Returns `undefined` when unset or\n * unrecognized so callers can apply their own default. Accepts\n * `1|true|yes|on` (true) and `0|false|no|off|<empty>` (false),\n * case-insensitive. The single shared parser for all new `GH_ROUTER_*`\n * flags so on/off semantics don't drift per call site.\n */\nexport function parseBoolEnv(value: string | undefined): boolean | undefined {\n if (value === undefined) return undefined\n const v = value.trim().toLowerCase()\n if (v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\") return true\n if (v === \"0\" || v === \"false\" || v === \"no\" || v === \"off\" || v === \"\") {\n return false\n }\n return undefined\n}\n\n/**\n * Parse a positive-integer env value. Returns `undefined` when unset, empty, or\n * not a clean positive integer, so callers apply their own default.\n *\n * The single shared parser for numeric `GH_ROUTER_*` knobs, and it deliberately\n * uses `Number()` rather than `parseInt()`. `parseInt` stops at the first\n * character it cannot consume, so it silently truncates instead of rejecting:\n * `\"3e5\"` becomes 3, `\"300_000\"` becomes 300, `\"60000ms\"` becomes 60000. For a\n * timeout or a concurrency cap that is the worst possible failure — a user who\n * writes `GH_ROUTER_STOP_GATE_TIMEOUT_MS=3e5` intending five minutes gets three\n * MILLISECONDS, and the resulting instant failure looks like a product bug\n * rather than a typo. Falling back to the documented default is strictly safer\n * than honouring a number the user never meant.\n *\n * Found by a cross-lab reviewer during a capability audit, at three shipped call\n * sites, while three separate local helpers in this repo already parsed the same\n * shape correctly and one of them documented this exact hazard. That is the\n * argument for one shared parser rather than a fourth copy.\n */\nexport function parseIntEnv(value: string | undefined): number | undefined {\n if (value === undefined) return undefined\n const raw = value.trim()\n if (raw === \"\") return undefined\n const n = Number(raw)\n if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) return undefined\n return n\n}\n\n/** Read the PATH value from an env object, case-insensitively. */\nfunction pathValueOf(env: NodeJS.ProcessEnv): string {\n for (const key of Object.keys(env)) {\n if (key.toLowerCase() === \"path\") return env[key] ?? \"\"\n }\n return \"\"\n}\n\nexport interface ResolveExecutableOpts {\n env?: NodeJS.ProcessEnv\n platform?: NodeJS.Platform\n /** Directory to treat as \"current dir\" and exclude from resolution. */\n cwd?: string\n}\n\n/**\n * Resolve an executable name to an absolute path against PATH, honoring\n * `PATHEXT` on Windows and **excluding the current working directory**.\n *\n * Returns `null` when unresolved — callers treat that as \"tool absent\"\n * and skip (best-effort). Spawning the returned absolute path means\n * `cmd.exe`'s implicit cwd-first lookup never applies, closing the\n * planted-`npm.cmd` vector.\n */\nexport function resolveExecutable(\n name: string,\n opts: ResolveExecutableOpts = {},\n): string | null {\n const platform = opts.platform ?? process.platform\n const env = opts.env ?? process.env\n // Defensive: some test harnesses mock `node:process` without `cwd`.\n // Absent a usable cwd we simply skip the cwd-exclusion guard (the\n // returned absolute path already bypasses cmd.exe's cwd-first lookup).\n const cwdRaw =\n opts.cwd ?? (typeof process.cwd === \"function\" ? process.cwd() : undefined)\n const resolvedCwd = cwdRaw ? path.resolve(cwdRaw) : null\n\n const dirs = pathValueOf(env)\n .split(path.delimiter)\n // Drop empty entries and a literal \".\" — both denote the cwd on\n // Windows, which we explicitly refuse to resolve against.\n .filter((d) => d.length > 0 && d !== \".\")\n\n const isWin = platform === \"win32\"\n\n // POSIX: an explicit path component → resolve directly.\n if (!isWin && name.includes(\"/\")) {\n return existsSync(name) ? path.resolve(name) : null\n }\n // Windows: an explicit path component (with a separator) → direct.\n if (isWin && (name.includes(\"\\\\\") || name.includes(\"/\"))) {\n return existsSync(name) ? path.resolve(name) : null\n }\n\n const exts =\n isWin && path.extname(name) === \"\"\n ? (env.PATHEXT ?? \".COM;.EXE;.BAT;.CMD\")\n .split(\";\")\n .map((e) => e.trim())\n .filter(Boolean)\n : [\"\"]\n\n for (const dir of dirs) {\n // Belt-and-suspenders: never resolve against the cwd even if it is\n // listed explicitly in PATH.\n if (resolvedCwd && path.resolve(dir) === resolvedCwd) continue\n for (const ext of exts) {\n const candidate = path.join(dir, name + ext)\n if (existsSync(candidate)) return candidate\n }\n }\n return null\n}\n\n/**\n * Quote one argument for a `cmd.exe /c \"<line>\"` command line so the\n * target program receives it verbatim and no `cmd.exe` metacharacter\n * retains shell meaning.\n *\n * Two phases (the canonical Windows approach — Colascione / Rust std):\n * 1. **argv quoting** so `CommandLineToArgvW` in the target parses\n * the token as one argument (double-quote, backslash-escape).\n * 2. **caret-escaping** every `cmd.exe` metacharacter — including the\n * quotes from phase 1 — so `cmd.exe` is never in quote-mode, strips\n * the carets, and hands the argv-quoted string to the program.\n *\n * `%` is special: it cannot be reliably escaped on the `cmd.exe`\n * *command line* (caret does not stop `%VAR%` expansion there). Rather\n * than mis-escape, we **throw** — our callers never pass `%`, so this\n * fails closed on the one unescapable injection vector.\n */\nexport function quoteWinArg(arg: string): string {\n if (arg.includes(\"%\")) {\n throw new Error(\n \"buildExecInvocation: argument contains '%', which cannot be safely \" +\n \"escaped on the Windows command line; refusing to build the command.\",\n )\n }\n\n // Phase 1 — argv quoting.\n let quoted: string\n if (arg.length > 0 && !/[ \\t\\n\\v\"&|<>()^!]/.test(arg)) {\n quoted = arg\n } else {\n let s = '\"'\n let backslashes = 0\n for (const ch of arg) {\n if (ch === \"\\\\\") {\n backslashes++\n } else if (ch === '\"') {\n s += \"\\\\\".repeat(backslashes * 2 + 1) + '\"'\n backslashes = 0\n } else {\n s += \"\\\\\".repeat(backslashes) + ch\n backslashes = 0\n }\n }\n s += \"\\\\\".repeat(backslashes * 2) + '\"'\n quoted = s\n }\n\n // Phase 2 — caret-escape all cmd.exe metacharacters (and the carets\n // themselves). cmd strips these; the program sees the phase-1 string.\n return quoted.replace(/[()!^\"<>&|]/g, \"^$&\")\n}\n\nexport interface ExecInvocation {\n command: string\n args: string[]\n shell: boolean\n}\n\n/**\n * Build the platform-correct `spawn` invocation for a command given as\n * an argv array. Pure / unit-testable (no spawn).\n *\n * - win32 → a single caret/argv-quoted command string + `shell:true`\n * + empty args array (the empty array avoids the DEP0190 warning\n * that fires when args and `shell:true` are combined). `cmd[0]`\n * should already be an absolute path from `resolveExecutable`.\n * - posix → `(cmd[0], cmd.slice(1))` with `shell:false` — no shell,\n * no injection surface.\n */\nexport function buildExecInvocation(\n cmd: string[],\n platform: NodeJS.Platform = process.platform,\n): ExecInvocation {\n if (cmd.length === 0) throw new Error(\"buildExecInvocation: empty command\")\n if (platform === \"win32\") {\n return { command: cmd.map(quoteWinArg).join(\" \"), args: [], shell: true }\n }\n return { command: cmd[0], args: cmd.slice(1), shell: false }\n}\n\nexport interface RunOpts {\n cwd?: string\n /** Hard timeout in ms; the process tree is killed on expiry. */\n timeoutMs?: number\n /** Extra env to merge over the parent env for the child. */\n env?: NodeJS.ProcessEnv\n /**\n * Byte ceiling on captured stdout. Defaults to {@link DEFAULT_CAPTURE_CAP}.\n *\n * On overflow we STOP BUFFERING but deliberately do NOT kill the child, and\n * the result is reported as a normal completion with `truncated: true`. That\n * asymmetry against `runManagedExeCapture` (which tree-kills on overflow) is\n * load-bearing: callers here read the exit CODE as a verdict —\n * `~/lib/orchestration/live-exec` maps `code ?? 1` onto a FAILED GATE — so\n * killing the child would turn \"this command printed a lot\" into \"typecheck\n * failed\". Truncation is a capture-side limit, never a command outcome.\n *\n * The data handler keeps firing and discarding after the cap, so the pipe\n * drains and the child never blocks writing into a full buffer.\n */\n maxStdoutBytes?: number\n}\n\n/**\n * Default stdout capture ceiling.\n *\n * A `timeoutMs` is NOT a memory bound: `git diff HEAD` on a repo with a large\n * generated file happily emits hundreds of MB inside a 5s window, and every\n * byte was being concatenated onto one JS string. 16 MiB matches the worker\n * layer's `GH_ROUTER_WORKER_MAX_TOOL_BYTES` so the two capture paths agree on\n * what \"too much output to be useful\" means.\n */\nexport const DEFAULT_CAPTURE_CAP = 16 * 1024 * 1024\n\n/** Same ceiling shape for stderr, which only ever feeds diagnostics. */\nconst CAPTURE_STDERR_CAP = 64 * 1024\n\nexport interface RunResult {\n stdout: string\n stderr: string\n /** Exit code; `null` when killed by signal/timeout. */\n code: number | null\n timedOut: boolean\n /**\n * True when stdout exceeded the cap and was cut short. The command itself\n * still ran to completion — check this, never `code`, to detect truncation.\n */\n truncated: boolean\n /**\n * True when stderr exceeded its 64 KiB ceiling and was cut short.\n *\n * Reported separately from `truncated` because it means something different\n * to a caller: stdout truncation loses DATA, stderr truncation loses a\n * DIAGNOSTIC. A failing compiler often puts the root error at the tail of a\n * long stderr, so a caller that surfaces stderr to a human needs to know it\n * is looking at a prefix rather than the whole story.\n */\n stderrTruncated: boolean\n}\n\nfunction runInternal(\n cmd: string[],\n stdoutMode: \"pipe\" | \"inherit\" | \"ignore\",\n opts: RunOpts,\n): Promise<RunResult> {\n const { command, args, shell } = buildExecInvocation(cmd)\n return new Promise<RunResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, args, {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell,\n windowsHide: true,\n stdio: [\n \"ignore\",\n stdoutMode,\n stdoutMode === \"inherit\" ? \"inherit\" : \"pipe\",\n ],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n // Buffer BYTES and decode once at the end (or incrementally through a\n // streaming decoder). The previous `stdout += chunk.toString(\"utf8\")` per\n // chunk corrupted any multi-byte character that straddled a chunk\n // boundary — a 3-byte `→` split 1/2 decoded as two replacement chars, so\n // captured diffs and command output silently mangled non-ASCII.\n const stdoutDecoder = new TextDecoder()\n const stderrDecoder = new TextDecoder()\n let stdout = \"\"\n let stderr = \"\"\n let stdoutBytes = 0\n let stderrBytes = 0\n let truncated = false\n let stderrTruncated = false\n let timedOut = false\n let settled = false\n const maxStdoutBytes = opts.maxStdoutBytes ?? DEFAULT_CAPTURE_CAP\n\n const timer = opts.timeoutMs\n ? setTimeout(() => {\n timedOut = true\n killTree(child.pid)\n }, opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n // Past the cap: keep draining the pipe, stop growing the string. The\n // child is intentionally left alive (see `maxStdoutBytes` docs).\n if (truncated) return\n const room = maxStdoutBytes - stdoutBytes\n if (c.length > room) {\n // Append only the slice that fits, then stop. `stream: true` lets the\n // decoder hold a partial code point back rather than emit U+FFFD, so a\n // cap landing mid-character truncates cleanly instead of corrupting.\n if (room > 0) {\n stdout += stdoutDecoder.decode(c.subarray(0, room), { stream: true })\n stdoutBytes += room\n }\n truncated = true\n return\n }\n stdout += stdoutDecoder.decode(c, { stream: true })\n stdoutBytes += c.length\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n // Already full: this chunk is dropped entirely, which is truncation just\n // as much as a partial slice is. Flagging only the slicing chunk missed\n // the common case where the cap lands on a chunk boundary and every\n // subsequent chunk returns here.\n if (stderrBytes >= CAPTURE_STDERR_CAP) {\n stderrTruncated = true\n return\n }\n const room = CAPTURE_STDERR_CAP - stderrBytes\n const slice = c.length > room ? c.subarray(0, room) : c\n if (slice.length < c.length) stderrTruncated = true\n stderr += stderrDecoder.decode(slice, { stream: true })\n stderrBytes += slice.length\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n // Flush the decoders' held-back bytes.\n //\n // NOT flushed on the truncated path: the cap deliberately cuts mid-byte-\n // sequence, so the decoder is holding a partial code point we chose to\n // drop. Flushing it would emit U+FFFD — turning a clean truncation into\n // apparent corruption, which is the exact failure this fix removes.\n // Un-truncated output has no such intent, so a trailing partial there IS\n // malformed input and should surface as U+FFFD rather than vanish.\n if (!truncated) stdout += stdoutDecoder.decode()\n if (!stderrTruncated) stderr += stderrDecoder.decode()\n resolve({ stdout, stderr, code, timedOut, truncated, stderrTruncated })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/**\n * Spawn `taskkill` best-effort, without any chance of taking the proxy down.\n *\n * Two things a bare `try { spawn(\"taskkill\", ...) } catch {}` gets wrong, both\n * of which this helper is the single place that fixes:\n *\n * 1. **The catch cannot catch.** `spawn` reports ENOENT / EPERM / EACCES by\n * emitting `'error'` ASYNCHRONOUSLY, so the surrounding `try` (which only\n * guards the synchronous throw) never sees it. An EventEmitter `'error'`\n * with no listener THROWS — which lands in the `uncaughtException` handler\n * in `~/main` and calls `process.exit(1)`. A best-effort child kill on a\n * timeout path could therefore kill the whole proxy. The listener must be\n * attached synchronously at the spawn site, which is why this is a helper\n * and not a convention.\n * 2. **The bare name is hijackable.** `CreateProcess`'s search order can\n * include the cwd, so `\"taskkill\"` may resolve to a planted binary;\n * `taskkillExe()` pins `System32\\taskkill.exe`.\n */\nexport function spawnTaskkillBestEffort(pid: number): boolean {\n try {\n const child = spawn(taskkillExe(), [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n })\n // Load-bearing: see (1) above. Without this an async spawn failure is an\n // uncaught exception, not a no-op. It is NOT a silent swallow, either —\n // the fallback below is what keeps a caller from hanging.\n //\n // (No logging here on purpose: this module has no logger import, and\n // taking one would pull a dependency into the lowest-level exec helper\n // that every other module builds on. The fallback is the observable\n // behavior that matters.)\n child.on(\"error\", () => {\n fallbackKill(pid)\n })\n return true\n } catch {\n // Synchronous spawn failure (bad argv shape).\n fallbackKill(pid)\n return false\n }\n}\n\n/**\n * Last-resort single-process kill when `taskkill` itself cannot run.\n *\n * Not a tree kill — grandchildren survive — but it is the difference between\n * \"the timed-out child dies and its `close` event settles the caller\" and \"the\n * caller waits forever\". `runInternal` resolves ONLY from `close`, so a timeout\n * whose kill never lands leaves that promise pending for the life of the\n * process, which is a worse failure than the crash `spawnTaskkillBestEffort`\n * was written to prevent. Best-effort by construction: ESRCH means the child\n * was already gone, which is the success case.\n */\nfunction fallbackKill(pid: number): void {\n try {\n process.kill(pid)\n } catch {\n // Already exited, or we lack permission — nothing further to try.\n }\n}\n\n/** Kill a process tree best-effort (taskkill /T on Windows). */\nfunction killTree(pid: number | undefined): void {\n if (!pid) return\n try {\n if (process.platform === \"win32\") {\n spawnTaskkillBestEffort(pid)\n } else {\n process.kill(pid, \"SIGTERM\")\n }\n } catch {\n // already gone\n }\n}\n\n/** Run a command and capture stdout/stderr. Rejects on spawn error. */\nexport function runCommandCapture(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command discarding output (still captures stderr for errors). */\nexport function runCommandVoid(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"pipe\", opts)\n}\n\n/** Run a command with the child's stdout/stderr inherited to the user. */\nexport function runCommandInherit(\n cmd: string[],\n opts: RunOpts = {},\n): Promise<RunResult> {\n return runInternal(cmd, \"inherit\", opts)\n}\n\nexport interface ManagedExeOpts extends RunOpts {\n /**\n * Hard cap on captured stdout bytes. On overflow the child is\n * tree-killed and the result carries `stdoutTruncated: true`. Defends\n * against a full-`CodeUnit` colgrep `--json` payload (source + 5\n * analysis layers per hit) bloating memory.\n */\n maxStdoutBytes?: number\n /**\n * When true, exceeding `maxStdoutBytes` does NOT tree-kill the child — it\n * sets `stdoutTruncated` and stops BUFFERING further stdout while still\n * draining the pipe (no backpressure). Use for a child whose kill is unsafe\n * (e.g. colgrep, which writes a non-atomic index — a byte-cap kill during\n * its result output could interrupt a write). The child runs to completion\n * (bounded by `timeoutMs` / `inactivityTimeoutMs`). Default false (kill).\n */\n truncateInsteadOfKill?: boolean\n /**\n * Called synchronously with the spawned child right after spawn\n * succeeds, BEFORE any output arrives. The colbert lifecycle ledger\n * uses this to register the child so a session-exit sweep can\n * tree-kill an orphan. Never throws into the runner.\n */\n onSpawn?: (child: ReturnType<typeof spawn>) => void\n /**\n * Inactivity (stall) watchdog. When set, a timer is armed for\n * `inactivityTimeoutMs` and RESET on every stdout/stderr data chunk. On\n * expiry (no output for the window) it consults `onInactivityCheck`: a\n * `true` return means \"still making progress via an out-of-band signal\"\n * (e.g. the colgrep index dir is still growing on disk even though the\n * process is silent on a non-TTY pipe) and the watchdog re-arms; a\n * `false`/absent return tree-kills the child and sets `stalled: true`.\n * This is the progress-based \"stuck\" detector that lets a long-but-\n * progressing build run to completion while still killing a hung one —\n * independent of the coarse total `timeoutMs` backstop.\n */\n inactivityTimeoutMs?: number\n /** External progress probe consulted when the inactivity timer fires.\n * Return `true` to re-arm (still progressing), `false`/throw to kill.\n * MUST be cheap + synchronous (called on a timer, not awaited). */\n onInactivityCheck?: () => boolean\n}\n\nexport interface ManagedExeResult extends RunResult {\n /** True iff stdout was truncated at `maxStdoutBytes` (child was killed). */\n stdoutTruncated: boolean\n /** True iff the inactivity watchdog killed the child (no progress). */\n stalled: boolean\n}\n\n/**\n * Run a **native executable** (a real `.exe`/Mach-O/ELF, NOT a `.cmd`\n * shim) capturing stdout/stderr, with `shell:false` on EVERY platform.\n *\n * Why a separate runner from `runCommandCapture`: that path routes\n * through `buildExecInvocation`, which on Windows builds a\n * `cmd.exe`-quoted command string and **throws on `%`** (`quoteWinArg`).\n * A workspace path can legally contain `%` (and `&`, `(`, `)`, `!`, …),\n * so the managed colgrep binary — which IS a native `.exe`, not a shim —\n * must bypass cmd.exe entirely. `spawn(absExe, args, {shell:false})`\n * resolves the `.exe` via CreateProcess directly: no cmd.exe, no\n * metacharacter hazard, no `%` refusal. POSIX was already `shell:false`.\n * This is what makes \"ANY absolute workspace\" hold on Windows.\n *\n * Lifecycle:\n * - `timeoutMs` → tree-kill on expiry (`taskkill /T /F` on Windows,\n * POSIX process-group `kill(-pgid)` so colgrep's rayon worker\n * children die too). `timedOut: true` in the result.\n * - `maxStdoutBytes` → tree-kill + `stdoutTruncated: true` once the\n * captured stdout exceeds the cap.\n * - `onSpawn(child)` → register the child with the caller's ledger.\n *\n * `command` MUST be an absolute path to the executable (the caller\n * resolves it; we never search PATH here — there is nothing to inject).\n */\nexport function runManagedExeCapture(\n command: string,\n args: ReadonlyArray<string>,\n opts: ManagedExeOpts = {},\n): Promise<ManagedExeResult> {\n const isWin = process.platform === \"win32\"\n return new Promise<ManagedExeResult>((resolve, reject) => {\n let child: ReturnType<typeof spawn>\n try {\n child = spawn(command, [...args], {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n shell: false,\n windowsHide: true,\n // POSIX: new process group so we can kill the whole tree\n // (colgrep + rayon workers) with kill(-pgid). Windows uses\n // taskkill /T instead; `detached` there has no group semantics.\n detached: !isWin,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n try {\n opts.onSpawn?.(child)\n } catch {\n // ledger registration must never break the spawn\n }\n\n const chunks: Array<Buffer> = []\n let stdoutBytes = 0\n const stderrChunks: Array<Buffer> = []\n let stderrBytes = 0\n const STDERR_CAP = 64 * 1024\n let timedOut = false\n let stdoutTruncated = false\n let stalled = false\n let settled = false\n\n // Exactly one terminator wins: the first to fire records its reason and\n // tree-kills; later ones no-op. Keeps timedOut / stalled / stdoutTruncated\n // mutually exclusive and avoids a double tree-kill.\n let terminated = false\n const terminate = (reason: \"timeout\" | \"stall\" | \"truncate\"): void => {\n if (terminated) return\n terminated = true\n if (reason === \"timeout\") timedOut = true\n else if (reason === \"stall\") stalled = true\n else stdoutTruncated = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n killManagedTree(child, isWin)\n }\n\n const timer = opts.timeoutMs\n ? setTimeout(() => terminate(\"timeout\"), opts.timeoutMs)\n : undefined\n timer?.unref?.()\n\n // Inactivity (stall) watchdog. Re-arms itself: when the window elapses\n // with no output, consult the external progress probe — re-arm if it\n // says \"still progressing\" (e.g. the index dir grew), else kill + mark\n // stalled. Reset on every data chunk (a chatty process never stalls).\n let inactivityTimer: ReturnType<typeof setTimeout> | undefined\n const armInactivity = (): void => {\n if (opts.inactivityTimeoutMs === undefined || settled || terminated) return\n inactivityTimer = setTimeout(() => {\n if (settled) return\n // No probe → pure output-inactivity kill. A probe that THROWS is\n // inconclusive → don't kill (re-arm); the absolute timeoutMs backstop\n // still catches a genuinely wedged process.\n let progressing = false\n if (opts.onInactivityCheck) {\n try {\n progressing = opts.onInactivityCheck() === true\n } catch {\n progressing = true\n }\n }\n if (progressing) {\n armInactivity()\n return\n }\n terminate(\"stall\")\n }, opts.inactivityTimeoutMs)\n inactivityTimer?.unref?.()\n }\n const resetInactivity = (): void => {\n if (inactivityTimer) clearTimeout(inactivityTimer)\n armInactivity()\n }\n armInactivity()\n\n child.stdout?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n if (stdoutTruncated) return\n stdoutBytes += c.length\n if (\n opts.maxStdoutBytes !== undefined &&\n stdoutBytes > opts.maxStdoutBytes\n ) {\n stdoutTruncated = true\n // Default: tree-kill on overflow. Opt-in: keep the child alive and\n // just stop buffering (the data handler keeps firing + discarding via\n // the `if (stdoutTruncated) return` guard above, so the pipe drains\n // and the child never blocks on a full buffer).\n if (!opts.truncateInsteadOfKill) terminate(\"truncate\")\n return\n }\n chunks.push(c)\n })\n child.stderr?.on(\"data\", (c: Buffer) => {\n resetInactivity()\n // Hard byte cap on stderr — append only the slice that fits so a\n // single huge chunk can't overshoot. Never logged raw (it can\n // embed source code from colgrep).\n if (stderrBytes >= STDERR_CAP) return\n const remaining = STDERR_CAP - stderrBytes\n const slice = c.length > remaining ? c.subarray(0, remaining) : c\n stderrChunks.push(slice)\n stderrBytes += slice.length\n })\n child.stdout?.on(\"error\", () => {})\n child.stderr?.on(\"error\", () => {})\n\n const finish = (code: number | null): void => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n resolve({\n stdout: Buffer.concat(chunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n code,\n timedOut,\n stdoutTruncated,\n // `RunResult.truncated` is the shared, runner-agnostic signal. Kept in\n // lockstep with `stdoutTruncated` (this runner's older, more specific\n // name, which callers already branch on) so a caller holding either\n // shape reads the same fact.\n truncated: stdoutTruncated,\n // This runner slices stderr to fit its own 64 KiB cap without tracking\n // whether it did, so report the honest thing rather than guess.\n stderrTruncated: stderrBytes >= STDERR_CAP,\n stalled,\n })\n }\n\n child.on(\"error\", (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n if (inactivityTimer) clearTimeout(inactivityTimer)\n reject(err)\n })\n child.on(\"close\", (code) => finish(code))\n })\n}\n\n/**\n * Tree-kill a managed child. Windows: `taskkill /T /F /PID` (whole\n * tree). POSIX: kill the process GROUP (`-pgid`) so colgrep's rayon\n * worker children die with the parent.\n *\n * `runManagedExeCapture` always spawns POSIX children `detached:true`\n * (their own process group), so the group kill is the correct and\n * sufficient primitive. We deliberately do NOT fall back to a positive-\n * pid `process.kill(pid)` when the group kill fails: by the time a kill\n * fires (timeout / byte-cap race), the child may have already exited and\n * its PID been recycled by an unrelated process — a positive-pid kill\n * would then target the wrong process. `ESRCH` (group already gone) is\n * the success case for our purposes; any other error is swallowed.\n */\nexport function killManagedTree(\n child: ReturnType<typeof spawn>,\n isWin: boolean = process.platform === \"win32\",\n): void {\n const pid = child.pid\n if (!pid) return\n try {\n if (isWin) {\n spawnTaskkillBestEffort(pid)\n } else {\n // Negative pid → the process group (we spawned detached). No\n // positive-pid fallback (PID-reuse hazard — see doc comment).\n process.kill(-pid, \"SIGKILL\")\n }\n } catch {\n // ESRCH (already gone) / EPERM — best-effort, nothing more to do.\n }\n}\n\n/**\n * Absolute path to Windows `taskkill.exe`. Bare `\"taskkill\"` would let\n * `CreateProcess`'s search order (which can include the cwd) run a planted\n * `taskkill.exe`; pinning the System32 path closes that hijack.\n */\nfunction taskkillExe(): string {\n const root =\n process.env.SystemRoot || process.env.windir || \"C:\\\\Windows\"\n return path.join(root, \"System32\", \"taskkill.exe\")\n}\n\n/**\n * Tree-kill a child process on a SHUTDOWN/exit path, deterministically.\n *\n * Distinct from `killManagedTree` in two ways that matter when the proxy\n * is about to call `process.exit`:\n *\n * 1. **Windows uses `spawnSync`, not async `spawn`.** An async taskkill\n * may never issue before `process.exit` tears the event loop down;\n * the synchronous form guarantees the kill command has run. It is\n * time-bounded so a wedged taskkill can't hang shutdown.\n * 2. **POSIX honors `detachedGroup`.** The launched CLI is spawned\n * `detached:true` (its own process group) so `kill(-pid)` reaps the\n * whole group; other callers (e.g. ripgrep) are NOT detached, so\n * `child.kill()` on the positive pid is correct. A negative-pid kill\n * on a non-detached child would throw or hit the wrong target.\n *\n * Both branches escalate SIGTERM → SIGKILL after `graceMs` so a CLI that\n * traps SIGTERM still dies. The escalation timer is `.unref()`'d so it\n * never holds the event loop open. Errors are swallowed (already-gone /\n * EBUSY / ESRCH are the success case for our purposes).\n */\nexport function killChildProcessTree(\n child: ChildProcess,\n opts: { detachedGroup: boolean; graceMs?: number } = { detachedGroup: false },\n): void {\n const pid = child.pid\n if (!pid) return\n const graceMs = opts.graceMs ?? 500\n\n if (process.platform === \"win32\") {\n // /T = whole tree, /F = force. taskkill walks the live parent-PID\n // chain, so it reaps grandchildren regardless of the cmd.exe-vs-direct\n // spawn shape. Sync (completes before an imminent process.exit) and\n // time-bounded (a wedged taskkill must not hang shutdown).\n try {\n spawnSync(taskkillExe(), [\"/T\", \"/F\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n windowsHide: true,\n timeout: 5000,\n })\n } catch {\n // Already gone / EBUSY race — best-effort.\n }\n return\n }\n\n // POSIX: signal the group (detached) or the lone child (non-detached).\n const target = opts.detachedGroup ? -pid : pid\n try {\n process.kill(target, \"SIGTERM\")\n } catch {\n // ESRCH — already gone.\n }\n const t = setTimeout(() => {\n // For a lone (non-detached) child, skip escalation once it has exited\n // — its positive PID could have been recycled. For a process GROUP the\n // leader's exit does NOT mean the group is empty (a SIGTERM-ignoring\n // grandchild may survive), and the group id is only reused once the\n // group is fully gone, so always attempt the group SIGKILL (ESRCH when\n // already empty is the success case).\n if (\n !opts.detachedGroup &&\n (child.exitCode !== null || child.signalCode !== null)\n ) {\n return\n }\n try {\n process.kill(target, \"SIGKILL\")\n } catch {\n // Already dead.\n }\n }, graceMs)\n t.unref?.()\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { t as PATHS } from "./paths-De1Qh9Q9.js";
2
- import { a as resolveExecutable } from "./exec-y8C_MU8A.js";
3
- import { f as resolveSealedGate } from "./stop-gate-hook-BEU8qQrC.js";
4
- import { c as repoRoot, s as repoFingerprint } from "./stop-gate-policy-qzqXfRxE.js";
1
+ import { t as PATHS } from "./paths-CMxlQEfQ.js";
2
+ import { o as resolveExecutable } from "./exec-COUT4QsS.js";
3
+ import { f as resolveSealedGate } from "./stop-gate-hook-BUCxa5Kc.js";
4
+ import { c as repoRoot, s as repoFingerprint } from "./stop-gate-policy--bpyxxXU.js";
5
5
  import { existsSync, promises } from "node:fs";
6
6
  import path from "node:path";
7
7
  import { createHash } from "node:crypto";
@@ -606,7 +606,7 @@ async function discoverGateCommands(cwd, opts) {
606
606
  if (files.length === 0) return null;
607
607
  let result;
608
608
  try {
609
- const { runWorkerAgent } = await import("./engine-DsVZuCrD.js");
609
+ const { runWorkerAgent } = await import("./engine-DwrxceOS.js");
610
610
  result = await runWorkerAgent({
611
611
  mode: "explore",
612
612
  workspace: root,
@@ -632,4 +632,4 @@ async function discoverGateCommands(cwd, opts) {
632
632
  //#endregion
633
633
  export { checksForDescriptor as a, writeDiscoveredGate as i, readDiscoveredGate as n, descriptorHash as o, sourcesHash as r, parseGateDescriptor as s, discoverGateCommands as t };
634
634
 
635
- //# sourceMappingURL=gate-discovery-CO2njGBB.js.map
635
+ //# sourceMappingURL=gate-discovery-z04ywOzB.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate-discovery-CO2njGBB.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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"}
1
+ {"version":3,"file":"gate-discovery-z04ywOzB.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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"}
@@ -1,5 +1,5 @@
1
- import { t as state } from "./state-B8adscCv.js";
2
- import { S as GITHUB_API_BASE_URL, g as HTTPError, k as githubHeaders, y as fetchWithTransientRetry } from "./token-CCg-Ov5q.js";
1
+ import { t as state } from "./state-BlMpEFAt.js";
2
+ import { S as GITHUB_API_BASE_URL, g as HTTPError, k as githubHeaders, y as fetchWithTransientRetry } from "./token-C1gthnbV.js";
3
3
  //#region src/services/github/get-copilot-usage.ts
4
4
  const getCopilotUsage = async () => {
5
5
  const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(state) }), { label: "/copilot_internal/user" });
@@ -9,4 +9,4 @@ const getCopilotUsage = async () => {
9
9
  //#endregion
10
10
  export { getCopilotUsage as t };
11
11
 
12
- //# sourceMappingURL=get-copilot-usage-Cjy8TiFf.js.map
12
+ //# sourceMappingURL=get-copilot-usage-BMD_cJNT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-copilot-usage-Cjy8TiFf.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"}
1
+ {"version":3,"file":"get-copilot-usage-BMD_cJNT.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"}