github-router 0.3.293 → 0.3.295
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{attribution-settings-Dx8TBPE8.js → attribution-settings-pkIop1NU.js} +67 -28
- package/dist/attribution-settings-pkIop1NU.js.map +1 -0
- package/dist/{auth-BwUHopJz.js → auth-CluG5e-l.js} +3 -3
- package/dist/{auth-BwUHopJz.js.map → auth-CluG5e-l.js.map} +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{check-usage-BTda5753.js → check-usage-iOzfnKJe.js} +4 -4
- package/dist/{check-usage-BTda5753.js.map → check-usage-iOzfnKJe.js.map} +1 -1
- package/dist/{claude-BoAd_L2W.js → claude-DNmxk-Rm.js} +142 -30
- package/dist/claude-DNmxk-Rm.js.map +1 -0
- package/dist/{codex-BdJgGT6Q.js → codex-Dl4gpHn-.js} +5 -5
- package/dist/{codex-BdJgGT6Q.js.map → codex-Dl4gpHn-.js.map} +1 -1
- package/dist/{debug-B3UrZTHQ.js → debug-DsEwleGg.js} +2 -2
- package/dist/{debug-B3UrZTHQ.js.map → debug-DsEwleGg.js.map} +1 -1
- package/dist/engine-XnluJUs6.js +2 -0
- package/dist/{gate-discovery-C30rfnyo.js → gate-discovery-WcduofL4.js} +5 -5
- package/dist/{gate-discovery-C30rfnyo.js.map → gate-discovery-WcduofL4.js.map} +1 -1
- package/dist/{get-copilot-usage-CRrf1ZSC.js → get-copilot-usage-B7Z4XVWj.js} +2 -2
- package/dist/{get-copilot-usage-CRrf1ZSC.js.map → get-copilot-usage-B7Z4XVWj.js.map} +1 -1
- package/dist/hooks.mjs +180 -3
- package/dist/hooks.sha256 +1 -1
- package/dist/{internal-artifact-open-BsEQRvDi.js → internal-artifact-open-D6tKMg8v.js} +2 -2
- package/dist/{internal-artifact-open-BsEQRvDi.js.map → internal-artifact-open-D6tKMg8v.js.map} +1 -1
- package/dist/internal-fast-dispatch-guard-CPlCrTlU.js +2 -0
- package/dist/internal-fast-dispatch-guard-Xyu17k9X.js +187 -0
- package/dist/internal-fast-dispatch-guard-Xyu17k9X.js.map +1 -0
- package/dist/{internal-first-mate-guard-CVFFJriA.js → internal-first-mate-guard-C9BV7J4U.js} +3 -3
- package/dist/{internal-first-mate-guard-CVFFJriA.js.map → internal-first-mate-guard-C9BV7J4U.js.map} +1 -1
- package/dist/{internal-first-mate-guard-DJdX6t48.js → internal-first-mate-guard-WJApyc8j.js} +1 -1
- package/dist/{internal-plan-review-8TKDLco6.js → internal-plan-review-t3PuNfuL.js} +3 -3
- package/dist/{internal-plan-review-8TKDLco6.js.map → internal-plan-review-t3PuNfuL.js.map} +1 -1
- package/dist/{internal-prompt-submit-Da7pxqua.js → internal-prompt-submit-C3KovHQG.js} +4 -4
- package/dist/{internal-prompt-submit-Da7pxqua.js.map → internal-prompt-submit-C3KovHQG.js.map} +1 -1
- package/dist/{internal-session-bind-BOFytA1f.js → internal-session-bind-d2uoja8I.js} +2 -2
- package/dist/{internal-session-bind-BOFytA1f.js.map → internal-session-bind-d2uoja8I.js.map} +1 -1
- package/dist/{internal-stop-hook-OnfK3BxE.js → internal-stop-hook-DJFtGCsE.js} +5 -5
- package/dist/{internal-stop-hook-OnfK3BxE.js.map → internal-stop-hook-DJFtGCsE.js.map} +1 -1
- package/dist/{internal-stop-review-CdouacHL.js → internal-stop-review-YtDUGXpX.js} +2 -2
- package/dist/{internal-stop-review-CdouacHL.js.map → internal-stop-review-YtDUGXpX.js.map} +1 -1
- package/dist/{internal-worker-guard-Bx-itoP8.js → internal-worker-guard-CdkeS9PN.js} +2 -2
- package/dist/{internal-worker-guard-Bx-itoP8.js.map → internal-worker-guard-CdkeS9PN.js.map} +1 -1
- package/dist/{internal-workspace-header-8WT0iB5K.js → internal-workspace-header-tuBXJV22.js} +2 -2
- package/dist/{internal-workspace-header-8WT0iB5K.js.map → internal-workspace-header-tuBXJV22.js.map} +1 -1
- package/dist/{lifecycle-nuOHfwgj.js → lifecycle-Bg6doY3-.js} +2 -2
- package/dist/{lifecycle-nuOHfwgj.js.map → lifecycle-Bg6doY3-.js.map} +1 -1
- package/dist/lifecycle-CGLt1cJQ.js +2 -0
- package/dist/{lifecycle-LeSfa7wH.js → lifecycle-CM9eTzvk.js} +2 -2
- package/dist/{lifecycle-LeSfa7wH.js.map → lifecycle-CM9eTzvk.js.map} +1 -1
- package/dist/lifecycle-DoUwpVDB.js +2 -0
- package/dist/main.js +19 -18
- package/dist/main.js.map +1 -1
- package/dist/{mcp-workspace-header-q34H_4wL.js → mcp-workspace-header-dERl2YTT.js} +2 -2
- package/dist/{mcp-workspace-header-q34H_4wL.js.map → mcp-workspace-header-dERl2YTT.js.map} +1 -1
- package/dist/{models-hhJcrZhr.js → models-DZ4hYsR7.js} +3 -3
- package/dist/{models-hhJcrZhr.js.map → models-DZ4hYsR7.js.map} +1 -1
- package/dist/{orchestration-pzbrKkgD.js → orchestration-BiGCEwbH.js} +2 -2
- package/dist/{orchestration-pzbrKkgD.js.map → orchestration-BiGCEwbH.js.map} +1 -1
- package/dist/{paths-BH4J7slC.js → paths-Ci485qSJ.js} +4 -4
- package/dist/{paths-BH4J7slC.js.map → paths-Ci485qSJ.js.map} +1 -1
- package/dist/paths-GD7bgGGy.js +2 -0
- package/dist/{peer-mcp-personas-DhI7ZPSx.js → peer-mcp-personas-BwNtC8jt.js} +385 -75
- package/dist/peer-mcp-personas-BwNtC8jt.js.map +1 -0
- package/dist/{plan-review-hook-CfcanA7_.js → plan-review-hook-DuW0pSoM.js} +3 -3
- package/dist/{plan-review-hook-CfcanA7_.js.map → plan-review-hook-DuW0pSoM.js.map} +1 -1
- package/dist/{prompt-submit-hook-Bqf9ORgb.js → prompt-submit-hook-IMl7ssfr.js} +3 -3
- package/dist/{prompt-submit-hook-Bqf9ORgb.js.map → prompt-submit-hook-IMl7ssfr.js.map} +1 -1
- package/dist/{provision-CZJ4EWls.js → provision-DeNqzvSM.js} +4 -4
- package/dist/{provision-CZJ4EWls.js.map → provision-DeNqzvSM.js.map} +1 -1
- package/dist/{self-invocation-DhO1Z8iD.js → self-invocation-DAB_od0C.js} +2 -2
- package/dist/{self-invocation-DhO1Z8iD.js.map → self-invocation-DAB_od0C.js.map} +1 -1
- package/dist/{serve-RxWYzoOl.js → serve-BS5EmCpM.js} +12 -12
- package/dist/{serve-RxWYzoOl.js.map → serve-BS5EmCpM.js.map} +1 -1
- package/dist/{server-setup-DbvbW5Ve.js → server-setup-DsGJnJ_O.js} +379 -271
- package/dist/server-setup-DsGJnJ_O.js.map +1 -0
- package/dist/{start-BNcGsXsY.js → start-vJdt2MiM.js} +3 -3
- package/dist/{start-BNcGsXsY.js.map → start-vJdt2MiM.js.map} +1 -1
- package/dist/{stop-gate-hook-BiBp5aGm.js → stop-gate-hook-DQnh_KfI.js} +3 -3
- package/dist/{stop-gate-hook-BiBp5aGm.js.map → stop-gate-hook-DQnh_KfI.js.map} +1 -1
- package/dist/{stop-gate-policy-BGd6b5hR.js → stop-gate-policy-DMr3KVmw.js} +2 -2
- package/dist/{stop-gate-policy-BGd6b5hR.js.map → stop-gate-policy-DMr3KVmw.js.map} +1 -1
- package/dist/{token-8drORhXg.js → token-BtJhjXXu.js} +54 -7
- package/dist/token-BtJhjXXu.js.map +1 -0
- package/dist/{worker-dispatch-D5fGroNr.js → worker-dispatch-BEHkTbnH.js} +2 -2
- package/dist/{worker-dispatch-D5fGroNr.js.map → worker-dispatch-BEHkTbnH.js.map} +1 -1
- package/package.json +1 -1
- package/dist/attribution-settings-Dx8TBPE8.js.map +0 -1
- package/dist/claude-BoAd_L2W.js.map +0 -1
- package/dist/engine-CK2b_cTt.js +0 -2
- package/dist/lifecycle-C8fOsQke.js +0 -2
- package/dist/lifecycle-D4Yc1aap.js +0 -2
- package/dist/paths-DJZoXfAS.js +0 -2
- package/dist/peer-mcp-personas-DhI7ZPSx.js.map +0 -1
- package/dist/server-setup-DbvbW5Ve.js.map +0 -1
- package/dist/token-8drORhXg.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as parseBoolEnv } from "./exec-y8C_MU8A.js";
|
|
2
|
-
import { t as buildSelfCommand } from "./self-invocation-
|
|
3
|
-
import { a as isSubagentContext } from "./stop-gate-policy-
|
|
2
|
+
import { t as buildSelfCommand } from "./self-invocation-DAB_od0C.js";
|
|
3
|
+
import { a as isSubagentContext } from "./stop-gate-policy-DMr3KVmw.js";
|
|
4
4
|
import { n as callMcpTool } from "./hook-mcp-client-DBvm9608.js";
|
|
5
5
|
import { promises } from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
@@ -155,4 +155,4 @@ function buildPlanReviewHookCommand(invocation) {
|
|
|
155
155
|
//#endregion
|
|
156
156
|
export { runPlanReview as a, planReviewEnabled as i, decidePlanReviewHook as n, filePlanReviewDebounce as r, buildPlanReviewHookCommand as t };
|
|
157
157
|
|
|
158
|
-
//# sourceMappingURL=plan-review-hook-
|
|
158
|
+
//# sourceMappingURL=plan-review-hook-DuW0pSoM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-review-hook-CfcanA7_.js","names":["nodePath","fs"],"sources":["../src/lib/orchestration/plan-review-hook.ts"],"sourcesContent":["import { createHash } from \"node:crypto\"\nimport { promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { parseBoolEnv } from \"~/lib/exec\"\nimport { buildSelfCommand, type SelfInvocation } from \"~/lib/hook-launcher/self-invocation\"\n\nimport { callMcpTool, type HookMcpRuntime, type McpToolResult } from \"./hook-mcp-client\"\nimport {\n isSubagentContext,\n type FindingsStore,\n type ReviewDebounce,\n} from \"./stop-gate-policy\"\n\n/** Minimum finalized-plan size worth spending a model review on. */\nexport const PLAN_REVIEW_MIN_CHARS = 220\n\n/** Hard wall-clock for the advisory plan critic. */\nexport const PLAN_REVIEW_TIMEOUT_MS = 25_000\n\n/** Cap the plan embedded in the critic brief so a huge plan file cannot dominate. */\nconst MAX_REVIEWED_PLAN_CHARS = 80 * 1024\n\nexport interface PlanReviewSpawnContext {\n sessionId: string\n cwd: string\n plan: string\n planHash: string\n}\n\nexport type PlanReviewDecision =\n | { kind: \"skip\"; reason: string }\n | ({ kind: \"spawn\" } & PlanReviewSpawnContext)\n\n/** Default-on advisory plan review. Opt out with GH_ROUTER_DISABLE_PLAN_REVIEW=1. */\nexport function planReviewEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_DISABLE_PLAN_REVIEW) !== true\n}\n\n/** File-backed per-session debounce for finalized plans, separate from Stop diff hashes. */\nexport function filePlanReviewDebounce(stateDir: string): ReviewDebounce {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `plan-review-hash-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n const readLast = async (sid: string): Promise<string> => {\n try {\n return (await fs.readFile(fileFor(sid), \"utf8\")).trim()\n } catch {\n return \"\"\n }\n }\n return {\n async shouldReview(sid, planHash) {\n if (planHash.length === 0) return false\n return (await readLast(sid)) !== planHash\n },\n async markReviewed(sid, planHash) {\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), planHash, { mode: 0o600 })\n },\n }\n}\n\nfunction parseObject(raw: string): Record<string, unknown> | undefined {\n try {\n const parsed: unknown = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed as Record<string, unknown>\n } catch {\n /* malformed hook payload -> skip */\n }\n return undefined\n}\n\nasync function resolvePlanText(\n toolInput: unknown,\n readFile: (filePath: string) => Promise<string>,\n): Promise<string> {\n if (!toolInput || typeof toolInput !== \"object\") return \"\"\n const ti = toolInput as { planFilePath?: unknown; plan?: unknown }\n const planFilePath = typeof ti.planFilePath === \"string\" && ti.planFilePath.trim().length > 0\n ? ti.planFilePath.trim()\n : \"\"\n if (planFilePath.length > 0) {\n try {\n const fromFile = await readFile(planFilePath)\n if (fromFile.trim().length > 0) return fromFile\n } catch {\n /* fall through to inline plan */\n }\n }\n return typeof ti.plan === \"string\" ? ti.plan : \"\"\n}\n\n/**\n * Decide whether the ExitPlanMode PostToolUse hook should spawn a detached review.\n * Pure aside from injected file reads + debounce store. Fail-open: any malformed\n * payload, missing runtime, subagent context, trivial/absent plan, or store error\n * returns a skip decision.\n */\nexport async function decidePlanReviewHook(input: {\n stdin: string\n runtimeAvailable: boolean\n debounce: ReviewDebounce\n fallbackCwd: string\n minChars?: number\n readFile?: (filePath: string) => Promise<string>\n}): Promise<PlanReviewDecision> {\n try {\n if (!input.runtimeAvailable) return { kind: \"skip\", reason: \"missing-runtime\" }\n const payload = parseObject(input.stdin)\n if (!payload) return { kind: \"skip\", reason: \"bad-payload\" }\n if (isSubagentContext(payload)) return { kind: \"skip\", reason: \"subagent\" }\n\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (!sessionId) return { kind: \"skip\", reason: \"missing-session\" }\n const cwd = typeof payload.cwd === \"string\" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd\n const plan = await resolvePlanText(\n payload.tool_input,\n input.readFile ?? ((filePath: string) => fs.readFile(filePath, \"utf8\")),\n )\n const trimmed = plan.trim()\n if (trimmed.length < (input.minChars ?? PLAN_REVIEW_MIN_CHARS)) return { kind: \"skip\", reason: \"trivial-plan\" }\n\n const planHash = createHash(\"sha256\").update(trimmed).digest(\"hex\")\n if (!(await input.debounce.shouldReview(sessionId, planHash))) return { kind: \"skip\", reason: \"debounced\" }\n await input.debounce.markReviewed(sessionId, planHash)\n return { kind: \"spawn\", sessionId, cwd, plan: trimmed, planHash }\n } catch {\n return { kind: \"skip\", reason: \"error\" }\n }\n}\n\nfunction reviewBrief(plan: string, cwd: string): string {\n const embeddedPlan =\n plan.length > MAX_REVIEWED_PLAN_CHARS\n ? `${plan.slice(0, MAX_REVIEWED_PLAN_CHARS)}\\n\\n[plan truncated at ${MAX_REVIEWED_PLAN_CHARS} characters]`\n : plan\n return (\n \"You are an independent plan reviewer. A coding agent has just finalized a plan with ExitPlanMode. \"\n + \"Your job is to find load-bearing risks before implementation starts: unstated assumptions, missing \"\n + \"failure modes, gaps in verification, dangerous sequencing, or places where the plan could satisfy the \"\n + \"letter of the request while missing the user's likely intent.\\n\\n\"\n + \"Be concise and skeptical. Do NOT rewrite the plan, do NOT praise it, and do NOT invent new product \"\n + \"requirements. Report only material objections that would change what the agent should do next. Include \"\n + \"file:line anchors when the plan names concrete files or when the risk depends on repo code. If the plan \"\n + \"is sound, say exactly: \\\"no material objection\\\".\\n\\n\"\n + `Workspace: ${cwd}\\n\\n`\n + \"FINALIZED PLAN:\\n\"\n + embeddedPlan\n )\n}\n\nfunction hasMaterialFinding(text: string): boolean {\n const normalized = text.trim().toLowerCase().replace(/[.!]+$/g, \"\")\n return normalized.length > 0 && normalized !== \"no material objection\" && normalized !== \"no material objections\"\n}\n\nfunction framePlanFindings(text: string): string {\n return (\n \"PLAN REVIEW: an independent cross-lab critic reviewed the finalized plan and found \"\n + \"the following material concern(s).\\n\"\n + text.trim()\n )\n}\n\n/** Run the bounded critic call and write material findings to the shared findings store. */\nexport async function runPlanReview(input: {\n runtime: HookMcpRuntime\n sessionId: string\n cwd: string\n plan: string\n findingsStore: FindingsStore\n timeoutMs?: number\n callReview?: (brief: string, signal: AbortSignal) => Promise<McpToolResult>\n}): Promise<void> {\n const timeoutMs = input.timeoutMs ?? PLAN_REVIEW_TIMEOUT_MS\n let timer: ReturnType<typeof setTimeout> | undefined\n const controller = new AbortController()\n try {\n const brief = reviewBrief(input.plan, input.cwd)\n const review = (input.callReview ?? ((prompt: string, signal: AbortSignal) => callMcpTool({\n runtime: input.runtime,\n group: \"peers\",\n tool: \"codex_critic\",\n args: { prompt, effort: \"high\" },\n timeoutMs,\n signal,\n })))(brief, controller.signal)\n review.catch(() => {})\n const raced = await Promise.race<McpToolResult | \"timeout\">([\n review,\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeoutMs)\n }),\n ])\n if (raced === \"timeout\" || raced.isError || !hasMaterialFinding(raced.text)) return\n\n const framed = framePlanFindings(raced.text)\n const existing = await input.findingsStore.read(input.sessionId).catch(() => null)\n const next = existing && existing.trim().length > 0 ? `${existing.trim()}\\n\\n${framed}` : framed\n await input.findingsStore.write(input.sessionId, next)\n } catch {\n /* advisory layer must never surface an error */\n } finally {\n if (timer) clearTimeout(timer)\n controller.abort()\n }\n}\n\n/** Build the command registered for PostToolUse(ExitPlanMode). */\nexport function buildPlanReviewHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-plan-review\")\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plan-review-hook-DuW0pSoM.js","names":["nodePath","fs"],"sources":["../src/lib/orchestration/plan-review-hook.ts"],"sourcesContent":["import { createHash } from \"node:crypto\"\nimport { promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { parseBoolEnv } from \"~/lib/exec\"\nimport { buildSelfCommand, type SelfInvocation } from \"~/lib/hook-launcher/self-invocation\"\n\nimport { callMcpTool, type HookMcpRuntime, type McpToolResult } from \"./hook-mcp-client\"\nimport {\n isSubagentContext,\n type FindingsStore,\n type ReviewDebounce,\n} from \"./stop-gate-policy\"\n\n/** Minimum finalized-plan size worth spending a model review on. */\nexport const PLAN_REVIEW_MIN_CHARS = 220\n\n/** Hard wall-clock for the advisory plan critic. */\nexport const PLAN_REVIEW_TIMEOUT_MS = 25_000\n\n/** Cap the plan embedded in the critic brief so a huge plan file cannot dominate. */\nconst MAX_REVIEWED_PLAN_CHARS = 80 * 1024\n\nexport interface PlanReviewSpawnContext {\n sessionId: string\n cwd: string\n plan: string\n planHash: string\n}\n\nexport type PlanReviewDecision =\n | { kind: \"skip\"; reason: string }\n | ({ kind: \"spawn\" } & PlanReviewSpawnContext)\n\n/** Default-on advisory plan review. Opt out with GH_ROUTER_DISABLE_PLAN_REVIEW=1. */\nexport function planReviewEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBoolEnv(env.GH_ROUTER_DISABLE_PLAN_REVIEW) !== true\n}\n\n/** File-backed per-session debounce for finalized plans, separate from Stop diff hashes. */\nexport function filePlanReviewDebounce(stateDir: string): ReviewDebounce {\n const fileFor = (sid: string): string =>\n nodePath.join(stateDir, `plan-review-hash-${createHash(\"sha256\").update(sid).digest(\"hex\").slice(0, 32)}`)\n const readLast = async (sid: string): Promise<string> => {\n try {\n return (await fs.readFile(fileFor(sid), \"utf8\")).trim()\n } catch {\n return \"\"\n }\n }\n return {\n async shouldReview(sid, planHash) {\n if (planHash.length === 0) return false\n return (await readLast(sid)) !== planHash\n },\n async markReviewed(sid, planHash) {\n await fs.mkdir(stateDir, { recursive: true })\n await fs.writeFile(fileFor(sid), planHash, { mode: 0o600 })\n },\n }\n}\n\nfunction parseObject(raw: string): Record<string, unknown> | undefined {\n try {\n const parsed: unknown = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed as Record<string, unknown>\n } catch {\n /* malformed hook payload -> skip */\n }\n return undefined\n}\n\nasync function resolvePlanText(\n toolInput: unknown,\n readFile: (filePath: string) => Promise<string>,\n): Promise<string> {\n if (!toolInput || typeof toolInput !== \"object\") return \"\"\n const ti = toolInput as { planFilePath?: unknown; plan?: unknown }\n const planFilePath = typeof ti.planFilePath === \"string\" && ti.planFilePath.trim().length > 0\n ? ti.planFilePath.trim()\n : \"\"\n if (planFilePath.length > 0) {\n try {\n const fromFile = await readFile(planFilePath)\n if (fromFile.trim().length > 0) return fromFile\n } catch {\n /* fall through to inline plan */\n }\n }\n return typeof ti.plan === \"string\" ? ti.plan : \"\"\n}\n\n/**\n * Decide whether the ExitPlanMode PostToolUse hook should spawn a detached review.\n * Pure aside from injected file reads + debounce store. Fail-open: any malformed\n * payload, missing runtime, subagent context, trivial/absent plan, or store error\n * returns a skip decision.\n */\nexport async function decidePlanReviewHook(input: {\n stdin: string\n runtimeAvailable: boolean\n debounce: ReviewDebounce\n fallbackCwd: string\n minChars?: number\n readFile?: (filePath: string) => Promise<string>\n}): Promise<PlanReviewDecision> {\n try {\n if (!input.runtimeAvailable) return { kind: \"skip\", reason: \"missing-runtime\" }\n const payload = parseObject(input.stdin)\n if (!payload) return { kind: \"skip\", reason: \"bad-payload\" }\n if (isSubagentContext(payload)) return { kind: \"skip\", reason: \"subagent\" }\n\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (!sessionId) return { kind: \"skip\", reason: \"missing-session\" }\n const cwd = typeof payload.cwd === \"string\" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd\n const plan = await resolvePlanText(\n payload.tool_input,\n input.readFile ?? ((filePath: string) => fs.readFile(filePath, \"utf8\")),\n )\n const trimmed = plan.trim()\n if (trimmed.length < (input.minChars ?? PLAN_REVIEW_MIN_CHARS)) return { kind: \"skip\", reason: \"trivial-plan\" }\n\n const planHash = createHash(\"sha256\").update(trimmed).digest(\"hex\")\n if (!(await input.debounce.shouldReview(sessionId, planHash))) return { kind: \"skip\", reason: \"debounced\" }\n await input.debounce.markReviewed(sessionId, planHash)\n return { kind: \"spawn\", sessionId, cwd, plan: trimmed, planHash }\n } catch {\n return { kind: \"skip\", reason: \"error\" }\n }\n}\n\nfunction reviewBrief(plan: string, cwd: string): string {\n const embeddedPlan =\n plan.length > MAX_REVIEWED_PLAN_CHARS\n ? `${plan.slice(0, MAX_REVIEWED_PLAN_CHARS)}\\n\\n[plan truncated at ${MAX_REVIEWED_PLAN_CHARS} characters]`\n : plan\n return (\n \"You are an independent plan reviewer. A coding agent has just finalized a plan with ExitPlanMode. \"\n + \"Your job is to find load-bearing risks before implementation starts: unstated assumptions, missing \"\n + \"failure modes, gaps in verification, dangerous sequencing, or places where the plan could satisfy the \"\n + \"letter of the request while missing the user's likely intent.\\n\\n\"\n + \"Be concise and skeptical. Do NOT rewrite the plan, do NOT praise it, and do NOT invent new product \"\n + \"requirements. Report only material objections that would change what the agent should do next. Include \"\n + \"file:line anchors when the plan names concrete files or when the risk depends on repo code. If the plan \"\n + \"is sound, say exactly: \\\"no material objection\\\".\\n\\n\"\n + `Workspace: ${cwd}\\n\\n`\n + \"FINALIZED PLAN:\\n\"\n + embeddedPlan\n )\n}\n\nfunction hasMaterialFinding(text: string): boolean {\n const normalized = text.trim().toLowerCase().replace(/[.!]+$/g, \"\")\n return normalized.length > 0 && normalized !== \"no material objection\" && normalized !== \"no material objections\"\n}\n\nfunction framePlanFindings(text: string): string {\n return (\n \"PLAN REVIEW: an independent cross-lab critic reviewed the finalized plan and found \"\n + \"the following material concern(s).\\n\"\n + text.trim()\n )\n}\n\n/** Run the bounded critic call and write material findings to the shared findings store. */\nexport async function runPlanReview(input: {\n runtime: HookMcpRuntime\n sessionId: string\n cwd: string\n plan: string\n findingsStore: FindingsStore\n timeoutMs?: number\n callReview?: (brief: string, signal: AbortSignal) => Promise<McpToolResult>\n}): Promise<void> {\n const timeoutMs = input.timeoutMs ?? PLAN_REVIEW_TIMEOUT_MS\n let timer: ReturnType<typeof setTimeout> | undefined\n const controller = new AbortController()\n try {\n const brief = reviewBrief(input.plan, input.cwd)\n const review = (input.callReview ?? ((prompt: string, signal: AbortSignal) => callMcpTool({\n runtime: input.runtime,\n group: \"peers\",\n tool: \"codex_critic\",\n args: { prompt, effort: \"high\" },\n timeoutMs,\n signal,\n })))(brief, controller.signal)\n review.catch(() => {})\n const raced = await Promise.race<McpToolResult | \"timeout\">([\n review,\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeoutMs)\n }),\n ])\n if (raced === \"timeout\" || raced.isError || !hasMaterialFinding(raced.text)) return\n\n const framed = framePlanFindings(raced.text)\n const existing = await input.findingsStore.read(input.sessionId).catch(() => null)\n const next = existing && existing.trim().length > 0 ? `${existing.trim()}\\n\\n${framed}` : framed\n await input.findingsStore.write(input.sessionId, next)\n } catch {\n /* advisory layer must never surface an error */\n } finally {\n if (timer) clearTimeout(timer)\n controller.abort()\n }\n}\n\n/** Build the command registered for PostToolUse(ExitPlanMode). */\nexport function buildPlanReviewHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-plan-review\")\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as buildSelfCommand } from "./self-invocation-
|
|
2
|
-
import { a as isSubagentContext } from "./stop-gate-policy-
|
|
1
|
+
import { t as buildSelfCommand } from "./self-invocation-DAB_od0C.js";
|
|
2
|
+
import { a as isSubagentContext } from "./stop-gate-policy-DMr3KVmw.js";
|
|
3
3
|
//#region src/lib/orchestration/prompt-submit-hook.ts
|
|
4
4
|
/**
|
|
5
5
|
* Pure decision logic for the `UserPromptSubmit` hook (the front-end of the
|
|
@@ -145,4 +145,4 @@ function buildPromptSubmitHookCommand(invocation) {
|
|
|
145
145
|
//#endregion
|
|
146
146
|
export { decidePromptSubmit as n, decidePromptSubmitV2 as r, buildPromptSubmitHookCommand as t };
|
|
147
147
|
|
|
148
|
-
//# sourceMappingURL=prompt-submit-hook-
|
|
148
|
+
//# sourceMappingURL=prompt-submit-hook-IMl7ssfr.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-submit-hook-Bqf9ORgb.js","names":[],"sources":["../src/lib/orchestration/prompt-submit-hook.ts"],"sourcesContent":["/**\n * Pure decision logic for the `UserPromptSubmit` hook (the front-end of the\n * floor-raising surface). It serves TWO duties on the one event:\n *\n * 1. Per-prompt budget reset — clears the Stop-gate's block counter for this\n * session so `maxBlocks` is \"per user prompt\", not \"per session lifetime\"\n * (the per-prompt scoping the Stop-gate relies on).\n * 2. Goal steer — for a NON-trivial prompt, inject an advisory GOAL directive\n * (additive context, never blocking) pointing the model at `/gh-research`\n * then `/gh-orchestrate`. It raises the prior; it does not force execution.\n *\n * Top-level only: like the Stop-gate, it stands down in a subagent/teammate\n * context (the payload carries `agent_type`/`agent_id` there) so it never\n * recurses into spawned workers. Fail-open and never blocking: any error or a\n * trivial prompt yields an empty injection.\n */\n\nimport { buildSelfCommand, type SelfInvocation } from \"../hook-launcher/self-invocation\"\nimport { isSubagentContext } from \"./stop-gate-policy\"\n\n/**\n * The advisory goal injected for a non-trivial prompt. Uses the skills' slash\n * invocation form. The model still decides whether to follow it; the Stop-gate\n * backstops correctness at the output end.\n */\nexport const PROMPT_STEER_GOAL =\n \"GOAL (advisory): for a non-trivial task, FIRST run /gh-research on this ask to \"\n + \"information saturation — verify the load-bearing claims against the actual code \"\n + \"before planning, and do not plan or write code until research is saturated. THEN, \"\n + \"for an implementation or change task, run /gh-orchestrate to compose and run a \"\n + \"floor-raising workflow (it checkpoints before expensive work). Skip both for a \"\n + \"trivial ask; you may decline if they do not fit.\"\n\n/**\n * Cheap, conservative complexity heuristic — a long prompt, an imperative\n * build/change verb, or an explicit multi-file scope. Trivial prompts get no\n * steer (no analysis-paralysis tax on quick asks).\n */\nexport function isNonTrivialPrompt(prompt: string): boolean {\n const p = prompt.trim()\n if (p.length === 0) return false\n if (p.length >= 280) return true\n const verb =\n /\\b(implement|build|refactor|migrate|fix|debug|diagnose|design|add|create|rewrite|optimi[sz]e|integrate|architect|investigate|audit)\\b/i\n if (verb.test(p)) return true\n // Multi-file / cross-cutting scope signal.\n return /\\b(across|throughout|every|all)\\b.*\\b(file|module|test|route|component)s?\\b/i.test(p)\n}\n\nexport interface PromptSubmitDecision {\n /** Session whose Stop-gate block budget to reset (undefined = none). */\n resetSession?: string\n /** Text to print to stdout (added to the model's context). Empty = nothing. */\n inject: string\n}\n\nexport function decidePromptSubmit(input: { stdin: string; steerEnabled: boolean }): PromptSubmitDecision {\n let payload: { session_id?: unknown; prompt?: unknown; agent_type?: unknown; agent_id?: unknown } = {}\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") payload = p as typeof payload\n } catch {\n return { inject: \"\" }\n }\n // Top-level only: a subagent/teammate prompt gets neither reset nor steer.\n if (isSubagentContext(payload)) return { inject: \"\" }\n const decision: PromptSubmitDecision = { inject: \"\" }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (sessionId) decision.resetSession = sessionId\n const prompt = typeof payload.prompt === \"string\" ? payload.prompt : \"\"\n if (input.steerEnabled && isNonTrivialPrompt(prompt)) decision.inject = PROMPT_STEER_GOAL\n return decision\n}\n\n// ─── UserPromptSubmit V2 (advisory: classify + grounded scope/goal) ──────────\n\n/**\n * Static encouragement injected only for a non-trivial prompt (no extra model\n * call, no latency tax): nudge parallel lexical+semantic search before\n * concluding. Mirrors the v1 advisory tone: additive, never blocking.\n */\nexport const PROMPT_SEARCH_TIP =\n \"TIP (advisory): when this task needs code context, search lexical + semantic in \"\n + \"parallel — one `mcp__search__code` call with mode:\\\"lexical\\\" and one with \"\n + \"mode:\\\"semantic\\\", issued in the same turn — before concluding.\"\n\n/** System prompt for the single gpt-5.6-sol scope/goal inference. Steers a SHORT,\n * user-derived (not invented) advisory note grounded in the search results. */\nexport const PROMPT_SCOPE_SYSTEM =\n \"You are a scoping assistant for a coding agent about to act on a user's request. \"\n + \"You are given the user's request and the results of a lexical + semantic code search \"\n + \"over the relevant repository. Produce a SHORT advisory note (<= 120 words), plain text only:\\n\"\n + \"1. SCOPE: one line — is this trivial, focused (one area), or large/cross-cutting — grounded in \"\n + \"what the search surfaced (reference the most relevant file(s) by name).\\n\"\n + \"2. GOAL: restate the user's OWN ask as a single measurable objective, in THEIR terms. Do NOT \"\n + \"invent new requirements or acceptance criteria beyond what they asked.\\n\"\n + \"3. Only if the task is large/cross-cutting, add a final line: \\\"Consider /gh-research first to \"\n + \"saturate understanding, then /gh-orchestrate to compose a floor-raising workflow.\\\" Omit it for a \"\n + \"focused or trivial task.\\n\"\n + \"This is advisory — the agent decides whether to follow it. Be concrete and concise; no preamble.\"\n\n/** Injected IO for V2, all best-effort. Each network call returns its text and\n * never throws to the orchestrator (the orchestrator wraps the enrichment in a\n * timeout + try/catch and falls open to the regex goal).\n *\n * INVARIANT for live callers: each IO call MUST self-timeout (the live wiring\n * passes a per-call timeoutMs to callMcpTool / callInference). The orchestrator's\n * outer `timeoutMs` race only bounds how long it WAITS for a result before\n * failing open — it does NOT abort the in-flight call. A non-self-timing-out IO\n * would leave a socket open until the hook process's terminal `process.exit(0)`,\n * which is the hard backstop but not a substitute for the per-call bound. */\nexport interface PromptSubmitV2IO {\n /** One `mcp__search__code` call; returns the raw result text (\"\" on failure).\n * Receives the orchestrator's AbortSignal so a timed-out enrichment cancels\n * the in-flight request (live callers thread it into the HTTP fetch). */\n searchCode: (query: string, mode: \"lexical\" | \"semantic\", signal?: AbortSignal) => Promise<string>\n /** One gpt-5.6-sol `/v1/responses` inference; returns assistant text (\"\" on failure).\n * Receives the orchestrator's AbortSignal (see `searchCode`). */\n infer: (system: string, user: string, signal?: AbortSignal) => Promise<string>\n /** Pending advisory findings from the prior turn's background review. */\n readFindings: (sessionId: string) => Promise<string | null>\n clearFindings: (sessionId: string) => Promise<void>\n /** Stash this prompt so the next Stop review can judge the diff against it. */\n storePrompt: (sessionId: string, prompt: string) => Promise<void>\n /** Wall-clock budget for the substantive-prompt enrichment (search+infer);\n * on overrun the decision falls open to the regex goal. Default 22s. */\n timeoutMs?: number\n}\n\n/** Max chars of each search-result blob fed into the scope inference. */\nconst SEARCH_CONTEXT_CAP = 6 * 1024\n\n/** Wrap the prior-turn review findings in an explicitly NON-AUTHORITATIVE frame. */\nfunction framePendingFindings(findings: string): string {\n return (\n \"ADVISORY — independent review of your PREVIOUS change (NON-AUTHORITATIVE): an independent \"\n + \"gpt-5.6-sol reviewer flagged the following. Evaluate each on its merits — fix the real ones, and \"\n + \"ignore any wrong one with a one-line reason. You are NOT obligated to act on these.\\n\"\n + findings.trim()\n )\n}\n\nfunction joinSections(sections: Array<string>): string {\n return sections\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n .join(\"\\n\\n\")\n}\n\n/**\n * V2 decision: budget reset (via resetSession) + a grounded, user-derived scope\n * note + surfaced prior-turn findings. ASYNC and IO-driven, but every IO is\n * best-effort and the substantive enrichment is timeout-bounded with a fail-open\n * to the v1 regex goal — so this never blocks and never wedges the prompt.\n *\n * - subagent/teammate -> empty (top-level only, like v1).\n * - findings -> always surfaced (+ cleared) regardless of triviality.\n * - trivial prompt -> findings only (no search tip, no model call).\n * - substantive prompt -> static search tip + parallel lexical+semantic search -> ONE gpt-5.6-sol call\n * -> grounded scope/goal note. Fail-open to PROMPT_STEER_GOAL.\n * - steerEnabled=false -> findings only (no goal/tip).\n */\nexport async function decidePromptSubmitV2(input: {\n stdin: string\n steerEnabled: boolean\n io: PromptSubmitV2IO\n}): Promise<PromptSubmitDecision> {\n let payload: { session_id?: unknown; prompt?: unknown; agent_type?: unknown; agent_id?: unknown } = {}\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") payload = p as typeof payload\n } catch {\n return { inject: \"\" }\n }\n if (isSubagentContext(payload)) return { inject: \"\" }\n\n const decision: PromptSubmitDecision = { inject: \"\" }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (sessionId) decision.resetSession = sessionId\n const prompt = typeof payload.prompt === \"string\" ? payload.prompt : \"\"\n\n // Stash the prompt for the next Stop review (best-effort, fire-and-await so a\n // slow disk doesn't matter — it's a tiny local write).\n if (sessionId) await input.io.storePrompt(sessionId, prompt).catch(() => {})\n\n // Read + clear any pending findings from the prior turn's background review.\n let findingsBlock = \"\"\n if (sessionId) {\n const pending = await input.io.readFindings(sessionId).catch(() => null)\n if (pending && pending.trim().length > 0) {\n findingsBlock = framePendingFindings(pending)\n await input.io.clearFindings(sessionId).catch(() => {})\n }\n }\n\n // steer disabled -> findings only.\n if (!input.steerEnabled) {\n decision.inject = findingsBlock\n return decision\n }\n\n // Trivial prompt -> no search tip and no model call; surface findings only.\n if (!isNonTrivialPrompt(prompt)) {\n decision.inject = findingsBlock\n return decision\n }\n\n // Substantive prompt -> static search tip + grounded enrichment, timeout-bounded + fail-open.\n const timeoutMs = input.io.timeoutMs ?? 22_000\n let goal = PROMPT_STEER_GOAL // fail-open default.\n let timer: ReturnType<typeof setTimeout> | undefined\n // One controller cancels BOTH search calls + the inference when the race ends\n // (timeout OR success), so a lost-race fetch never keeps the short-lived hook\n // process alive past its decision. Aborting after success is a harmless no-op.\n const controller = new AbortController()\n try {\n const enrich = (async (): Promise<string> => {\n const [lexical, semantic] = await Promise.all([\n input.io.searchCode(prompt, \"lexical\", controller.signal).catch(() => \"\"),\n input.io.searchCode(prompt, \"semantic\", controller.signal).catch(() => \"\"),\n ])\n const searchContext =\n `Lexical search results:\\n${lexical.slice(0, SEARCH_CONTEXT_CAP)}\\n\\n`\n + `Semantic search results:\\n${semantic.slice(0, SEARCH_CONTEXT_CAP)}`\n const note = await input.io.infer(\n PROMPT_SCOPE_SYSTEM,\n `USER REQUEST:\\n${prompt}\\n\\n${searchContext}`,\n controller.signal,\n )\n return note.trim()\n })()\n // Mark `enrich` handled so a post-race rejection (e.g. the inference aborting\n // after the timeout already won) can't surface as an unhandled rejection.\n enrich.catch(() => {})\n // The timer is cleared in `finally` (so when `enrich` wins, no live timer\n // lingers for the rest of `timeoutMs`).\n const raced = await Promise.race<string | \"__timeout__\">([\n enrich,\n new Promise<\"__timeout__\">((resolve) => {\n timer = setTimeout(() => resolve(\"__timeout__\"), timeoutMs)\n }),\n ])\n if (raced !== \"__timeout__\" && raced.length > 0) goal = raced\n } catch {\n // keep the fail-open regex goal.\n } finally {\n if (timer) clearTimeout(timer)\n controller.abort()\n }\n\n decision.inject = joinSections([PROMPT_SEARCH_TIP, goal, findingsBlock])\n return decision\n}\n\n/** Build the shell command Claude Code runs for the `UserPromptSubmit` hook. */\nexport function buildPromptSubmitHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-prompt-submit\")\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prompt-submit-hook-IMl7ssfr.js","names":[],"sources":["../src/lib/orchestration/prompt-submit-hook.ts"],"sourcesContent":["/**\n * Pure decision logic for the `UserPromptSubmit` hook (the front-end of the\n * floor-raising surface). It serves TWO duties on the one event:\n *\n * 1. Per-prompt budget reset — clears the Stop-gate's block counter for this\n * session so `maxBlocks` is \"per user prompt\", not \"per session lifetime\"\n * (the per-prompt scoping the Stop-gate relies on).\n * 2. Goal steer — for a NON-trivial prompt, inject an advisory GOAL directive\n * (additive context, never blocking) pointing the model at `/gh-research`\n * then `/gh-orchestrate`. It raises the prior; it does not force execution.\n *\n * Top-level only: like the Stop-gate, it stands down in a subagent/teammate\n * context (the payload carries `agent_type`/`agent_id` there) so it never\n * recurses into spawned workers. Fail-open and never blocking: any error or a\n * trivial prompt yields an empty injection.\n */\n\nimport { buildSelfCommand, type SelfInvocation } from \"../hook-launcher/self-invocation\"\nimport { isSubagentContext } from \"./stop-gate-policy\"\n\n/**\n * The advisory goal injected for a non-trivial prompt. Uses the skills' slash\n * invocation form. The model still decides whether to follow it; the Stop-gate\n * backstops correctness at the output end.\n */\nexport const PROMPT_STEER_GOAL =\n \"GOAL (advisory): for a non-trivial task, FIRST run /gh-research on this ask to \"\n + \"information saturation — verify the load-bearing claims against the actual code \"\n + \"before planning, and do not plan or write code until research is saturated. THEN, \"\n + \"for an implementation or change task, run /gh-orchestrate to compose and run a \"\n + \"floor-raising workflow (it checkpoints before expensive work). Skip both for a \"\n + \"trivial ask; you may decline if they do not fit.\"\n\n/**\n * Cheap, conservative complexity heuristic — a long prompt, an imperative\n * build/change verb, or an explicit multi-file scope. Trivial prompts get no\n * steer (no analysis-paralysis tax on quick asks).\n */\nexport function isNonTrivialPrompt(prompt: string): boolean {\n const p = prompt.trim()\n if (p.length === 0) return false\n if (p.length >= 280) return true\n const verb =\n /\\b(implement|build|refactor|migrate|fix|debug|diagnose|design|add|create|rewrite|optimi[sz]e|integrate|architect|investigate|audit)\\b/i\n if (verb.test(p)) return true\n // Multi-file / cross-cutting scope signal.\n return /\\b(across|throughout|every|all)\\b.*\\b(file|module|test|route|component)s?\\b/i.test(p)\n}\n\nexport interface PromptSubmitDecision {\n /** Session whose Stop-gate block budget to reset (undefined = none). */\n resetSession?: string\n /** Text to print to stdout (added to the model's context). Empty = nothing. */\n inject: string\n}\n\nexport function decidePromptSubmit(input: { stdin: string; steerEnabled: boolean }): PromptSubmitDecision {\n let payload: { session_id?: unknown; prompt?: unknown; agent_type?: unknown; agent_id?: unknown } = {}\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") payload = p as typeof payload\n } catch {\n return { inject: \"\" }\n }\n // Top-level only: a subagent/teammate prompt gets neither reset nor steer.\n if (isSubagentContext(payload)) return { inject: \"\" }\n const decision: PromptSubmitDecision = { inject: \"\" }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (sessionId) decision.resetSession = sessionId\n const prompt = typeof payload.prompt === \"string\" ? payload.prompt : \"\"\n if (input.steerEnabled && isNonTrivialPrompt(prompt)) decision.inject = PROMPT_STEER_GOAL\n return decision\n}\n\n// ─── UserPromptSubmit V2 (advisory: classify + grounded scope/goal) ──────────\n\n/**\n * Static encouragement injected only for a non-trivial prompt (no extra model\n * call, no latency tax): nudge parallel lexical+semantic search before\n * concluding. Mirrors the v1 advisory tone: additive, never blocking.\n */\nexport const PROMPT_SEARCH_TIP =\n \"TIP (advisory): when this task needs code context, search lexical + semantic in \"\n + \"parallel — one `mcp__search__code` call with mode:\\\"lexical\\\" and one with \"\n + \"mode:\\\"semantic\\\", issued in the same turn — before concluding.\"\n\n/** System prompt for the single gpt-5.6-sol scope/goal inference. Steers a SHORT,\n * user-derived (not invented) advisory note grounded in the search results. */\nexport const PROMPT_SCOPE_SYSTEM =\n \"You are a scoping assistant for a coding agent about to act on a user's request. \"\n + \"You are given the user's request and the results of a lexical + semantic code search \"\n + \"over the relevant repository. Produce a SHORT advisory note (<= 120 words), plain text only:\\n\"\n + \"1. SCOPE: one line — is this trivial, focused (one area), or large/cross-cutting — grounded in \"\n + \"what the search surfaced (reference the most relevant file(s) by name).\\n\"\n + \"2. GOAL: restate the user's OWN ask as a single measurable objective, in THEIR terms. Do NOT \"\n + \"invent new requirements or acceptance criteria beyond what they asked.\\n\"\n + \"3. Only if the task is large/cross-cutting, add a final line: \\\"Consider /gh-research first to \"\n + \"saturate understanding, then /gh-orchestrate to compose a floor-raising workflow.\\\" Omit it for a \"\n + \"focused or trivial task.\\n\"\n + \"This is advisory — the agent decides whether to follow it. Be concrete and concise; no preamble.\"\n\n/** Injected IO for V2, all best-effort. Each network call returns its text and\n * never throws to the orchestrator (the orchestrator wraps the enrichment in a\n * timeout + try/catch and falls open to the regex goal).\n *\n * INVARIANT for live callers: each IO call MUST self-timeout (the live wiring\n * passes a per-call timeoutMs to callMcpTool / callInference). The orchestrator's\n * outer `timeoutMs` race only bounds how long it WAITS for a result before\n * failing open — it does NOT abort the in-flight call. A non-self-timing-out IO\n * would leave a socket open until the hook process's terminal `process.exit(0)`,\n * which is the hard backstop but not a substitute for the per-call bound. */\nexport interface PromptSubmitV2IO {\n /** One `mcp__search__code` call; returns the raw result text (\"\" on failure).\n * Receives the orchestrator's AbortSignal so a timed-out enrichment cancels\n * the in-flight request (live callers thread it into the HTTP fetch). */\n searchCode: (query: string, mode: \"lexical\" | \"semantic\", signal?: AbortSignal) => Promise<string>\n /** One gpt-5.6-sol `/v1/responses` inference; returns assistant text (\"\" on failure).\n * Receives the orchestrator's AbortSignal (see `searchCode`). */\n infer: (system: string, user: string, signal?: AbortSignal) => Promise<string>\n /** Pending advisory findings from the prior turn's background review. */\n readFindings: (sessionId: string) => Promise<string | null>\n clearFindings: (sessionId: string) => Promise<void>\n /** Stash this prompt so the next Stop review can judge the diff against it. */\n storePrompt: (sessionId: string, prompt: string) => Promise<void>\n /** Wall-clock budget for the substantive-prompt enrichment (search+infer);\n * on overrun the decision falls open to the regex goal. Default 22s. */\n timeoutMs?: number\n}\n\n/** Max chars of each search-result blob fed into the scope inference. */\nconst SEARCH_CONTEXT_CAP = 6 * 1024\n\n/** Wrap the prior-turn review findings in an explicitly NON-AUTHORITATIVE frame. */\nfunction framePendingFindings(findings: string): string {\n return (\n \"ADVISORY — independent review of your PREVIOUS change (NON-AUTHORITATIVE): an independent \"\n + \"gpt-5.6-sol reviewer flagged the following. Evaluate each on its merits — fix the real ones, and \"\n + \"ignore any wrong one with a one-line reason. You are NOT obligated to act on these.\\n\"\n + findings.trim()\n )\n}\n\nfunction joinSections(sections: Array<string>): string {\n return sections\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n .join(\"\\n\\n\")\n}\n\n/**\n * V2 decision: budget reset (via resetSession) + a grounded, user-derived scope\n * note + surfaced prior-turn findings. ASYNC and IO-driven, but every IO is\n * best-effort and the substantive enrichment is timeout-bounded with a fail-open\n * to the v1 regex goal — so this never blocks and never wedges the prompt.\n *\n * - subagent/teammate -> empty (top-level only, like v1).\n * - findings -> always surfaced (+ cleared) regardless of triviality.\n * - trivial prompt -> findings only (no search tip, no model call).\n * - substantive prompt -> static search tip + parallel lexical+semantic search -> ONE gpt-5.6-sol call\n * -> grounded scope/goal note. Fail-open to PROMPT_STEER_GOAL.\n * - steerEnabled=false -> findings only (no goal/tip).\n */\nexport async function decidePromptSubmitV2(input: {\n stdin: string\n steerEnabled: boolean\n io: PromptSubmitV2IO\n}): Promise<PromptSubmitDecision> {\n let payload: { session_id?: unknown; prompt?: unknown; agent_type?: unknown; agent_id?: unknown } = {}\n try {\n const p: unknown = JSON.parse(input.stdin)\n if (p && typeof p === \"object\") payload = p as typeof payload\n } catch {\n return { inject: \"\" }\n }\n if (isSubagentContext(payload)) return { inject: \"\" }\n\n const decision: PromptSubmitDecision = { inject: \"\" }\n const sessionId = typeof payload.session_id === \"string\" && payload.session_id.length > 0 ? payload.session_id : \"\"\n if (sessionId) decision.resetSession = sessionId\n const prompt = typeof payload.prompt === \"string\" ? payload.prompt : \"\"\n\n // Stash the prompt for the next Stop review (best-effort, fire-and-await so a\n // slow disk doesn't matter — it's a tiny local write).\n if (sessionId) await input.io.storePrompt(sessionId, prompt).catch(() => {})\n\n // Read + clear any pending findings from the prior turn's background review.\n let findingsBlock = \"\"\n if (sessionId) {\n const pending = await input.io.readFindings(sessionId).catch(() => null)\n if (pending && pending.trim().length > 0) {\n findingsBlock = framePendingFindings(pending)\n await input.io.clearFindings(sessionId).catch(() => {})\n }\n }\n\n // steer disabled -> findings only.\n if (!input.steerEnabled) {\n decision.inject = findingsBlock\n return decision\n }\n\n // Trivial prompt -> no search tip and no model call; surface findings only.\n if (!isNonTrivialPrompt(prompt)) {\n decision.inject = findingsBlock\n return decision\n }\n\n // Substantive prompt -> static search tip + grounded enrichment, timeout-bounded + fail-open.\n const timeoutMs = input.io.timeoutMs ?? 22_000\n let goal = PROMPT_STEER_GOAL // fail-open default.\n let timer: ReturnType<typeof setTimeout> | undefined\n // One controller cancels BOTH search calls + the inference when the race ends\n // (timeout OR success), so a lost-race fetch never keeps the short-lived hook\n // process alive past its decision. Aborting after success is a harmless no-op.\n const controller = new AbortController()\n try {\n const enrich = (async (): Promise<string> => {\n const [lexical, semantic] = await Promise.all([\n input.io.searchCode(prompt, \"lexical\", controller.signal).catch(() => \"\"),\n input.io.searchCode(prompt, \"semantic\", controller.signal).catch(() => \"\"),\n ])\n const searchContext =\n `Lexical search results:\\n${lexical.slice(0, SEARCH_CONTEXT_CAP)}\\n\\n`\n + `Semantic search results:\\n${semantic.slice(0, SEARCH_CONTEXT_CAP)}`\n const note = await input.io.infer(\n PROMPT_SCOPE_SYSTEM,\n `USER REQUEST:\\n${prompt}\\n\\n${searchContext}`,\n controller.signal,\n )\n return note.trim()\n })()\n // Mark `enrich` handled so a post-race rejection (e.g. the inference aborting\n // after the timeout already won) can't surface as an unhandled rejection.\n enrich.catch(() => {})\n // The timer is cleared in `finally` (so when `enrich` wins, no live timer\n // lingers for the rest of `timeoutMs`).\n const raced = await Promise.race<string | \"__timeout__\">([\n enrich,\n new Promise<\"__timeout__\">((resolve) => {\n timer = setTimeout(() => resolve(\"__timeout__\"), timeoutMs)\n }),\n ])\n if (raced !== \"__timeout__\" && raced.length > 0) goal = raced\n } catch {\n // keep the fail-open regex goal.\n } finally {\n if (timer) clearTimeout(timer)\n controller.abort()\n }\n\n decision.inject = joinSections([PROMPT_SEARCH_TIP, goal, findingsBlock])\n return decision\n}\n\n/** Build the shell command Claude Code runs for the `UserPromptSubmit` hook. */\nexport function buildPromptSubmitHookCommand(invocation: SelfInvocation): string {\n return buildSelfCommand(invocation, \"internal-prompt-submit\")\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { A as assetFor, D as toolbeltSkipSet, E as toolbeltEnabled, O as vscodeRipgrepPath,
|
|
2
|
-
import { t as PATHS } from "./paths-
|
|
1
|
+
import { A as assetFor, D as toolbeltSkipSet, E as toolbeltEnabled, O as vscodeRipgrepPath, Vn as withInstallLock, fn as extractTarGzMember, j as satisfiesMinVersion, k as TOOLBELT_TOOLS, pn as extractZipMember, yn as collapsePathKeys } from "./peer-mcp-personas-BwNtC8jt.js";
|
|
2
|
+
import { t as PATHS } from "./paths-Ci485qSJ.js";
|
|
3
3
|
import { a as resolveExecutable, o as runCommandCapture, r as parseBoolEnv, t as killChildProcessTree } from "./exec-y8C_MU8A.js";
|
|
4
|
-
import { a as sweepRegistry } from "./lifecycle-
|
|
4
|
+
import { a as sweepRegistry } from "./lifecycle-Bg6doY3-.js";
|
|
5
5
|
import consola from "consola";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
@@ -678,4 +678,4 @@ async function exposedCommands(binDir) {
|
|
|
678
678
|
//#endregion
|
|
679
679
|
export { STRIPPED_AUTH_ROUTING_ENV_KEYS as a, launchChild as i, buildCodexProviderConfigFlags as n, getCodexVersion as r, provisionToolbelt as t };
|
|
680
680
|
|
|
681
|
-
//# sourceMappingURL=provision-
|
|
681
|
+
//# sourceMappingURL=provision-DeNqzvSM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provision-CZJ4EWls.js","names":[],"sources":["../src/lib/process-guard/index.ts","../src/lib/stripped-env-keys.ts","../src/lib/launch.ts","../src/lib/toolbelt/provision.ts"],"sourcesContent":["/**\n * Crash-safe process guard (POSIX only).\n *\n * If the proxy dies WITHOUT running its own shutdown handlers (hard crash,\n * SIGKILL, OOM), nothing in `cleanup()` runs to reap the launched CLI's\n * process tree. This guard is the last-resort net for that case.\n *\n * Platform split:\n * - **Windows: no guard needed.** Node assigns each child to a Job\n * Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so when the proxy\n * dies (any cause) the OS tears down the whole descendant tree —\n * verified: force-killing a Node parent kills child AND grandchild.\n * `startProcessGuard` is a no-op on win32.\n * - **POSIX: a detached `node -e` reaper.** There is no job-object\n * equivalent (a killed parent's children reparent to init), so we\n * spawn a detached watchdog that learns of proxy death via an inherited\n * stdin PIPE EOF (the proxy holds the write end; the OS closes it on\n * proxy death) and reaps the CLI's process group.\n *\n * Never kill the WRONG process: if the child's PID/PGID was recycled after\n * the child exited, a blind `kill(-pid)` could hit an innocent group. The\n * reaper snapshots the child's process start-time at startup and, on proxy\n * death, kills ONLY if the live PID's start-time still matches. A mismatch\n * / unreadable probe → skip (fail-safe). We never trade an orphan-leak for\n * killing the user's work.\n *\n * The reaper is detached + unref'd so it survives the proxy's SIGKILL, and\n * self-exits once it has acted (or after a 24h cap) so it can never become\n * the orphan it exists to prevent. Opt out with\n * `GH_ROUTER_DISABLE_PROCESS_GUARD=1`.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\"\nimport process from \"node:process\"\n\nimport { parseBoolEnv } from \"../exec\"\n\n/**\n * Live reaper children, held so the GC can't collect the `ChildProcess`\n * (and with it the stdin write-end that is the proxy-death signal) while\n * the proxy runs. Entries self-remove on the reaper's exit. We do NOT\n * proactively kill these on graceful shutdown: when the proxy exits the OS\n * closes the stdin pipe, the reaper hits EOF, re-verifies the child's\n * identity, and reaps any survivor that ignored the graceful SIGTERM —\n * exactly the backstop we want.\n */\nconst _activeReapers = new Set<ChildProcess>()\n\n/** Guard is on by default; `GH_ROUTER_DISABLE_PROCESS_GUARD=1` opts out. */\nexport function processGuardEnabled(): boolean {\n return parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROCESS_GUARD) !== true\n}\n\n/**\n * Build the start-time-verified node reaper script (POSIX). PURE — `pid`\n * is our own integer. `detachedGroup` selects `kill(-pid)` (process group,\n * the detached CLI) vs `kill(pid)` on POSIX.\n *\n * Kept dependency-free (require'd builtins only) so it runs under `node -e`\n * with no module resolution against the dist bundle.\n */\nexport function buildNodeReaperScript(\n pid: number,\n detachedGroup: boolean,\n): string {\n const target = detachedGroup ? \"-PID\" : \"PID\"\n return `\nconst cp = require(\"node:child_process\");\nconst fs = require(\"node:fs\");\nconst PID = ${pid >>> 0};\nfunction startTime() {\n try {\n if (process.platform === \"linux\") {\n const stat = fs.readFileSync(\"/proc/\" + PID + \"/stat\", \"utf8\");\n const post = stat.slice(stat.lastIndexOf(\")\") + 1).trim().split(/\\\\s+/);\n return post[19] || null; // field 22 (starttime) = index 19 after comm\n }\n const out = cp.execFileSync(\"ps\", [\"-o\",\"lstart=\",\"-p\",String(PID)],\n { stdio: [\"ignore\",\"pipe\",\"ignore\"], timeout: 2000 }).toString().trim();\n return out || null;\n } catch { return null; }\n}\nfunction alive() { try { process.kill(PID, 0); return true; } catch { return false; } }\nfunction treeKill() {\n try { process.kill(${target}, \"SIGTERM\"); } catch {}\n // NOT unref'd: this must keep the loop alive to deliver the escalated\n // SIGKILL (the onDeath exit timer below stays alive past 500ms too).\n setTimeout(() => { try { process.kill(${target}, \"SIGKILL\"); } catch {} }, 500);\n}\nconst snap = (() => {\n // The child is definitely ours and alive at startup, so a null here is a\n // transient probe failure (e.g. a momentary 'ps' hiccup), NOT a real\n // identity loss. Retry a few times so a one-off failure can't silently\n // disable the guard for the rest of the run.\n for (let i = 0; i < 3; i++) { const s = startTime(); if (s !== null) return s; }\n return null;\n})();\nlet done = false;\nfunction onDeath() {\n if (done) return; done = true;\n // Re-verify identity: kill ONLY if the live PID is still our child.\n if (snap !== null && alive() && startTime() === snap) treeKill();\n // REF'd (NOT unref'd): stdin has closed, so an unref'd timer would let\n // the loop empty and the process exit immediately — dropping the 500ms\n // SIGKILL escalation. Keep the loop alive to deliver it, then exit.\n setTimeout(() => process.exit(0), 1500);\n}\nprocess.stdin.resume();\nprocess.stdin.on(\"end\", onDeath);\nprocess.stdin.on(\"close\", onDeath);\nprocess.stdin.on(\"error\", onDeath);\nconst cap = setTimeout(() => process.exit(0), 24*3600*1000); if (cap.unref) cap.unref();\n`.trim()\n}\n\n/**\n * Spawn the detached node reaper holding the stdin death-pipe. Always\n * `detached` so it outlives a force-kill of the proxy, and `unref`'d so it\n * never holds the proxy's event loop open. Returns null on spawn failure.\n */\nfunction spawnNodeReaper(pid: number, detachedGroup: boolean): ChildProcess | null {\n try {\n const child = spawn(\n process.execPath,\n [\"-e\", buildNodeReaperScript(pid, detachedGroup)],\n {\n stdio: [\"pipe\", \"ignore\", \"ignore\"],\n detached: true,\n windowsHide: true,\n shell: false,\n },\n )\n child.on(\"error\", () => {})\n child.stdin?.on(\"error\", () => {})\n // Hold a reference so the GC can't drop the ChildProcess (and its stdin\n // write-end, the proxy-death signal) mid-run; drop it on reaper exit.\n _activeReapers.add(child)\n child.once(\"exit\", () => _activeReapers.delete(child))\n child.unref()\n return child\n } catch {\n return null\n }\n}\n\n/**\n * Start the crash-safe guard for a launched CLI child. No-op on Windows\n * (Node's Job Object already reaps the tree on proxy death) and when\n * disabled / unspawnable. Fire-and-forget; never throws.\n */\nexport function startProcessGuard(child: ChildProcess): void {\n if (!processGuardEnabled()) return\n const pid = child.pid\n if (!pid) return\n // Windows: the runtime's KILL_ON_JOB_CLOSE job object is the crash net.\n if (process.platform === \"win32\") return\n // POSIX: the CLI is spawned detached (own group), so reap the group.\n spawnNodeReaper(pid, true)\n}\n","/**\n * Auth / routing / remote / wire-fingerprint environment keys that must never\n * reach a github-router-spawned Claude or Codex child — through ANY channel.\n *\n * Two channels carry these keys to a spawned agent, and both must be defended:\n * 1. The PARENT PROCESS ENV — a user's shell exports. Stripped by\n * `sanitizeParentEnv` (`src/lib/launch.ts`) before the proxy's overrides.\n * 2. The serve mirror `settings.json` `env` BLOCK — a user's own\n * `~/.claude/settings.json`, snapshot-mirrored and applied to the\n * SDK-spawned claude via `settingSources`. Stripped by\n * `sanitizeServeSettingsEnv` (`src/lib/mcp-permissions-settings.ts`).\n *\n * If any of these survived, a Bedrock/Vertex/gateway user's config would\n * re-route the session OFF the github-router proxy, inject real auth that wins\n * over the synthetic `.credentials.json`, or activate Claude Code's\n * unimplemented Bridge/remote code path. `CLAUDE_CODE_COORDINATOR_MODE` (a\n * toolset-stripper) is NOT here — it's serve-specific, added to the serve list\n * directly. `ANTHROPIC_SMALL_FAST_MODEL` is deliberately NOT stripped (users\n * legitimately rely on it; `resolveModel` translates unrecognized values).\n */\nexport const STRIPPED_AUTH_ROUTING_ENV_KEYS = [\n // Claude Code auth surface\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"ANTHROPIC_BASE_URL\",\n \"ANTHROPIC_CUSTOM_HEADERS\",\n \"ANTHROPIC_MODEL\",\n \"CLAUDE_CODE_OAUTH_TOKEN\",\n // Alternate OAuth source (loads from an open file descriptor); stripping\n // prevents a third auth source alongside the synthetic .credentials.json.\n \"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR\",\n \"CLAUDE_CODE_USE_BEDROCK\",\n \"CLAUDE_CODE_USE_VERTEX\",\n \"CLAUDE_CODE_USE_FOUNDRY\",\n // Defense-in-depth: a parent- or settings-set CLAUDE_CONFIG_DIR would\n // redirect the session's config dir away from the router-owned mirror.\n \"CLAUDE_CONFIG_DIR\",\n // Claude Code Bridge / IDE remote-session surface — activates a remote code\n // path (POST /v1/code/sessions, /v1/environments/bridge, …) the proxy does\n // not implement. Stripping forces a local-only session.\n \"CLAUDE_BRIDGE_OAUTH_TOKEN\",\n \"CLAUDE_BRIDGE_BASE_URL\",\n \"CLAUDE_BRIDGE_SESSION_INGRESS_URL\",\n \"SESSION_INGRESS_URL\",\n \"CLAUDE_CODE_REMOTE\",\n \"CLAUDE_CODE_CONTAINER_ID\",\n \"CLAUDE_CODE_REMOTE_SESSION_ID\",\n \"CLAUDE_CODE_SESSION_ID\",\n // Emits `x-anthropic-additional-protection` — pure wire-fingerprint noise\n // that breaks the VS Code stealth posture.\n \"CLAUDE_CODE_ADDITIONAL_PROTECTION\",\n // Codex CLI auth surface\n \"OPENAI_API_KEY\",\n \"OPENAI_BASE_URL\",\n \"CODEX_HOME\",\n // ai-or-die per-tab session-bind / artifact-review surface — must not leak\n // into a nested launch and hijack the parent tab's sidecar / bearer token.\n \"AIORDIE_CLAUDE_BIND\",\n \"AIORDIE_TOKEN\",\n \"AIORDIE_INSECURE_TLS\",\n] as const\n","import { execFileSync, spawn, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport type { Server } from \"srvx\"\n\nimport { resolveExecutable, killChildProcessTree } from \"./exec\"\nimport { DEFAULT_CODEX_MODEL } from \"./port\"\nimport { startProcessGuard } from \"./process-guard\"\nimport { STRIPPED_AUTH_ROUTING_ENV_KEYS } from \"./stripped-env-keys\"\nimport { collapsePathKeys } from \"./toolbelt/path-inject\"\nimport { sweepRegistry } from \"./worker-agent/lifecycle\"\n\n/**\n * Auth-related env keys we strip from the parent before spawning the child CLI.\n * The proxy provides its own values for everything we care about, so the child\n * behaves as if the user had no parent-env auth at all. The canonical list lives\n * in `./stripped-env-keys` (shared with serve's settings.json `env` strip).\n *\n * Why strip rather than override-with-empty-string: Claude Code emits \"Auth\n * conflict\" warnings whenever both ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY\n * are present (any value); stripping suppresses it AND prevents an inherited\n * real key from leaking. Cloud-provider toggles + OAuth tokens are simpler\n * dropped than overridden — a missing var is unambiguously absent everywhere.\n */\nconst STRIPPED_PARENT_ENV_KEYS = STRIPPED_AUTH_ROUTING_ENV_KEYS\n\n/**\n * Strip auth-related keys from a parent-process env object. The result\n * is suitable to spread into a spawned child's env BEFORE the proxy's\n * explicit overrides, so the proxy is the only source of truth for\n * auth — and stale shell exports can't leak through.\n */\nexport function sanitizeParentEnv(\n parent: NodeJS.ProcessEnv,\n): NodeJS.ProcessEnv {\n const sanitized: NodeJS.ProcessEnv = { ...parent }\n for (const key of STRIPPED_PARENT_ENV_KEYS) {\n delete sanitized[key]\n }\n return sanitized\n}\n\nfunction commandExists(name: string): boolean {\n try {\n execFileSync(process.platform === \"win32\" ? \"where.exe\" : \"which\", [name], {\n stdio: \"ignore\",\n })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Whether the launcher can execute `executable`.\n *\n * `buildLaunchCommand` resolves the CLI to an ABSOLUTE path (anti-shadow).\n * `where.exe` (and POSIX `which`) reject a full path argument — `where`\n * returns \"Could not find files for the given pattern(s)\" for an absolute\n * path even when the file exists — so the where/which probe is only valid\n * for bare command names. For an absolute path (already resolved against\n * PATH and existence-checked by `resolveExecutable`), check the\n * filesystem directly. Without this split, every launch where the CLI is\n * installed fails with a spurious \"not found on PATH\".\n */\nexport function isExecutableAvailable(executable: string): boolean {\n if (path.isAbsolute(executable)) return existsSync(executable)\n return commandExists(executable)\n}\n\n/**\n * Provider-config flags (`-c model_providers.github_router=...`) that\n * point Codex at our proxy. Extracted from `buildCodexCmd` so the new\n * `codex mcp-server` MCP-config builder can reuse the exact same\n * provider definition — drift between the two paths would silently\n * break the MCP wiring.\n */\nexport function buildCodexProviderConfigFlags(serverUrl: string): Array<string> {\n return [\n \"-c\",\n `model_providers.github_router={name=\"github-router\",base_url=\"${serverUrl}/v1\",wire_api=\"responses\",env_key=\"OPENAI_API_KEY\"}`,\n \"-c\",\n \"model_provider=github_router\",\n ]\n}\n\n/**\n * Inspect the installed `codex` binary. Used by the codex-MCP wiring\n * in `claude.ts` to gate `--codex-cli`. Codex 0.129.0 introduced the\n * `mcp-server` subcommand; older versions don't expose it, so we\n * downgrade to the HTTP backend with a warning.\n */\nexport function getCodexVersion(): { ok: boolean; version?: string } {\n if (!commandExists(\"codex\")) return { ok: false }\n let raw: string\n try {\n raw = execFileSync(\"codex\", [\"--version\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim()\n } catch {\n return { ok: false }\n }\n // Output examples: \"codex-cli 0.129.0\", \"codex 0.130.1-dev\"\n const m = /(\\d+)\\.(\\d+)\\.(\\d+)/.exec(raw)\n if (!m) return { ok: false, version: raw }\n const major = Number.parseInt(m[1], 10)\n const minor = Number.parseInt(m[2], 10)\n const version = `${m[1]}.${m[2]}.${m[3]}`\n // mcp-server requires codex >= 0.129.0\n const ok = major > 0 || (major === 0 && minor >= 129)\n return { ok, version }\n}\n\nexport interface LaunchTarget {\n kind: \"claude-code\" | \"codex\"\n envVars: Record<string, string>\n extraArgs: string[]\n model?: string\n /**\n * Proxy URL the spawned child should target. Required for Codex 0.129+\n * which stopped honoring OPENAI_BASE_URL and now needs an explicit\n * `-c model_providers.<name>.base_url=...` argument. Set by the codex\n * subcommand from the same `serverUrl` it computed for env vars.\n */\n serverUrl?: string\n}\n\n/**\n * Codex 0.129.0 broke two things the launcher had been relying on:\n * (1) `--full-auto` was removed in favor of `--sandbox` + `--ask-for-approval`;\n * passing it now exits the child immediately with\n * `error: unexpected argument '--full-auto' found`.\n * (2) `OPENAI_BASE_URL` is silently ignored — Codex hardcodes\n * `https://api.openai.com/v1/responses` and 401s out without an\n * explicit `-c model_providers.<name>.base_url` override.\n *\n * `buildCodexCmd` builds the launch argv that works on Codex 0.129+ while\n * still being compatible with older versions that accept the same flags.\n */\nfunction buildCodexCmd(target: LaunchTarget): string[] {\n const cmd: string[] = [\"codex\"]\n if (target.serverUrl) {\n cmd.push(...buildCodexProviderConfigFlags(target.serverUrl))\n }\n cmd.push(\n \"--sandbox\",\n \"workspace-write\",\n \"--ask-for-approval\",\n \"on-request\",\n \"-m\",\n target.model ?? DEFAULT_CODEX_MODEL,\n ...target.extraArgs,\n )\n return cmd\n}\n\nexport function buildLaunchCommand(target: LaunchTarget): {\n cmd: string[]\n env: Record<string, string | undefined>\n} {\n // F10: don't force `--dangerously-skip-permissions` when the caller already\n // requested an explicit `--permission-mode` (e.g. ai-or-die's claude-bridge\n // appends one for a fleet create_session with permissionMode). The two flags\n // conflict — claude rejects them together — and the explicit mode must win.\n const wantsPermissionMode =\n target.kind === \"claude-code\" &&\n target.extraArgs.some((arg) => arg === \"--permission-mode\" || arg.startsWith(\"--permission-mode=\"))\n const cmd: string[] =\n target.kind === \"claude-code\"\n ? wantsPermissionMode\n ? [\"claude\", ...target.extraArgs]\n : [\"claude\", \"--dangerously-skip-permissions\", ...target.extraArgs]\n : buildCodexCmd(target)\n\n // Anti-shadow: resolve the top-level CLI to an ABSOLUTE path against\n // the clean parent PATH (excluding the cwd). The spawned child's env\n // prepends the toolbelt bin dir to PATH; without this, a stray\n // `claude.cmd`/`codex.cmd` in that dir — or in an untrusted repo's cwd\n // (Windows resolves cwd before PATH under shell:true) — could shadow\n // the real CLI. Resolving here means the toolbelt PATH only affects\n // the agent's OWN tool lookups, never which CLI we launch.\n const resolved = resolveExecutable(cmd[0], { env: process.env })\n if (resolved) cmd[0] = resolved\n\n const env = collapsePathKeys({\n ...sanitizeParentEnv(process.env),\n ...target.envVars,\n })\n return { cmd, env }\n}\n\n/**\n * Whether a resolved Windows executable must be launched through cmd.exe\n * (`shell:true`). Only batch shims (`.cmd`/`.bat`) need it — and even then\n * cmd.exe stays alive as the CLI's parent, so `taskkill /T` reaps the\n * whole tree. A real `.exe` (e.g. the native-installer `claude.exe`) is\n * spawned DIRECTLY so the CLI is the direct child, with no cmd.exe\n * intermediary to orphan its node/MCP grandchildren on a kill.\n */\nexport function windowsLaunchNeedsShell(executable: string): boolean {\n const ext = path.extname(executable).toLowerCase()\n return ext === \".cmd\" || ext === \".bat\"\n}\n\nexport function launchChild(\n target: LaunchTarget,\n server: Server,\n options: { onShutdown?: () => Promise<void> | void } = {},\n): void {\n const { cmd, env } = buildLaunchCommand(target)\n\n const executable = cmd[0]\n if (!isExecutableAvailable(executable)) {\n const msg = `\"${executable}\" not found on PATH. Install it first, then try again.`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n process.exit(1)\n }\n\n let child: ChildProcess\n try {\n if (process.platform === \"win32\") {\n if (windowsLaunchNeedsShell(cmd[0])) {\n // A batch shim genuinely needs cmd.exe. cmd.exe stays alive as the\n // CLI's parent for the shim's lifetime, so the tree-kill (taskkill\n // /T) and the crash guard both reap the whole tree through it. Use\n // the full command as a single string to avoid the DEP0190\n // deprecation warning about shell + args.\n const quoted = cmd.map((a) => (a.includes(\" \") ? `\"${a}\"` : a)).join(\" \")\n child = spawn(quoted, [], {\n env,\n stdio: \"inherit\",\n shell: true,\n })\n } else {\n // A real .exe (e.g. the native-installer claude.exe) needs no\n // shell. Spawning it directly makes the CLI the DIRECT child, so\n // child.kill()/taskkill and the crash guard target the real\n // process instead of a cmd.exe wrapper that would orphan its\n // node/MCP grandchildren.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n })\n }\n } else {\n // detached:true → the CLI leads its OWN process group, so cleanup()\n // can kill(-pgid) the whole tree (grandchildren included). We still\n // await its exit, so we do NOT unref(). Terminal Ctrl-C no longer\n // reaches the child directly — the signal handlers below forward it.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n detached: true,\n })\n }\n } catch (error) {\n const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n server.close(true).catch(() => {})\n if (options.onShutdown) {\n void Promise.resolve(options.onShutdown()).catch(() => {})\n }\n process.exit(1)\n }\n\n // Crash-safe net: if the proxy dies WITHOUT running cleanup() (hard\n // crash, SIGKILL/taskkill, OOM). On Windows this is the OS's job —\n // Node's KILL_ON_JOB_CLOSE job object reaps the tree — so this is a\n // no-op there. On POSIX (no such mechanism) it spawns a detached reaper\n // that sees the proxy's stdin-pipe EOF, re-verifies the child's\n // identity, and reaps the group (also backstopping a graceful shutdown\n // where the child ignores SIGTERM and the proxy exits before the\n // escalation lands). Fire-and-forget; self-acts and self-exits.\n startProcessGuard(child)\n\n let cleaned = false\n let exiting = false\n async function cleanup(): Promise<void> {\n if (cleaned) return\n cleaned = true\n\n // Arm the hard fail-safe FIRST, before any (potentially blocking) kill\n // or async teardown, so a wedged taskkill / server.close can't hang\n // shutdown indefinitely.\n const timeout = setTimeout(() => process.exit(1), 5000)\n\n // Tree-kill the whole CLI subtree, not just the direct child. On\n // Windows taskkill /T reaps grandchildren; on POSIX kill(-pgid) reaps\n // the detached process group. (Replaces a plain child.kill() that\n // orphaned node/MCP grandchildren on Windows.) A SIGTERM-ignoring\n // survivor that outlives our exit is caught by the crash guard above.\n try {\n killChildProcessTree(child, {\n detachedGroup: process.platform !== \"win32\",\n })\n } catch {\n // Already exited / best-effort.\n }\n\n try {\n await server.close(true)\n } catch {\n // Server already closed\n }\n if (options.onShutdown) {\n try {\n await options.onShutdown()\n } catch {\n // Best-effort cleanup; shutdown must not be blocked by it.\n }\n }\n clearTimeout(timeout)\n }\n\n function exit(code: number): void {\n if (exiting) return\n exiting = true\n process.exit(code)\n }\n\n // On POSIX the CLI is in its own process group (detached), so terminal\n // Ctrl-C reaches only the proxy. Forward the signal to the child group\n // so the CLI's own interactive handler runs (e.g. Claude's \"press\n // Ctrl-C again to exit\"); let its natural exit drive shutdown and\n // escalate to a hard tree-kill only if it ignores us past a grace.\n let forwardGrace: NodeJS.Timeout | null = null\n const lastForwardAt: Record<\"SIGINT\" | \"SIGTERM\", number> = {\n SIGINT: 0,\n SIGTERM: 0,\n }\n const onSignal = (sig: \"SIGINT\" | \"SIGTERM\") => {\n if (process.platform !== \"win32\" && child.pid && !cleaned) {\n // Debounce PER SIGNAL a sub-second burst into ONE forward. The other\n // subsystem signal handlers (keep-awake/colbert/worker/browser)\n // re-raise the SAME signal to restore default-terminate, which\n // re-invokes us several times within milliseconds — without this the\n // child would receive a volley and Claude's \"press Ctrl-C again\"\n // guard would never see a single press. Per-signal so a SIGTERM right\n // after a SIGINT still forwards. A genuine second human Ctrl-C\n // (seconds later) falls outside the window and forwards again.\n const now = Date.now()\n if (now - lastForwardAt[sig] > 250) {\n lastForwardAt[sig] = now\n try {\n process.kill(-child.pid, sig)\n } catch {\n // group already gone — fall through to the grace escalation\n }\n }\n const graceMs = sig === \"SIGINT\" ? 10000 : 3000\n if (!forwardGrace) {\n forwardGrace = setTimeout(() => {\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }, graceMs)\n forwardGrace.unref?.()\n }\n return\n }\n // Windows: the console already delivered Ctrl-C to the direct child;\n // cleanup()'s taskkill /T is the escalation hammer.\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }\n process.on(\"SIGINT\", () => onSignal(\"SIGINT\"))\n process.on(\"SIGTERM\", () => onSignal(\"SIGTERM\"))\n\n child.on(\"exit\", (exitCode, signal) => {\n // When the spawned CLI exits we may be holding worker-agent\n // worktrees that the proxy will never get a chance to clean up\n // through the per-call finally (the proxy itself is about to\n // shut down). Drain them synchronously here — same sweep the\n // SIGINT/SIGTERM handlers use. No-op if no worker tools were\n // ever invoked (registry is null).\n try {\n sweepRegistry()\n } catch {\n // best-effort; don't let cleanup failures override the exit code\n }\n // When killed by a signal, exitCode is null — derive from signal number\n const code = exitCode ?? (signal ? 128 : 1)\n cleanup().then(() => exit(code)).catch(() => exit(1))\n })\n child.on(\"error\", () => {\n cleanup().then(() => exit(1)).catch(() => exit(1))\n })\n}\n","/**\n * Toolbelt provisioner: materialize curated CLI tools into the durable\n * router `bin/` dir so the spawned agent can call them on PATH.\n *\n * Properties (all load-bearing):\n * - **Best-effort.** Every tool is isolated in try/catch; a failure\n * skips that one tool and never throws to the launcher.\n * - **Gap-fill.** A tool already on the user's PATH is NOT\n * materialized (and a stale copy is removed) so we never shadow the\n * user's own `jq`/`yq`/etc.\n * - **Verified.** The downloaded archive's SHA256 must match the\n * hardcoded manifest digest BEFORE extraction.\n * - **Safe extraction.** Only the expected regular-file binary is\n * pulled out; symlink/hardlink/device entries are rejected.\n * - **Concurrency-safe.** A cross-process lock serializes writers;\n * unique temp names + atomic rename avoid half-written binaries.\n * - **Integrity-pruned.** Unexpected files in `bin/` are removed each\n * launch so a prompt-injected agent can't plant a `git.cmd` that\n * shadows real tools in future sessions.\n */\n\nimport { randomBytes, createHash } from \"node:crypto\"\nimport { existsSync } from \"node:fs\"\nimport {\n chmod,\n copyFile,\n link,\n mkdir,\n readdir,\n readFile,\n rename,\n rm,\n writeFile,\n} from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport { resolveExecutable, runCommandCapture } from \"../exec\"\nimport { PATHS } from \"../paths\"\nimport { withInstallLock } from \"../update-lock\"\n\nimport { extractTarGzMember, extractZipMember } from \"./extract\"\nimport { toolbeltEnabled, toolbeltSkipSet, vscodeRipgrepPath } from \"./index\"\nimport { assetFor, satisfiesMinVersion, TOOLBELT_TOOLS, type ToolAsset, type ToolSpec } from \"./manifest\"\n\n/** Per-download cap (bytes) — these binaries are a few MB at most. */\nconst MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024\nconst DOWNLOAD_TIMEOUT_MS = 30_000\n\nconst EXE_EXT = process.platform === \"win32\" ? \".exe\" : \"\"\n\n/**\n * Materialize the toolbelt. Returns the list of command names exposed\n * in `bin/` after provisioning. Best-effort; never throws.\n */\nexport async function provisionToolbelt(): Promise<string[]> {\n if (!toolbeltEnabled()) return []\n const binDir = PATHS.TOOLBELT_BIN_DIR\n try {\n await mkdir(binDir, { recursive: true })\n } catch (err) {\n consola.debug(\"toolbelt: could not create bin dir:\", err)\n return []\n }\n\n const skip = toolbeltSkipSet()\n\n // Serialize across concurrent proxies. If another process holds the\n // lock it is provisioning; we skip and just report current state.\n await withInstallLock(\"toolbelt.lock\", async () => {\n await pruneUnexpected(binDir)\n await provisionRg(binDir, skip).catch((err) =>\n consola.debug(\"toolbelt: rg skipped:\", err),\n )\n await Promise.all(\n TOOLBELT_TOOLS.map((spec) =>\n provisionTool(spec, binDir, skip).catch((err) =>\n consola.debug(`toolbelt: ${spec.command} skipped:`, err),\n ),\n ),\n )\n })\n\n return exposedCommands(binDir)\n}\n\n/** Names allowed to live in `bin/` (managed binaries + their sidecars). */\nfunction expectedFileNames(): Set<string> {\n const names = new Set<string>()\n const add = (base: string) => {\n names.add(base + EXE_EXT)\n names.add(`${base}${EXE_EXT}.sha256`)\n }\n add(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n add(spec.binBasename)\n for (const a of spec.aliases ?? []) add(a)\n }\n return names\n}\n\n/** Remove any file in `bin/` that isn't a managed binary or sidecar. */\nasync function pruneUnexpected(binDir: string): Promise<void> {\n const expected = expectedFileNames()\n let entries: string[]\n try {\n entries = await readdir(binDir)\n } catch {\n return\n }\n for (const name of entries) {\n if (name.endsWith(\".tmp\")) continue // in-flight temp from a peer\n if (!expected.has(name)) {\n await rm(path.join(binDir, name), { force: true }).catch(() => {})\n }\n }\n}\n\nasync function provisionRg(binDir: string, skip: Set<string>): Promise<void> {\n const dest = path.join(binDir, \"rg\" + EXE_EXT)\n if (skip.has(\"rg\") || resolveExecutable(\"rg\")) {\n // User has rg (or skipped) — don't shadow it. code_search uses its\n // own resolver regardless.\n await removeBin(dest)\n return\n }\n if (existsSync(dest)) return // already materialized\n const src = vscodeRipgrepPath()\n if (!src) return\n\n const tmp = tempName(dest)\n try {\n await link(src, tmp) // hardlink (same-volume fast path)\n } catch {\n await copyFile(src, tmp) // cross-volume (EXDEV) or other → copy\n }\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/**\n * Does the user's own copy on PATH satisfy `spec.minVersion`?\n *\n * Returns true when there is nothing to check (no minVersion), and — this\n * is the load-bearing part — ALSO returns true when the probe fails or the\n * output cannot be parsed. Gap-fill exists so we never shadow a user's\n * pinned or wrapper install, and an unreadable `--version` is not evidence\n * that their tool is inadequate. We only step in front of it on positive\n * proof that it is too old.\n */\nasync function pathCopyIsAdequate(spec: ToolSpec, resolved: string): Promise<boolean> {\n if (!spec.minVersion) return true\n try {\n // `runCommandCapture`, NOT `runManagedExeCapture`: on Windows a tool\n // installed via npm resolves to a `.cmd` shim, which cannot be spawned\n // with `shell:false` (it throws EINVAL). That is exactly how the\n // ast-grep case presented — the probe threw, we treated the version as\n // unknown, and yielded to the outdated copy anyway. runCommandCapture\n // goes through buildExecInvocation, which routes shims via cmd.exe.\n const res = await runCommandCapture([resolved, \"--version\"], {\n timeoutMs: VERSION_PROBE_TIMEOUT_MS,\n })\n const verdict = satisfiesMinVersion(`${res.stdout} ${res.stderr}`, spec.minVersion)\n if (verdict === undefined) return true\n if (!verdict) {\n consola.debug(\n `[toolbelt] ${spec.command} on PATH is older than ${spec.minVersion}; using the router-pinned build instead`,\n )\n }\n return verdict\n } catch {\n return true\n }\n}\n\n/** A `--version` call should be instant; this only bounds a hung binary. */\nconst VERSION_PROBE_TIMEOUT_MS = 5_000\n\nasync function provisionTool(\n spec: ToolSpec,\n binDir: string,\n skip: Set<string>,\n): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n const sidecar = `${dest}.sha256`\n const asset = assetFor(spec)\n\n if (skip.has(spec.command) || !asset) {\n await removeTool(spec, binDir)\n return\n }\n // Gap-fill: user already has it on PATH → remove ours, don't shadow.\n // Exception: a tool carrying `minVersion` is one this project's own code\n // depends on, so an older PATH copy is not \"good enough\" — it degrades\n // us silently. In that case we materialize ours anyway; the toolbelt bin\n // is prepended to the spawned agent's PATH and code_search checks the\n // router-owned paths first, so ours wins where it matters while the\n // user's shell keeps theirs.\n const onPath = resolveExecutable(spec.command)\n if (onPath && (await pathCopyIsAdequate(spec, onPath))) {\n await removeTool(spec, binDir)\n return\n }\n // Idempotent: present + sidecar matches → just ensure aliases exist.\n if (existsSync(dest) && (await sidecarMatches(sidecar, asset.sha256))) {\n await ensureAliases(spec, binDir, dest)\n return\n }\n\n const bytes = await downloadAndExtract(spec, asset)\n await atomicInstall(dest, bytes)\n await writeFile(sidecar, asset.sha256).catch(() => {})\n await ensureAliases(spec, binDir, dest)\n}\n\nasync function downloadAndExtract(\n spec: ToolSpec,\n asset: ToolAsset,\n): Promise<Buffer> {\n const data = await download(asset.url)\n const digest = createHash(\"sha256\").update(data).digest(\"hex\")\n if (digest !== asset.sha256) {\n throw new Error(\n `checksum mismatch for ${spec.command} (${asset.url}): expected ${asset.sha256}, got ${digest}`,\n )\n }\n if (asset.archive === \"raw\") return data\n const member =\n asset.archive === \"zip\"\n ? extractZipMember(data, spec.binBasename)\n : extractTarGzMember(data, spec.binBasename)\n if (!member) {\n throw new Error(`binary \"${spec.binBasename}\" not found in ${asset.url}`)\n }\n return member\n}\n\nasync function download(url: string): Promise<Buffer> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS)\n try {\n const res = await fetch(url, {\n signal: controller.signal,\n redirect: \"follow\",\n headers: { \"User-Agent\": \"github-router-toolbelt\" },\n })\n if (!res.ok) throw new Error(`download ${url}: HTTP ${res.status}`)\n const buf = Buffer.from(await res.arrayBuffer())\n if (buf.length > MAX_DOWNLOAD_BYTES) {\n throw new Error(`download ${url}: exceeds ${MAX_DOWNLOAD_BYTES} bytes`)\n }\n return buf\n } finally {\n clearTimeout(timer)\n }\n}\n\n/** Write to a unique temp then atomically rename into place. */\nasync function atomicInstall(dest: string, bytes: Buffer): Promise<void> {\n const tmp = tempName(dest)\n await writeFile(tmp, bytes)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/** Rename tmp→dest, handling Windows replace-existing / in-use locks. */\nasync function commit(tmp: string, dest: string): Promise<void> {\n try {\n await rename(tmp, dest)\n } catch {\n // Windows: rename won't replace an existing/locked dest. Remove and\n // retry once; if that fails (in-use .exe), leave the existing copy.\n try {\n await rm(dest, { force: true })\n await rename(tmp, dest)\n } catch (err) {\n await rm(tmp, { force: true }).catch(() => {})\n throw err\n }\n }\n}\n\nasync function ensureAliases(\n spec: ToolSpec,\n binDir: string,\n dest: string,\n): Promise<void> {\n for (const alias of spec.aliases ?? []) {\n const ap = path.join(binDir, alias + EXE_EXT)\n if (existsSync(ap)) continue\n const tmp = tempName(ap)\n try {\n await copyFile(dest, tmp)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, ap)\n } catch (err) {\n consola.debug(`toolbelt: alias ${alias} skipped:`, err)\n }\n }\n}\n\nasync function removeTool(spec: ToolSpec, binDir: string): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n await removeBin(dest)\n for (const alias of spec.aliases ?? []) {\n await removeBin(path.join(binDir, alias + EXE_EXT))\n }\n}\n\nasync function removeBin(dest: string): Promise<void> {\n await rm(dest, { force: true }).catch(() => {})\n await rm(`${dest}.sha256`, { force: true }).catch(() => {})\n}\n\nasync function sidecarMatches(sidecar: string, sha256: string): Promise<boolean> {\n try {\n return (await readFile(sidecar, \"utf8\")).trim() === sha256\n } catch {\n return false\n }\n}\n\nfunction tempName(dest: string): string {\n return `${dest}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`\n}\n\n/** The command names currently exposed in `bin/`. */\nasync function exposedCommands(binDir: string): Promise<string[]> {\n let files: Set<string>\n try {\n files = new Set(await readdir(binDir))\n } catch {\n return []\n }\n const present = (base: string) => files.has(base + EXE_EXT)\n const out: string[] = []\n if (present(\"rg\")) out.push(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n if (present(spec.binBasename)) out.push(spec.command)\n }\n return out\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provision-DeNqzvSM.js","names":[],"sources":["../src/lib/process-guard/index.ts","../src/lib/stripped-env-keys.ts","../src/lib/launch.ts","../src/lib/toolbelt/provision.ts"],"sourcesContent":["/**\n * Crash-safe process guard (POSIX only).\n *\n * If the proxy dies WITHOUT running its own shutdown handlers (hard crash,\n * SIGKILL, OOM), nothing in `cleanup()` runs to reap the launched CLI's\n * process tree. This guard is the last-resort net for that case.\n *\n * Platform split:\n * - **Windows: no guard needed.** Node assigns each child to a Job\n * Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so when the proxy\n * dies (any cause) the OS tears down the whole descendant tree —\n * verified: force-killing a Node parent kills child AND grandchild.\n * `startProcessGuard` is a no-op on win32.\n * - **POSIX: a detached `node -e` reaper.** There is no job-object\n * equivalent (a killed parent's children reparent to init), so we\n * spawn a detached watchdog that learns of proxy death via an inherited\n * stdin PIPE EOF (the proxy holds the write end; the OS closes it on\n * proxy death) and reaps the CLI's process group.\n *\n * Never kill the WRONG process: if the child's PID/PGID was recycled after\n * the child exited, a blind `kill(-pid)` could hit an innocent group. The\n * reaper snapshots the child's process start-time at startup and, on proxy\n * death, kills ONLY if the live PID's start-time still matches. A mismatch\n * / unreadable probe → skip (fail-safe). We never trade an orphan-leak for\n * killing the user's work.\n *\n * The reaper is detached + unref'd so it survives the proxy's SIGKILL, and\n * self-exits once it has acted (or after a 24h cap) so it can never become\n * the orphan it exists to prevent. Opt out with\n * `GH_ROUTER_DISABLE_PROCESS_GUARD=1`.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\"\nimport process from \"node:process\"\n\nimport { parseBoolEnv } from \"../exec\"\n\n/**\n * Live reaper children, held so the GC can't collect the `ChildProcess`\n * (and with it the stdin write-end that is the proxy-death signal) while\n * the proxy runs. Entries self-remove on the reaper's exit. We do NOT\n * proactively kill these on graceful shutdown: when the proxy exits the OS\n * closes the stdin pipe, the reaper hits EOF, re-verifies the child's\n * identity, and reaps any survivor that ignored the graceful SIGTERM —\n * exactly the backstop we want.\n */\nconst _activeReapers = new Set<ChildProcess>()\n\n/** Guard is on by default; `GH_ROUTER_DISABLE_PROCESS_GUARD=1` opts out. */\nexport function processGuardEnabled(): boolean {\n return parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROCESS_GUARD) !== true\n}\n\n/**\n * Build the start-time-verified node reaper script (POSIX). PURE — `pid`\n * is our own integer. `detachedGroup` selects `kill(-pid)` (process group,\n * the detached CLI) vs `kill(pid)` on POSIX.\n *\n * Kept dependency-free (require'd builtins only) so it runs under `node -e`\n * with no module resolution against the dist bundle.\n */\nexport function buildNodeReaperScript(\n pid: number,\n detachedGroup: boolean,\n): string {\n const target = detachedGroup ? \"-PID\" : \"PID\"\n return `\nconst cp = require(\"node:child_process\");\nconst fs = require(\"node:fs\");\nconst PID = ${pid >>> 0};\nfunction startTime() {\n try {\n if (process.platform === \"linux\") {\n const stat = fs.readFileSync(\"/proc/\" + PID + \"/stat\", \"utf8\");\n const post = stat.slice(stat.lastIndexOf(\")\") + 1).trim().split(/\\\\s+/);\n return post[19] || null; // field 22 (starttime) = index 19 after comm\n }\n const out = cp.execFileSync(\"ps\", [\"-o\",\"lstart=\",\"-p\",String(PID)],\n { stdio: [\"ignore\",\"pipe\",\"ignore\"], timeout: 2000 }).toString().trim();\n return out || null;\n } catch { return null; }\n}\nfunction alive() { try { process.kill(PID, 0); return true; } catch { return false; } }\nfunction treeKill() {\n try { process.kill(${target}, \"SIGTERM\"); } catch {}\n // NOT unref'd: this must keep the loop alive to deliver the escalated\n // SIGKILL (the onDeath exit timer below stays alive past 500ms too).\n setTimeout(() => { try { process.kill(${target}, \"SIGKILL\"); } catch {} }, 500);\n}\nconst snap = (() => {\n // The child is definitely ours and alive at startup, so a null here is a\n // transient probe failure (e.g. a momentary 'ps' hiccup), NOT a real\n // identity loss. Retry a few times so a one-off failure can't silently\n // disable the guard for the rest of the run.\n for (let i = 0; i < 3; i++) { const s = startTime(); if (s !== null) return s; }\n return null;\n})();\nlet done = false;\nfunction onDeath() {\n if (done) return; done = true;\n // Re-verify identity: kill ONLY if the live PID is still our child.\n if (snap !== null && alive() && startTime() === snap) treeKill();\n // REF'd (NOT unref'd): stdin has closed, so an unref'd timer would let\n // the loop empty and the process exit immediately — dropping the 500ms\n // SIGKILL escalation. Keep the loop alive to deliver it, then exit.\n setTimeout(() => process.exit(0), 1500);\n}\nprocess.stdin.resume();\nprocess.stdin.on(\"end\", onDeath);\nprocess.stdin.on(\"close\", onDeath);\nprocess.stdin.on(\"error\", onDeath);\nconst cap = setTimeout(() => process.exit(0), 24*3600*1000); if (cap.unref) cap.unref();\n`.trim()\n}\n\n/**\n * Spawn the detached node reaper holding the stdin death-pipe. Always\n * `detached` so it outlives a force-kill of the proxy, and `unref`'d so it\n * never holds the proxy's event loop open. Returns null on spawn failure.\n */\nfunction spawnNodeReaper(pid: number, detachedGroup: boolean): ChildProcess | null {\n try {\n const child = spawn(\n process.execPath,\n [\"-e\", buildNodeReaperScript(pid, detachedGroup)],\n {\n stdio: [\"pipe\", \"ignore\", \"ignore\"],\n detached: true,\n windowsHide: true,\n shell: false,\n },\n )\n child.on(\"error\", () => {})\n child.stdin?.on(\"error\", () => {})\n // Hold a reference so the GC can't drop the ChildProcess (and its stdin\n // write-end, the proxy-death signal) mid-run; drop it on reaper exit.\n _activeReapers.add(child)\n child.once(\"exit\", () => _activeReapers.delete(child))\n child.unref()\n return child\n } catch {\n return null\n }\n}\n\n/**\n * Start the crash-safe guard for a launched CLI child. No-op on Windows\n * (Node's Job Object already reaps the tree on proxy death) and when\n * disabled / unspawnable. Fire-and-forget; never throws.\n */\nexport function startProcessGuard(child: ChildProcess): void {\n if (!processGuardEnabled()) return\n const pid = child.pid\n if (!pid) return\n // Windows: the runtime's KILL_ON_JOB_CLOSE job object is the crash net.\n if (process.platform === \"win32\") return\n // POSIX: the CLI is spawned detached (own group), so reap the group.\n spawnNodeReaper(pid, true)\n}\n","/**\n * Auth / routing / remote / wire-fingerprint environment keys that must never\n * reach a github-router-spawned Claude or Codex child — through ANY channel.\n *\n * Two channels carry these keys to a spawned agent, and both must be defended:\n * 1. The PARENT PROCESS ENV — a user's shell exports. Stripped by\n * `sanitizeParentEnv` (`src/lib/launch.ts`) before the proxy's overrides.\n * 2. The serve mirror `settings.json` `env` BLOCK — a user's own\n * `~/.claude/settings.json`, snapshot-mirrored and applied to the\n * SDK-spawned claude via `settingSources`. Stripped by\n * `sanitizeServeSettingsEnv` (`src/lib/mcp-permissions-settings.ts`).\n *\n * If any of these survived, a Bedrock/Vertex/gateway user's config would\n * re-route the session OFF the github-router proxy, inject real auth that wins\n * over the synthetic `.credentials.json`, or activate Claude Code's\n * unimplemented Bridge/remote code path. `CLAUDE_CODE_COORDINATOR_MODE` (a\n * toolset-stripper) is NOT here — it's serve-specific, added to the serve list\n * directly. `ANTHROPIC_SMALL_FAST_MODEL` is deliberately NOT stripped (users\n * legitimately rely on it; `resolveModel` translates unrecognized values).\n */\nexport const STRIPPED_AUTH_ROUTING_ENV_KEYS = [\n // Claude Code auth surface\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"ANTHROPIC_BASE_URL\",\n \"ANTHROPIC_CUSTOM_HEADERS\",\n \"ANTHROPIC_MODEL\",\n \"CLAUDE_CODE_OAUTH_TOKEN\",\n // Alternate OAuth source (loads from an open file descriptor); stripping\n // prevents a third auth source alongside the synthetic .credentials.json.\n \"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR\",\n \"CLAUDE_CODE_USE_BEDROCK\",\n \"CLAUDE_CODE_USE_VERTEX\",\n \"CLAUDE_CODE_USE_FOUNDRY\",\n // Defense-in-depth: a parent- or settings-set CLAUDE_CONFIG_DIR would\n // redirect the session's config dir away from the router-owned mirror.\n \"CLAUDE_CONFIG_DIR\",\n // Claude Code Bridge / IDE remote-session surface — activates a remote code\n // path (POST /v1/code/sessions, /v1/environments/bridge, …) the proxy does\n // not implement. Stripping forces a local-only session.\n \"CLAUDE_BRIDGE_OAUTH_TOKEN\",\n \"CLAUDE_BRIDGE_BASE_URL\",\n \"CLAUDE_BRIDGE_SESSION_INGRESS_URL\",\n \"SESSION_INGRESS_URL\",\n \"CLAUDE_CODE_REMOTE\",\n \"CLAUDE_CODE_CONTAINER_ID\",\n \"CLAUDE_CODE_REMOTE_SESSION_ID\",\n \"CLAUDE_CODE_SESSION_ID\",\n // Emits `x-anthropic-additional-protection` — pure wire-fingerprint noise\n // that breaks the VS Code stealth posture.\n \"CLAUDE_CODE_ADDITIONAL_PROTECTION\",\n // Codex CLI auth surface\n \"OPENAI_API_KEY\",\n \"OPENAI_BASE_URL\",\n \"CODEX_HOME\",\n // ai-or-die per-tab session-bind / artifact-review surface — must not leak\n // into a nested launch and hijack the parent tab's sidecar / bearer token.\n \"AIORDIE_CLAUDE_BIND\",\n \"AIORDIE_TOKEN\",\n \"AIORDIE_INSECURE_TLS\",\n] as const\n","import { execFileSync, spawn, type ChildProcess } from \"node:child_process\"\nimport { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport type { Server } from \"srvx\"\n\nimport { resolveExecutable, killChildProcessTree } from \"./exec\"\nimport { DEFAULT_CODEX_MODEL } from \"./port\"\nimport { startProcessGuard } from \"./process-guard\"\nimport { STRIPPED_AUTH_ROUTING_ENV_KEYS } from \"./stripped-env-keys\"\nimport { collapsePathKeys } from \"./toolbelt/path-inject\"\nimport { sweepRegistry } from \"./worker-agent/lifecycle\"\n\n/**\n * Auth-related env keys we strip from the parent before spawning the child CLI.\n * The proxy provides its own values for everything we care about, so the child\n * behaves as if the user had no parent-env auth at all. The canonical list lives\n * in `./stripped-env-keys` (shared with serve's settings.json `env` strip).\n *\n * Why strip rather than override-with-empty-string: Claude Code emits \"Auth\n * conflict\" warnings whenever both ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY\n * are present (any value); stripping suppresses it AND prevents an inherited\n * real key from leaking. Cloud-provider toggles + OAuth tokens are simpler\n * dropped than overridden — a missing var is unambiguously absent everywhere.\n */\nconst STRIPPED_PARENT_ENV_KEYS = STRIPPED_AUTH_ROUTING_ENV_KEYS\n\n/**\n * Strip auth-related keys from a parent-process env object. The result\n * is suitable to spread into a spawned child's env BEFORE the proxy's\n * explicit overrides, so the proxy is the only source of truth for\n * auth — and stale shell exports can't leak through.\n */\nexport function sanitizeParentEnv(\n parent: NodeJS.ProcessEnv,\n): NodeJS.ProcessEnv {\n const sanitized: NodeJS.ProcessEnv = { ...parent }\n for (const key of STRIPPED_PARENT_ENV_KEYS) {\n delete sanitized[key]\n }\n return sanitized\n}\n\nfunction commandExists(name: string): boolean {\n try {\n execFileSync(process.platform === \"win32\" ? \"where.exe\" : \"which\", [name], {\n stdio: \"ignore\",\n })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Whether the launcher can execute `executable`.\n *\n * `buildLaunchCommand` resolves the CLI to an ABSOLUTE path (anti-shadow).\n * `where.exe` (and POSIX `which`) reject a full path argument — `where`\n * returns \"Could not find files for the given pattern(s)\" for an absolute\n * path even when the file exists — so the where/which probe is only valid\n * for bare command names. For an absolute path (already resolved against\n * PATH and existence-checked by `resolveExecutable`), check the\n * filesystem directly. Without this split, every launch where the CLI is\n * installed fails with a spurious \"not found on PATH\".\n */\nexport function isExecutableAvailable(executable: string): boolean {\n if (path.isAbsolute(executable)) return existsSync(executable)\n return commandExists(executable)\n}\n\n/**\n * Provider-config flags (`-c model_providers.github_router=...`) that\n * point Codex at our proxy. Extracted from `buildCodexCmd` so the new\n * `codex mcp-server` MCP-config builder can reuse the exact same\n * provider definition — drift between the two paths would silently\n * break the MCP wiring.\n */\nexport function buildCodexProviderConfigFlags(serverUrl: string): Array<string> {\n return [\n \"-c\",\n `model_providers.github_router={name=\"github-router\",base_url=\"${serverUrl}/v1\",wire_api=\"responses\",env_key=\"OPENAI_API_KEY\"}`,\n \"-c\",\n \"model_provider=github_router\",\n ]\n}\n\n/**\n * Inspect the installed `codex` binary. Used by the codex-MCP wiring\n * in `claude.ts` to gate `--codex-cli`. Codex 0.129.0 introduced the\n * `mcp-server` subcommand; older versions don't expose it, so we\n * downgrade to the HTTP backend with a warning.\n */\nexport function getCodexVersion(): { ok: boolean; version?: string } {\n if (!commandExists(\"codex\")) return { ok: false }\n let raw: string\n try {\n raw = execFileSync(\"codex\", [\"--version\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim()\n } catch {\n return { ok: false }\n }\n // Output examples: \"codex-cli 0.129.0\", \"codex 0.130.1-dev\"\n const m = /(\\d+)\\.(\\d+)\\.(\\d+)/.exec(raw)\n if (!m) return { ok: false, version: raw }\n const major = Number.parseInt(m[1], 10)\n const minor = Number.parseInt(m[2], 10)\n const version = `${m[1]}.${m[2]}.${m[3]}`\n // mcp-server requires codex >= 0.129.0\n const ok = major > 0 || (major === 0 && minor >= 129)\n return { ok, version }\n}\n\nexport interface LaunchTarget {\n kind: \"claude-code\" | \"codex\"\n envVars: Record<string, string>\n extraArgs: string[]\n model?: string\n /**\n * Proxy URL the spawned child should target. Required for Codex 0.129+\n * which stopped honoring OPENAI_BASE_URL and now needs an explicit\n * `-c model_providers.<name>.base_url=...` argument. Set by the codex\n * subcommand from the same `serverUrl` it computed for env vars.\n */\n serverUrl?: string\n}\n\n/**\n * Codex 0.129.0 broke two things the launcher had been relying on:\n * (1) `--full-auto` was removed in favor of `--sandbox` + `--ask-for-approval`;\n * passing it now exits the child immediately with\n * `error: unexpected argument '--full-auto' found`.\n * (2) `OPENAI_BASE_URL` is silently ignored — Codex hardcodes\n * `https://api.openai.com/v1/responses` and 401s out without an\n * explicit `-c model_providers.<name>.base_url` override.\n *\n * `buildCodexCmd` builds the launch argv that works on Codex 0.129+ while\n * still being compatible with older versions that accept the same flags.\n */\nfunction buildCodexCmd(target: LaunchTarget): string[] {\n const cmd: string[] = [\"codex\"]\n if (target.serverUrl) {\n cmd.push(...buildCodexProviderConfigFlags(target.serverUrl))\n }\n cmd.push(\n \"--sandbox\",\n \"workspace-write\",\n \"--ask-for-approval\",\n \"on-request\",\n \"-m\",\n target.model ?? DEFAULT_CODEX_MODEL,\n ...target.extraArgs,\n )\n return cmd\n}\n\nexport function buildLaunchCommand(target: LaunchTarget): {\n cmd: string[]\n env: Record<string, string | undefined>\n} {\n // F10: don't force `--dangerously-skip-permissions` when the caller already\n // requested an explicit `--permission-mode` (e.g. ai-or-die's claude-bridge\n // appends one for a fleet create_session with permissionMode). The two flags\n // conflict — claude rejects them together — and the explicit mode must win.\n const wantsPermissionMode =\n target.kind === \"claude-code\" &&\n target.extraArgs.some((arg) => arg === \"--permission-mode\" || arg.startsWith(\"--permission-mode=\"))\n const cmd: string[] =\n target.kind === \"claude-code\"\n ? wantsPermissionMode\n ? [\"claude\", ...target.extraArgs]\n : [\"claude\", \"--dangerously-skip-permissions\", ...target.extraArgs]\n : buildCodexCmd(target)\n\n // Anti-shadow: resolve the top-level CLI to an ABSOLUTE path against\n // the clean parent PATH (excluding the cwd). The spawned child's env\n // prepends the toolbelt bin dir to PATH; without this, a stray\n // `claude.cmd`/`codex.cmd` in that dir — or in an untrusted repo's cwd\n // (Windows resolves cwd before PATH under shell:true) — could shadow\n // the real CLI. Resolving here means the toolbelt PATH only affects\n // the agent's OWN tool lookups, never which CLI we launch.\n const resolved = resolveExecutable(cmd[0], { env: process.env })\n if (resolved) cmd[0] = resolved\n\n const env = collapsePathKeys({\n ...sanitizeParentEnv(process.env),\n ...target.envVars,\n })\n return { cmd, env }\n}\n\n/**\n * Whether a resolved Windows executable must be launched through cmd.exe\n * (`shell:true`). Only batch shims (`.cmd`/`.bat`) need it — and even then\n * cmd.exe stays alive as the CLI's parent, so `taskkill /T` reaps the\n * whole tree. A real `.exe` (e.g. the native-installer `claude.exe`) is\n * spawned DIRECTLY so the CLI is the direct child, with no cmd.exe\n * intermediary to orphan its node/MCP grandchildren on a kill.\n */\nexport function windowsLaunchNeedsShell(executable: string): boolean {\n const ext = path.extname(executable).toLowerCase()\n return ext === \".cmd\" || ext === \".bat\"\n}\n\nexport function launchChild(\n target: LaunchTarget,\n server: Server,\n options: { onShutdown?: () => Promise<void> | void } = {},\n): void {\n const { cmd, env } = buildLaunchCommand(target)\n\n const executable = cmd[0]\n if (!isExecutableAvailable(executable)) {\n const msg = `\"${executable}\" not found on PATH. Install it first, then try again.`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n process.exit(1)\n }\n\n let child: ChildProcess\n try {\n if (process.platform === \"win32\") {\n if (windowsLaunchNeedsShell(cmd[0])) {\n // A batch shim genuinely needs cmd.exe. cmd.exe stays alive as the\n // CLI's parent for the shim's lifetime, so the tree-kill (taskkill\n // /T) and the crash guard both reap the whole tree through it. Use\n // the full command as a single string to avoid the DEP0190\n // deprecation warning about shell + args.\n const quoted = cmd.map((a) => (a.includes(\" \") ? `\"${a}\"` : a)).join(\" \")\n child = spawn(quoted, [], {\n env,\n stdio: \"inherit\",\n shell: true,\n })\n } else {\n // A real .exe (e.g. the native-installer claude.exe) needs no\n // shell. Spawning it directly makes the CLI the DIRECT child, so\n // child.kill()/taskkill and the crash guard target the real\n // process instead of a cmd.exe wrapper that would orphan its\n // node/MCP grandchildren.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n })\n }\n } else {\n // detached:true → the CLI leads its OWN process group, so cleanup()\n // can kill(-pgid) the whole tree (grandchildren included). We still\n // await its exit, so we do NOT unref(). Terminal Ctrl-C no longer\n // reaches the child directly — the signal handlers below forward it.\n child = spawn(cmd[0], cmd.slice(1), {\n env,\n stdio: \"inherit\",\n detached: true,\n })\n }\n } catch (error) {\n const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`\n consola.error(msg)\n process.stderr.write(msg + \"\\n\")\n server.close(true).catch(() => {})\n if (options.onShutdown) {\n void Promise.resolve(options.onShutdown()).catch(() => {})\n }\n process.exit(1)\n }\n\n // Crash-safe net: if the proxy dies WITHOUT running cleanup() (hard\n // crash, SIGKILL/taskkill, OOM). On Windows this is the OS's job —\n // Node's KILL_ON_JOB_CLOSE job object reaps the tree — so this is a\n // no-op there. On POSIX (no such mechanism) it spawns a detached reaper\n // that sees the proxy's stdin-pipe EOF, re-verifies the child's\n // identity, and reaps the group (also backstopping a graceful shutdown\n // where the child ignores SIGTERM and the proxy exits before the\n // escalation lands). Fire-and-forget; self-acts and self-exits.\n startProcessGuard(child)\n\n let cleaned = false\n let exiting = false\n async function cleanup(): Promise<void> {\n if (cleaned) return\n cleaned = true\n\n // Arm the hard fail-safe FIRST, before any (potentially blocking) kill\n // or async teardown, so a wedged taskkill / server.close can't hang\n // shutdown indefinitely.\n const timeout = setTimeout(() => process.exit(1), 5000)\n\n // Tree-kill the whole CLI subtree, not just the direct child. On\n // Windows taskkill /T reaps grandchildren; on POSIX kill(-pgid) reaps\n // the detached process group. (Replaces a plain child.kill() that\n // orphaned node/MCP grandchildren on Windows.) A SIGTERM-ignoring\n // survivor that outlives our exit is caught by the crash guard above.\n try {\n killChildProcessTree(child, {\n detachedGroup: process.platform !== \"win32\",\n })\n } catch {\n // Already exited / best-effort.\n }\n\n try {\n await server.close(true)\n } catch {\n // Server already closed\n }\n if (options.onShutdown) {\n try {\n await options.onShutdown()\n } catch {\n // Best-effort cleanup; shutdown must not be blocked by it.\n }\n }\n clearTimeout(timeout)\n }\n\n function exit(code: number): void {\n if (exiting) return\n exiting = true\n process.exit(code)\n }\n\n // On POSIX the CLI is in its own process group (detached), so terminal\n // Ctrl-C reaches only the proxy. Forward the signal to the child group\n // so the CLI's own interactive handler runs (e.g. Claude's \"press\n // Ctrl-C again to exit\"); let its natural exit drive shutdown and\n // escalate to a hard tree-kill only if it ignores us past a grace.\n let forwardGrace: NodeJS.Timeout | null = null\n const lastForwardAt: Record<\"SIGINT\" | \"SIGTERM\", number> = {\n SIGINT: 0,\n SIGTERM: 0,\n }\n const onSignal = (sig: \"SIGINT\" | \"SIGTERM\") => {\n if (process.platform !== \"win32\" && child.pid && !cleaned) {\n // Debounce PER SIGNAL a sub-second burst into ONE forward. The other\n // subsystem signal handlers (keep-awake/colbert/worker/browser)\n // re-raise the SAME signal to restore default-terminate, which\n // re-invokes us several times within milliseconds — without this the\n // child would receive a volley and Claude's \"press Ctrl-C again\"\n // guard would never see a single press. Per-signal so a SIGTERM right\n // after a SIGINT still forwards. A genuine second human Ctrl-C\n // (seconds later) falls outside the window and forwards again.\n const now = Date.now()\n if (now - lastForwardAt[sig] > 250) {\n lastForwardAt[sig] = now\n try {\n process.kill(-child.pid, sig)\n } catch {\n // group already gone — fall through to the grace escalation\n }\n }\n const graceMs = sig === \"SIGINT\" ? 10000 : 3000\n if (!forwardGrace) {\n forwardGrace = setTimeout(() => {\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }, graceMs)\n forwardGrace.unref?.()\n }\n return\n }\n // Windows: the console already delivered Ctrl-C to the direct child;\n // cleanup()'s taskkill /T is the escalation hammer.\n cleanup().then(() => exit(130)).catch(() => exit(1))\n }\n process.on(\"SIGINT\", () => onSignal(\"SIGINT\"))\n process.on(\"SIGTERM\", () => onSignal(\"SIGTERM\"))\n\n child.on(\"exit\", (exitCode, signal) => {\n // When the spawned CLI exits we may be holding worker-agent\n // worktrees that the proxy will never get a chance to clean up\n // through the per-call finally (the proxy itself is about to\n // shut down). Drain them synchronously here — same sweep the\n // SIGINT/SIGTERM handlers use. No-op if no worker tools were\n // ever invoked (registry is null).\n try {\n sweepRegistry()\n } catch {\n // best-effort; don't let cleanup failures override the exit code\n }\n // When killed by a signal, exitCode is null — derive from signal number\n const code = exitCode ?? (signal ? 128 : 1)\n cleanup().then(() => exit(code)).catch(() => exit(1))\n })\n child.on(\"error\", () => {\n cleanup().then(() => exit(1)).catch(() => exit(1))\n })\n}\n","/**\n * Toolbelt provisioner: materialize curated CLI tools into the durable\n * router `bin/` dir so the spawned agent can call them on PATH.\n *\n * Properties (all load-bearing):\n * - **Best-effort.** Every tool is isolated in try/catch; a failure\n * skips that one tool and never throws to the launcher.\n * - **Gap-fill.** A tool already on the user's PATH is NOT\n * materialized (and a stale copy is removed) so we never shadow the\n * user's own `jq`/`yq`/etc.\n * - **Verified.** The downloaded archive's SHA256 must match the\n * hardcoded manifest digest BEFORE extraction.\n * - **Safe extraction.** Only the expected regular-file binary is\n * pulled out; symlink/hardlink/device entries are rejected.\n * - **Concurrency-safe.** A cross-process lock serializes writers;\n * unique temp names + atomic rename avoid half-written binaries.\n * - **Integrity-pruned.** Unexpected files in `bin/` are removed each\n * launch so a prompt-injected agent can't plant a `git.cmd` that\n * shadows real tools in future sessions.\n */\n\nimport { randomBytes, createHash } from \"node:crypto\"\nimport { existsSync } from \"node:fs\"\nimport {\n chmod,\n copyFile,\n link,\n mkdir,\n readdir,\n readFile,\n rename,\n rm,\n writeFile,\n} from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport consola from \"consola\"\n\nimport { resolveExecutable, runCommandCapture } from \"../exec\"\nimport { PATHS } from \"../paths\"\nimport { withInstallLock } from \"../update-lock\"\n\nimport { extractTarGzMember, extractZipMember } from \"./extract\"\nimport { toolbeltEnabled, toolbeltSkipSet, vscodeRipgrepPath } from \"./index\"\nimport { assetFor, satisfiesMinVersion, TOOLBELT_TOOLS, type ToolAsset, type ToolSpec } from \"./manifest\"\n\n/** Per-download cap (bytes) — these binaries are a few MB at most. */\nconst MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024\nconst DOWNLOAD_TIMEOUT_MS = 30_000\n\nconst EXE_EXT = process.platform === \"win32\" ? \".exe\" : \"\"\n\n/**\n * Materialize the toolbelt. Returns the list of command names exposed\n * in `bin/` after provisioning. Best-effort; never throws.\n */\nexport async function provisionToolbelt(): Promise<string[]> {\n if (!toolbeltEnabled()) return []\n const binDir = PATHS.TOOLBELT_BIN_DIR\n try {\n await mkdir(binDir, { recursive: true })\n } catch (err) {\n consola.debug(\"toolbelt: could not create bin dir:\", err)\n return []\n }\n\n const skip = toolbeltSkipSet()\n\n // Serialize across concurrent proxies. If another process holds the\n // lock it is provisioning; we skip and just report current state.\n await withInstallLock(\"toolbelt.lock\", async () => {\n await pruneUnexpected(binDir)\n await provisionRg(binDir, skip).catch((err) =>\n consola.debug(\"toolbelt: rg skipped:\", err),\n )\n await Promise.all(\n TOOLBELT_TOOLS.map((spec) =>\n provisionTool(spec, binDir, skip).catch((err) =>\n consola.debug(`toolbelt: ${spec.command} skipped:`, err),\n ),\n ),\n )\n })\n\n return exposedCommands(binDir)\n}\n\n/** Names allowed to live in `bin/` (managed binaries + their sidecars). */\nfunction expectedFileNames(): Set<string> {\n const names = new Set<string>()\n const add = (base: string) => {\n names.add(base + EXE_EXT)\n names.add(`${base}${EXE_EXT}.sha256`)\n }\n add(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n add(spec.binBasename)\n for (const a of spec.aliases ?? []) add(a)\n }\n return names\n}\n\n/** Remove any file in `bin/` that isn't a managed binary or sidecar. */\nasync function pruneUnexpected(binDir: string): Promise<void> {\n const expected = expectedFileNames()\n let entries: string[]\n try {\n entries = await readdir(binDir)\n } catch {\n return\n }\n for (const name of entries) {\n if (name.endsWith(\".tmp\")) continue // in-flight temp from a peer\n if (!expected.has(name)) {\n await rm(path.join(binDir, name), { force: true }).catch(() => {})\n }\n }\n}\n\nasync function provisionRg(binDir: string, skip: Set<string>): Promise<void> {\n const dest = path.join(binDir, \"rg\" + EXE_EXT)\n if (skip.has(\"rg\") || resolveExecutable(\"rg\")) {\n // User has rg (or skipped) — don't shadow it. code_search uses its\n // own resolver regardless.\n await removeBin(dest)\n return\n }\n if (existsSync(dest)) return // already materialized\n const src = vscodeRipgrepPath()\n if (!src) return\n\n const tmp = tempName(dest)\n try {\n await link(src, tmp) // hardlink (same-volume fast path)\n } catch {\n await copyFile(src, tmp) // cross-volume (EXDEV) or other → copy\n }\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/**\n * Does the user's own copy on PATH satisfy `spec.minVersion`?\n *\n * Returns true when there is nothing to check (no minVersion), and — this\n * is the load-bearing part — ALSO returns true when the probe fails or the\n * output cannot be parsed. Gap-fill exists so we never shadow a user's\n * pinned or wrapper install, and an unreadable `--version` is not evidence\n * that their tool is inadequate. We only step in front of it on positive\n * proof that it is too old.\n */\nasync function pathCopyIsAdequate(spec: ToolSpec, resolved: string): Promise<boolean> {\n if (!spec.minVersion) return true\n try {\n // `runCommandCapture`, NOT `runManagedExeCapture`: on Windows a tool\n // installed via npm resolves to a `.cmd` shim, which cannot be spawned\n // with `shell:false` (it throws EINVAL). That is exactly how the\n // ast-grep case presented — the probe threw, we treated the version as\n // unknown, and yielded to the outdated copy anyway. runCommandCapture\n // goes through buildExecInvocation, which routes shims via cmd.exe.\n const res = await runCommandCapture([resolved, \"--version\"], {\n timeoutMs: VERSION_PROBE_TIMEOUT_MS,\n })\n const verdict = satisfiesMinVersion(`${res.stdout} ${res.stderr}`, spec.minVersion)\n if (verdict === undefined) return true\n if (!verdict) {\n consola.debug(\n `[toolbelt] ${spec.command} on PATH is older than ${spec.minVersion}; using the router-pinned build instead`,\n )\n }\n return verdict\n } catch {\n return true\n }\n}\n\n/** A `--version` call should be instant; this only bounds a hung binary. */\nconst VERSION_PROBE_TIMEOUT_MS = 5_000\n\nasync function provisionTool(\n spec: ToolSpec,\n binDir: string,\n skip: Set<string>,\n): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n const sidecar = `${dest}.sha256`\n const asset = assetFor(spec)\n\n if (skip.has(spec.command) || !asset) {\n await removeTool(spec, binDir)\n return\n }\n // Gap-fill: user already has it on PATH → remove ours, don't shadow.\n // Exception: a tool carrying `minVersion` is one this project's own code\n // depends on, so an older PATH copy is not \"good enough\" — it degrades\n // us silently. In that case we materialize ours anyway; the toolbelt bin\n // is prepended to the spawned agent's PATH and code_search checks the\n // router-owned paths first, so ours wins where it matters while the\n // user's shell keeps theirs.\n const onPath = resolveExecutable(spec.command)\n if (onPath && (await pathCopyIsAdequate(spec, onPath))) {\n await removeTool(spec, binDir)\n return\n }\n // Idempotent: present + sidecar matches → just ensure aliases exist.\n if (existsSync(dest) && (await sidecarMatches(sidecar, asset.sha256))) {\n await ensureAliases(spec, binDir, dest)\n return\n }\n\n const bytes = await downloadAndExtract(spec, asset)\n await atomicInstall(dest, bytes)\n await writeFile(sidecar, asset.sha256).catch(() => {})\n await ensureAliases(spec, binDir, dest)\n}\n\nasync function downloadAndExtract(\n spec: ToolSpec,\n asset: ToolAsset,\n): Promise<Buffer> {\n const data = await download(asset.url)\n const digest = createHash(\"sha256\").update(data).digest(\"hex\")\n if (digest !== asset.sha256) {\n throw new Error(\n `checksum mismatch for ${spec.command} (${asset.url}): expected ${asset.sha256}, got ${digest}`,\n )\n }\n if (asset.archive === \"raw\") return data\n const member =\n asset.archive === \"zip\"\n ? extractZipMember(data, spec.binBasename)\n : extractTarGzMember(data, spec.binBasename)\n if (!member) {\n throw new Error(`binary \"${spec.binBasename}\" not found in ${asset.url}`)\n }\n return member\n}\n\nasync function download(url: string): Promise<Buffer> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS)\n try {\n const res = await fetch(url, {\n signal: controller.signal,\n redirect: \"follow\",\n headers: { \"User-Agent\": \"github-router-toolbelt\" },\n })\n if (!res.ok) throw new Error(`download ${url}: HTTP ${res.status}`)\n const buf = Buffer.from(await res.arrayBuffer())\n if (buf.length > MAX_DOWNLOAD_BYTES) {\n throw new Error(`download ${url}: exceeds ${MAX_DOWNLOAD_BYTES} bytes`)\n }\n return buf\n } finally {\n clearTimeout(timer)\n }\n}\n\n/** Write to a unique temp then atomically rename into place. */\nasync function atomicInstall(dest: string, bytes: Buffer): Promise<void> {\n const tmp = tempName(dest)\n await writeFile(tmp, bytes)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, dest)\n}\n\n/** Rename tmp→dest, handling Windows replace-existing / in-use locks. */\nasync function commit(tmp: string, dest: string): Promise<void> {\n try {\n await rename(tmp, dest)\n } catch {\n // Windows: rename won't replace an existing/locked dest. Remove and\n // retry once; if that fails (in-use .exe), leave the existing copy.\n try {\n await rm(dest, { force: true })\n await rename(tmp, dest)\n } catch (err) {\n await rm(tmp, { force: true }).catch(() => {})\n throw err\n }\n }\n}\n\nasync function ensureAliases(\n spec: ToolSpec,\n binDir: string,\n dest: string,\n): Promise<void> {\n for (const alias of spec.aliases ?? []) {\n const ap = path.join(binDir, alias + EXE_EXT)\n if (existsSync(ap)) continue\n const tmp = tempName(ap)\n try {\n await copyFile(dest, tmp)\n if (process.platform !== \"win32\") await chmod(tmp, 0o755).catch(() => {})\n await commit(tmp, ap)\n } catch (err) {\n consola.debug(`toolbelt: alias ${alias} skipped:`, err)\n }\n }\n}\n\nasync function removeTool(spec: ToolSpec, binDir: string): Promise<void> {\n const dest = path.join(binDir, spec.binBasename + EXE_EXT)\n await removeBin(dest)\n for (const alias of spec.aliases ?? []) {\n await removeBin(path.join(binDir, alias + EXE_EXT))\n }\n}\n\nasync function removeBin(dest: string): Promise<void> {\n await rm(dest, { force: true }).catch(() => {})\n await rm(`${dest}.sha256`, { force: true }).catch(() => {})\n}\n\nasync function sidecarMatches(sidecar: string, sha256: string): Promise<boolean> {\n try {\n return (await readFile(sidecar, \"utf8\")).trim() === sha256\n } catch {\n return false\n }\n}\n\nfunction tempName(dest: string): string {\n return `${dest}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`\n}\n\n/** The command names currently exposed in `bin/`. */\nasync function exposedCommands(binDir: string): Promise<string[]> {\n let files: Set<string>\n try {\n files = new Set(await readdir(binDir))\n } catch {\n return []\n }\n const present = (base: string) => files.has(base + EXE_EXT)\n const out: string[] = []\n if (present(\"rg\")) out.push(\"rg\")\n for (const spec of TOOLBELT_TOOLS) {\n if (present(spec.binBasename)) out.push(spec.command)\n }\n return out\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as PACKAGE_ROOT_FLAG } from "./package-root-B-osctCk.js";
|
|
2
|
-
import { t as PATHS } from "./paths-
|
|
2
|
+
import { t as PATHS } from "./paths-Ci485qSJ.js";
|
|
3
3
|
import consola from "consola";
|
|
4
4
|
import { chmodSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
@@ -377,4 +377,4 @@ function realpathIfPossible(p) {
|
|
|
377
377
|
//#endregion
|
|
378
378
|
export { hookLauncherDegradedWarning as n, resolveSelfInvocation as r, buildSelfCommand as t };
|
|
379
379
|
|
|
380
|
-
//# sourceMappingURL=self-invocation-
|
|
380
|
+
//# sourceMappingURL=self-invocation-DAB_od0C.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"self-invocation-DhO1Z8iD.js","names":[],"sources":["../src/lib/hook-launcher/provision.ts","../src/lib/hook-launcher/self-invocation.ts"],"sourcesContent":["/**\n * Publish the self-contained hook launcher (`dist/hooks.mjs`) to a stable,\n * content-addressed path under `<APP_DIR>/hooks/`.\n *\n * Why this exists: `github-router claude` persists Claude Code hook commands\n * into a settings.json that outlives the process that wrote it, built from\n * `process.argv[1]`. Under `bunx` that path is inside `$TMPDIR`, and macOS's\n * per-user temp reaper deletes files there (keeping the directory skeleton) —\n * so `node <tmp>/…/dist/main.js` died on its first bare import with\n * `ERR_MODULE_NOT_FOUND` and EVERY hook in a long-running session failed until\n * the tree happened to be re-extracted. `bunx pkg@latest` also re-extracts in\n * place whenever npm's `latest` advances, so a live session's hooks could\n * silently start executing a different version's code.\n *\n * Deliberate departures from `provisionBrowserAssets()`, which this otherwise\n * mirrors:\n *\n * - **Content-addressed, immutable names.** The digest IS the filename, so a\n * new build never overwrites an old launcher and there is nothing to\n * garbage-collect. Version-keyed names would need GC, and GC across\n * concurrent proxies is a race: proxy N deleting N+1's launcher (or the\n * reverse) breaks a live session's hooks. At ~1.5 MB, keeping stale\n * launchers costs less than the race would.\n *\n * - **The source is verified before it is published.** Hashing the bytes\n * read from `dist/hooks.mjs` only identifies those bytes; it does not\n * prove they are a complete artifact. A concurrent in-place `bunx`\n * extraction can hand over a truncated file, which content-addressing\n * would happily preserve forever under its own hash. The build writes a\n * `dist/hooks.sha256` sidecar and we publish only on a match.\n *\n * - **Failures are not latched.** A transient `EBUSY` (Windows antivirus, a\n * concurrent reader) must not disable provisioning for the process\n * lifetime, so only success is cached.\n *\n * Never throws: the caller degrades to `process.argv[1]` with a visible\n * warning rather than failing the launch.\n */\n\nimport { createHash } from \"node:crypto\"\nimport {\n chmodSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n utimesSync,\n writeFileSync,\n} from \"node:fs\"\nimport path from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nimport consola from \"consola\"\n\nimport { PATHS } from \"../paths\"\n\n/** Sidecar written by `scripts/stamp-hooks-digest.ts` next to the bundle. */\nconst DIGEST_SIDECAR = \"hooks.sha256\"\n\n/** Basename of the built launcher bundle. */\nconst BUNDLE_NAME = \"hooks.mjs\"\n\n/**\n * Publish retries. A rename can lose a race with another proxy publishing the\n * same digest, or hit a Windows sharing violation while an antivirus scanner\n * holds the temp file. Both clear in milliseconds.\n */\nconst PUBLISH_ATTEMPTS = 3\n\nlet _published: string | undefined\nlet _inFlight: Promise<string | undefined> | undefined\n\n/** @internal — reset module state between test cases. */\nexport function __resetHookLauncherForTests(): void {\n _published = undefined\n _inFlight = undefined\n}\n\n/**\n * Resolve the built bundle that sits beside the running code.\n *\n * In a built layout every chunk lives in `dist/`, so a sibling lookup finds\n * `dist/hooks.mjs`. Running from source it resolves to\n * `src/lib/hook-launcher/hooks.mjs`, which does not exist — which is exactly\n * the wanted behaviour: a `bun run dev` session must keep using\n * `process.argv[1]` rather than silently adopting a stale bundle from some\n * earlier build.\n */\nfunction bundledLauncherPath(): string | undefined {\n try {\n return path.join(path.dirname(fileURLToPath(import.meta.url)), BUNDLE_NAME)\n } catch {\n return undefined\n }\n}\n\nfunction sha256(buf: Buffer): string {\n return createHash(\"sha256\").update(buf).digest(\"hex\")\n}\n\n/**\n * Read the build's expected digest. Absent or malformed means \"cannot verify\",\n * which is treated as a refusal to publish rather than as permission to skip\n * the check — an unverified source is the one thing content-addressing cannot\n * recover from.\n */\nfunction expectedDigest(bundlePath: string): string | undefined {\n try {\n const raw = readFileSync(\n path.join(path.dirname(bundlePath), DIGEST_SIDECAR),\n \"utf8\",\n ).trim()\n return /^[0-9a-f]{64}$/.test(raw) ? raw : undefined\n } catch {\n return undefined\n }\n}\n\n/** True when `target` already holds exactly the bytes its name claims. */\nfunction targetIsValid(target: string, digest: string): boolean {\n try {\n return sha256(readFileSync(target)) === digest\n } catch {\n return false\n }\n}\n\n/**\n * Write `bytes` to `target` via a unique same-directory temp file plus a\n * rename, so a crash mid-write can never leave a partial file under the name\n * a hook command points at.\n *\n * The target is never deleted first: on Windows that would open a window where\n * a concurrently-launching session finds no launcher at all. A rename onto an\n * existing path can fail there, so a losing racer validates what the winner\n * wrote and accepts it.\n */\nfunction publish(target: string, bytes: Buffer, digest: string): boolean {\n // Checked once up front rather than per attempt: validation re-hashes ~1.5 MB\n // and the retries exist for rename contention, which revalidating cannot fix.\n if (targetIsValid(target, digest)) return true\n for (let attempt = 0; attempt < PUBLISH_ATTEMPTS; attempt++) {\n const tmp = `${target}.${process.pid}-${attempt}.tmp`\n try {\n writeFileSync(tmp, bytes, { mode: 0o600 })\n renameSync(tmp, target)\n return true\n } catch {\n try {\n rmSync(tmp, { force: true })\n } catch {\n // Best-effort. A Windows sharing violation can strand the temp file;\n // `sweepStaleLaunchers` reaps it later rather than leaking 1.5 MB.\n }\n }\n }\n return targetIsValid(target, digest)\n}\n\n/** A stranded publish temp is dead well inside this. */\nconst TMP_MAX_AGE_MS = 24 * 60 * 60 * 1000\n\n/**\n * Launchers older than this are reaped. Generous on purpose — see the residual\n * risk in `sweepStaleLaunchers`.\n */\nconst LAUNCHER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\n/**\n * Reap stranded publish temps and long-unused launchers.\n *\n * Without this the directory grows by ~1.5 MB per shipped version forever, and\n * this project ships a patch release per PR with self-update on by default.\n *\n * The immutable content-addressed naming exists to avoid a GC race, so the\n * sweep is built to not reintroduce one: it never touches the digest THIS\n * launch resolved, and every launch refreshes its own launcher's mtime, so a\n * file survives for as long as some proxy keeps launching against it. Residual\n * risk, stated plainly: a single proxy running continuously for more than 30\n * days without relaunching could have its launcher reaped by a newer proxy.\n * That is bounded and far rarer than the unbounded growth it prevents, but it\n * is not zero — a PID-liveness marker would close it if it ever bites.\n */\nfunction sweepStaleLaunchers(dir: string, keep: string): void {\n const now = Date.now()\n let entries: Array<string>\n try {\n entries = readdirSync(dir)\n } catch {\n return\n }\n for (const name of entries) {\n if (name === path.basename(keep)) continue\n const isTmp = name.endsWith(\".tmp\")\n // Strict shape match, like sweepAgedWorkerDiffs: this directory is ours,\n // but a name-shape check means a false positive can only ever cost a file\n // the next launch regenerates.\n const isLauncher = /^hooks-[0-9a-f]{64}\\.mjs$/.test(name)\n if (!isTmp && !isLauncher) continue\n const target = path.join(dir, name)\n try {\n const age = now - statSync(target).mtimeMs\n if (age > (isTmp ? TMP_MAX_AGE_MS : LAUNCHER_MAX_AGE_MS)) {\n rmSync(target, { force: true })\n }\n } catch {\n /* best-effort */\n }\n }\n}\n\n/**\n * Materialize the launcher and return its stable absolute path, or undefined\n * when it could not be published (running from source, bundle missing or\n * unverifiable, filesystem refusing the write).\n *\n * Single-flight so the startup call and any lazy caller collapse to one run.\n */\nexport function provisionHookLauncher(): Promise<string | undefined> {\n if (_published) return Promise.resolve(_published)\n if (_inFlight) return _inFlight\n _inFlight = Promise.resolve()\n .then(() => provisionImpl())\n .then((result) => {\n // Cache SUCCESS only. Latching a transient failure would disable the\n // stable launcher for the whole process lifetime.\n if (result) _published = result\n return result\n })\n .finally(() => {\n _inFlight = undefined\n })\n return _inFlight\n}\n\nfunction provisionImpl(): string | undefined {\n const bundlePath = bundledLauncherPath()\n if (!bundlePath) return undefined\n return publishLauncherFrom(bundlePath)\n}\n\n/**\n * Verify and publish one specific bundle. Split out from the source resolution\n * above so the publication semantics are testable: running from source,\n * `bundledLauncherPath()` deliberately points at a file that does not exist,\n * which is the right behaviour but leaves nothing to exercise.\n *\n * @internal — production callers go through `provisionHookLauncher()`.\n */\nexport function publishLauncherFrom(bundlePath: string): string | undefined {\n try {\n let bytes: Buffer\n try {\n bytes = readFileSync(bundlePath)\n } catch {\n // Running from source, or the package tree was reaped out from under us.\n // Either way there is nothing to publish; a launcher published by an\n // earlier launch still stands.\n return undefined\n }\n\n const digest = sha256(bytes)\n const expected = expectedDigest(bundlePath)\n if (!expected) {\n // \"Cannot verify\" and \"verified bad\" both mean do-not-publish. Treating\n // an absent sidecar as permission would delete the integrity check in\n // exactly the degraded conditions it exists for.\n consola.debug(\n \"[hook-launcher] no digest sidecar beside the bundle; not publishing\",\n )\n return undefined\n }\n if (expected !== digest) {\n // The classic cause is reading mid-`bunx`-re-extraction. Publishing here\n // would freeze a torn bundle under a stable name forever, because\n // content-addressing gives a truncated file its own perfectly valid hash.\n consola.debug(\n \"[hook-launcher] bundle digest does not match the build; not publishing\",\n )\n return undefined\n }\n\n const dir = PATHS.HOOK_LAUNCHER_DIR\n // 0o700 / 0o600, matching the convention for the runtime dir in paths.ts.\n // This is not incidental hygiene: the published file is EXECUTED by node on\n // every prompt of every session, so a group- or world-writable path here\n // would let a local attacker swap the launcher and run code inside the\n // user's agent. chmod is best-effort — it is a no-op on Windows, where the\n // ACL inherited from APP_DIR governs.\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n try {\n chmodSync(dir, 0o700)\n } catch {\n /* best-effort: Windows, or a dir we do not own */\n }\n const target = path.join(dir, `hooks-${digest}.mjs`)\n if (!publish(target, bytes, digest)) return undefined\n\n // Mark this launcher as still in use before sweeping, so a long-lived\n // install that keeps relaunching against an old version is never reaped by\n // a newer proxy's sweep.\n try {\n const now = new Date()\n utimesSync(target, now, now)\n } catch {\n /* best-effort */\n }\n sweepStaleLaunchers(dir, target)\n return target\n } catch (err) {\n consola.debug(\"[hook-launcher] provisioning skipped:\", err)\n return undefined\n }\n}\n","/**\n * How this process names ITSELF in a command string that gets persisted to\n * disk and executed later by someone else.\n *\n * Every Claude Code hook, the MCP `headersHelper`, and the worker-guard are\n * registered as shell commands inside a settings.json that outlives the\n * process that wrote it. They were built from `(process.execPath,\n * process.argv[1])`, which under `bunx` points into `$TMPDIR` — a directory\n * macOS reaps and `bunx pkg@latest` re-extracts in place. A persisted command\n * pointing there stops working mid-session (see\n * `src/lib/hook-launcher/provision.ts` for the failure in full).\n *\n * So the pair is resolved ONCE per launch, through the stable relocated\n * launcher when one can be published, and every builder composes its command\n * from that single answer.\n */\n\nimport { readFileSync, realpathSync } from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport { PACKAGE_ROOT_FLAG } from \"../package-root\"\nimport { PATHS } from \"../paths\"\nimport { provisionHookLauncher } from \"./provision\"\n\n/** The binary + script + package root a persisted command should name. */\nexport interface SelfInvocation {\n /** The node/bun binary to exec. */\n execPath: string\n /** Script to pass to it, omitted for a packaged single-file build. */\n scriptPath?: string\n /**\n * The github-router package root, baked in only when `scriptPath` is the\n * relocated launcher. From `<APP_DIR>/hooks/` nothing can derive it: the\n * pre-existing walk in `browser-mcp/native-host-installer.ts` falls back to\n * `process.cwd()`, which would silently resolve to the USER'S WORKSPACE.\n */\n packageRoot?: string\n}\n\nfunction quote(s: string): string {\n return `\"${s}\"`\n}\n\n/**\n * Compose a persisted command string: the binary, the script when it differs\n * from the binary, the baked package root when there is one, then the args.\n *\n * Every builder in the codebase routes through here so the three parts cannot\n * drift apart — and so a future part is added in one place, not eleven.\n */\nexport function buildSelfCommand(inv: SelfInvocation, args: string): string {\n const parts = [quote(inv.execPath)]\n if (inv.scriptPath && inv.scriptPath !== inv.execPath) {\n parts.push(quote(inv.scriptPath))\n }\n if (inv.packageRoot) {\n parts.push(PACKAGE_ROOT_FLAG, quote(inv.packageRoot))\n }\n if (args) parts.push(args)\n return parts.join(\" \")\n}\n\n/** The invocation describing the currently running entrypoint, unrelocated. */\nexport function currentInvocation(): SelfInvocation {\n return { execPath: process.execPath, scriptPath: process.argv[1] }\n}\n\nlet _resolved: SelfInvocation | undefined\n\n/** @internal — reset module state between test cases. */\nexport function __resetSelfInvocationForTests(): void {\n _resolved = undefined\n}\n\n/**\n * Resolve the invocation to bake into every persisted command this launch\n * writes. Awaited BEFORE the settings are written, never fire-and-forget: a\n * command persisted while provisioning is still pending would name the very\n * temp path this exists to avoid, and the session would stay broken however\n * well provisioning later went.\n *\n * Falls back to the running entrypoint if the launcher cannot be published, so\n * a provisioning failure degrades rather than breaking a launch. The caller is\n * responsible for surfacing that fallback — see `hookLauncherDegradedWarning`.\n */\nexport async function resolveSelfInvocation(): Promise<SelfInvocation> {\n if (_resolved) return _resolved\n const launcher = await provisionHookLauncher()\n _resolved =\n launcher ?\n {\n execPath: process.execPath,\n scriptPath: launcher,\n packageRoot: currentPackageRoot(),\n }\n : currentInvocation()\n return _resolved\n}\n\n/**\n * A human-facing warning when this launch is about to persist hook commands\n * pointing into a directory the OS may reap — the exact configuration that\n * produced the original bug.\n *\n * Returns the message rather than logging it, mirroring\n * `colbertDegradedWarning`: `claude` and `codex` call `enableFileLogging()`,\n * which redirects `consola.warn` to the error log, so a warning logged here\n * would be invisible on the one path where it matters most. The caller writes\n * it to stderr next to the readiness line instead.\n *\n * Silent when running from source, which has no bundle to publish and is not a\n * degraded state.\n */\nexport function hookLauncherDegradedWarning(\n inv: SelfInvocation,\n): string | null {\n if (!inv.scriptPath) return null\n // Our own published launcher is checked FIRST, and is authoritative: we know\n // we published it, so it can never warrant the \"could not publish\" message\n // below even if APP_DIR itself sits somewhere unusual.\n if (inv.scriptPath.startsWith(PATHS.HOOK_LAUNCHER_DIR)) {\n if (inv.packageRoot) return null\n // Relocated but rootless. Today this only degrades `--version` inside the\n // launcher, because nothing in the hook graph consumes the package root for\n // anything else. It is reported anyway because it is a trap rather than a\n // bug: the day a hook subcommand pulls in code_search or the browser\n // installer, `packageRoot()` falls through to `process.cwd()` and starts\n // resolving against the user's REPO with no signal at all.\n return (\n \"Hook launcher published without a package root; hook version reporting \"\n + \"will be degraded. This usually means the install tree is partially \"\n + \"missing — reinstall github-router.\"\n )\n }\n if (isUnderVolatileRoot(inv.scriptPath)) {\n return (\n \"Hook commands point into a temporary directory and will stop working \"\n + \"when the OS reaps it (the stable launcher could not be published). \"\n + \"Reinstall with `npm install -g github-router` to avoid this.\"\n )\n }\n return null\n}\n\n/**\n * The package root of the RUNNING code, derived while we are still inside the\n * package tree — the one moment it is knowable without guessing.\n *\n * The name is checked, not just the presence of a package.json: this value is\n * baked into a persisted command and consumed by `packageRoot()` as the\n * highest-priority source, so handing it the WRONG root would be worse than\n * handing it none. Matches `findPackageRoot()` in\n * `browser-mcp/native-host-installer.ts`, whose fallbacks this supersedes.\n */\nfunction currentPackageRoot(): string | undefined {\n const entry = typeof process.argv[1] === \"string\" ? process.argv[1] : undefined\n if (!entry) return undefined\n let cur = path.dirname(entry)\n for (let i = 0; i < 10; i++) {\n try {\n const pkg = JSON.parse(\n readFileSync(path.join(cur, \"package.json\"), \"utf8\"),\n ) as { name?: unknown }\n if (typeof pkg.name === \"string\" && pkg.name.includes(\"github-router\")) {\n return cur\n }\n } catch {\n // Not here, or unreadable; walk up.\n }\n const parent = path.dirname(cur)\n if (parent === cur) break\n cur = parent\n }\n return undefined\n}\n\n/**\n * Whether a path sits under a directory the OS is entitled to clean out from\n * under a running session: the per-user temp dir on every platform, which is\n * where `bunx` (unlike `npx`, which uses a cache under $HOME) installs.\n *\n * Both sides are compared in resolved AND unresolved form. macOS reports\n * `$TMPDIR` as `/var/folders/…` while a process sees `/private/var/folders/…`,\n * so a single-form prefix test silently never matches; and `realpathSync`\n * fails on a path that does not exist, which would otherwise make the answer\n * depend on whether the file happens to be there.\n */\nexport function isUnderVolatileRoot(target: string): boolean {\n try {\n const prefixes = new Set<string>()\n for (const root of [os.tmpdir(), realpathIfPossible(os.tmpdir())]) {\n prefixes.add(root.endsWith(path.sep) ? root : root + path.sep)\n }\n const targets = [target, realpathIfPossible(target)]\n return targets.some((t) => [...prefixes].some((p) => t.startsWith(p)))\n } catch {\n return false\n }\n}\n\nfunction realpathIfPossible(p: string): string {\n try {\n return realpathSync(p)\n } catch {\n return p\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"self-invocation-DAB_od0C.js","names":[],"sources":["../src/lib/hook-launcher/provision.ts","../src/lib/hook-launcher/self-invocation.ts"],"sourcesContent":["/**\n * Publish the self-contained hook launcher (`dist/hooks.mjs`) to a stable,\n * content-addressed path under `<APP_DIR>/hooks/`.\n *\n * Why this exists: `github-router claude` persists Claude Code hook commands\n * into a settings.json that outlives the process that wrote it, built from\n * `process.argv[1]`. Under `bunx` that path is inside `$TMPDIR`, and macOS's\n * per-user temp reaper deletes files there (keeping the directory skeleton) —\n * so `node <tmp>/…/dist/main.js` died on its first bare import with\n * `ERR_MODULE_NOT_FOUND` and EVERY hook in a long-running session failed until\n * the tree happened to be re-extracted. `bunx pkg@latest` also re-extracts in\n * place whenever npm's `latest` advances, so a live session's hooks could\n * silently start executing a different version's code.\n *\n * Deliberate departures from `provisionBrowserAssets()`, which this otherwise\n * mirrors:\n *\n * - **Content-addressed, immutable names.** The digest IS the filename, so a\n * new build never overwrites an old launcher and there is nothing to\n * garbage-collect. Version-keyed names would need GC, and GC across\n * concurrent proxies is a race: proxy N deleting N+1's launcher (or the\n * reverse) breaks a live session's hooks. At ~1.5 MB, keeping stale\n * launchers costs less than the race would.\n *\n * - **The source is verified before it is published.** Hashing the bytes\n * read from `dist/hooks.mjs` only identifies those bytes; it does not\n * prove they are a complete artifact. A concurrent in-place `bunx`\n * extraction can hand over a truncated file, which content-addressing\n * would happily preserve forever under its own hash. The build writes a\n * `dist/hooks.sha256` sidecar and we publish only on a match.\n *\n * - **Failures are not latched.** A transient `EBUSY` (Windows antivirus, a\n * concurrent reader) must not disable provisioning for the process\n * lifetime, so only success is cached.\n *\n * Never throws: the caller degrades to `process.argv[1]` with a visible\n * warning rather than failing the launch.\n */\n\nimport { createHash } from \"node:crypto\"\nimport {\n chmodSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n utimesSync,\n writeFileSync,\n} from \"node:fs\"\nimport path from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nimport consola from \"consola\"\n\nimport { PATHS } from \"../paths\"\n\n/** Sidecar written by `scripts/stamp-hooks-digest.ts` next to the bundle. */\nconst DIGEST_SIDECAR = \"hooks.sha256\"\n\n/** Basename of the built launcher bundle. */\nconst BUNDLE_NAME = \"hooks.mjs\"\n\n/**\n * Publish retries. A rename can lose a race with another proxy publishing the\n * same digest, or hit a Windows sharing violation while an antivirus scanner\n * holds the temp file. Both clear in milliseconds.\n */\nconst PUBLISH_ATTEMPTS = 3\n\nlet _published: string | undefined\nlet _inFlight: Promise<string | undefined> | undefined\n\n/** @internal — reset module state between test cases. */\nexport function __resetHookLauncherForTests(): void {\n _published = undefined\n _inFlight = undefined\n}\n\n/**\n * Resolve the built bundle that sits beside the running code.\n *\n * In a built layout every chunk lives in `dist/`, so a sibling lookup finds\n * `dist/hooks.mjs`. Running from source it resolves to\n * `src/lib/hook-launcher/hooks.mjs`, which does not exist — which is exactly\n * the wanted behaviour: a `bun run dev` session must keep using\n * `process.argv[1]` rather than silently adopting a stale bundle from some\n * earlier build.\n */\nfunction bundledLauncherPath(): string | undefined {\n try {\n return path.join(path.dirname(fileURLToPath(import.meta.url)), BUNDLE_NAME)\n } catch {\n return undefined\n }\n}\n\nfunction sha256(buf: Buffer): string {\n return createHash(\"sha256\").update(buf).digest(\"hex\")\n}\n\n/**\n * Read the build's expected digest. Absent or malformed means \"cannot verify\",\n * which is treated as a refusal to publish rather than as permission to skip\n * the check — an unverified source is the one thing content-addressing cannot\n * recover from.\n */\nfunction expectedDigest(bundlePath: string): string | undefined {\n try {\n const raw = readFileSync(\n path.join(path.dirname(bundlePath), DIGEST_SIDECAR),\n \"utf8\",\n ).trim()\n return /^[0-9a-f]{64}$/.test(raw) ? raw : undefined\n } catch {\n return undefined\n }\n}\n\n/** True when `target` already holds exactly the bytes its name claims. */\nfunction targetIsValid(target: string, digest: string): boolean {\n try {\n return sha256(readFileSync(target)) === digest\n } catch {\n return false\n }\n}\n\n/**\n * Write `bytes` to `target` via a unique same-directory temp file plus a\n * rename, so a crash mid-write can never leave a partial file under the name\n * a hook command points at.\n *\n * The target is never deleted first: on Windows that would open a window where\n * a concurrently-launching session finds no launcher at all. A rename onto an\n * existing path can fail there, so a losing racer validates what the winner\n * wrote and accepts it.\n */\nfunction publish(target: string, bytes: Buffer, digest: string): boolean {\n // Checked once up front rather than per attempt: validation re-hashes ~1.5 MB\n // and the retries exist for rename contention, which revalidating cannot fix.\n if (targetIsValid(target, digest)) return true\n for (let attempt = 0; attempt < PUBLISH_ATTEMPTS; attempt++) {\n const tmp = `${target}.${process.pid}-${attempt}.tmp`\n try {\n writeFileSync(tmp, bytes, { mode: 0o600 })\n renameSync(tmp, target)\n return true\n } catch {\n try {\n rmSync(tmp, { force: true })\n } catch {\n // Best-effort. A Windows sharing violation can strand the temp file;\n // `sweepStaleLaunchers` reaps it later rather than leaking 1.5 MB.\n }\n }\n }\n return targetIsValid(target, digest)\n}\n\n/** A stranded publish temp is dead well inside this. */\nconst TMP_MAX_AGE_MS = 24 * 60 * 60 * 1000\n\n/**\n * Launchers older than this are reaped. Generous on purpose — see the residual\n * risk in `sweepStaleLaunchers`.\n */\nconst LAUNCHER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\n/**\n * Reap stranded publish temps and long-unused launchers.\n *\n * Without this the directory grows by ~1.5 MB per shipped version forever, and\n * this project ships a patch release per PR with self-update on by default.\n *\n * The immutable content-addressed naming exists to avoid a GC race, so the\n * sweep is built to not reintroduce one: it never touches the digest THIS\n * launch resolved, and every launch refreshes its own launcher's mtime, so a\n * file survives for as long as some proxy keeps launching against it. Residual\n * risk, stated plainly: a single proxy running continuously for more than 30\n * days without relaunching could have its launcher reaped by a newer proxy.\n * That is bounded and far rarer than the unbounded growth it prevents, but it\n * is not zero — a PID-liveness marker would close it if it ever bites.\n */\nfunction sweepStaleLaunchers(dir: string, keep: string): void {\n const now = Date.now()\n let entries: Array<string>\n try {\n entries = readdirSync(dir)\n } catch {\n return\n }\n for (const name of entries) {\n if (name === path.basename(keep)) continue\n const isTmp = name.endsWith(\".tmp\")\n // Strict shape match, like sweepAgedWorkerDiffs: this directory is ours,\n // but a name-shape check means a false positive can only ever cost a file\n // the next launch regenerates.\n const isLauncher = /^hooks-[0-9a-f]{64}\\.mjs$/.test(name)\n if (!isTmp && !isLauncher) continue\n const target = path.join(dir, name)\n try {\n const age = now - statSync(target).mtimeMs\n if (age > (isTmp ? TMP_MAX_AGE_MS : LAUNCHER_MAX_AGE_MS)) {\n rmSync(target, { force: true })\n }\n } catch {\n /* best-effort */\n }\n }\n}\n\n/**\n * Materialize the launcher and return its stable absolute path, or undefined\n * when it could not be published (running from source, bundle missing or\n * unverifiable, filesystem refusing the write).\n *\n * Single-flight so the startup call and any lazy caller collapse to one run.\n */\nexport function provisionHookLauncher(): Promise<string | undefined> {\n if (_published) return Promise.resolve(_published)\n if (_inFlight) return _inFlight\n _inFlight = Promise.resolve()\n .then(() => provisionImpl())\n .then((result) => {\n // Cache SUCCESS only. Latching a transient failure would disable the\n // stable launcher for the whole process lifetime.\n if (result) _published = result\n return result\n })\n .finally(() => {\n _inFlight = undefined\n })\n return _inFlight\n}\n\nfunction provisionImpl(): string | undefined {\n const bundlePath = bundledLauncherPath()\n if (!bundlePath) return undefined\n return publishLauncherFrom(bundlePath)\n}\n\n/**\n * Verify and publish one specific bundle. Split out from the source resolution\n * above so the publication semantics are testable: running from source,\n * `bundledLauncherPath()` deliberately points at a file that does not exist,\n * which is the right behaviour but leaves nothing to exercise.\n *\n * @internal — production callers go through `provisionHookLauncher()`.\n */\nexport function publishLauncherFrom(bundlePath: string): string | undefined {\n try {\n let bytes: Buffer\n try {\n bytes = readFileSync(bundlePath)\n } catch {\n // Running from source, or the package tree was reaped out from under us.\n // Either way there is nothing to publish; a launcher published by an\n // earlier launch still stands.\n return undefined\n }\n\n const digest = sha256(bytes)\n const expected = expectedDigest(bundlePath)\n if (!expected) {\n // \"Cannot verify\" and \"verified bad\" both mean do-not-publish. Treating\n // an absent sidecar as permission would delete the integrity check in\n // exactly the degraded conditions it exists for.\n consola.debug(\n \"[hook-launcher] no digest sidecar beside the bundle; not publishing\",\n )\n return undefined\n }\n if (expected !== digest) {\n // The classic cause is reading mid-`bunx`-re-extraction. Publishing here\n // would freeze a torn bundle under a stable name forever, because\n // content-addressing gives a truncated file its own perfectly valid hash.\n consola.debug(\n \"[hook-launcher] bundle digest does not match the build; not publishing\",\n )\n return undefined\n }\n\n const dir = PATHS.HOOK_LAUNCHER_DIR\n // 0o700 / 0o600, matching the convention for the runtime dir in paths.ts.\n // This is not incidental hygiene: the published file is EXECUTED by node on\n // every prompt of every session, so a group- or world-writable path here\n // would let a local attacker swap the launcher and run code inside the\n // user's agent. chmod is best-effort — it is a no-op on Windows, where the\n // ACL inherited from APP_DIR governs.\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n try {\n chmodSync(dir, 0o700)\n } catch {\n /* best-effort: Windows, or a dir we do not own */\n }\n const target = path.join(dir, `hooks-${digest}.mjs`)\n if (!publish(target, bytes, digest)) return undefined\n\n // Mark this launcher as still in use before sweeping, so a long-lived\n // install that keeps relaunching against an old version is never reaped by\n // a newer proxy's sweep.\n try {\n const now = new Date()\n utimesSync(target, now, now)\n } catch {\n /* best-effort */\n }\n sweepStaleLaunchers(dir, target)\n return target\n } catch (err) {\n consola.debug(\"[hook-launcher] provisioning skipped:\", err)\n return undefined\n }\n}\n","/**\n * How this process names ITSELF in a command string that gets persisted to\n * disk and executed later by someone else.\n *\n * Every Claude Code hook, the MCP `headersHelper`, and the worker-guard are\n * registered as shell commands inside a settings.json that outlives the\n * process that wrote it. They were built from `(process.execPath,\n * process.argv[1])`, which under `bunx` points into `$TMPDIR` — a directory\n * macOS reaps and `bunx pkg@latest` re-extracts in place. A persisted command\n * pointing there stops working mid-session (see\n * `src/lib/hook-launcher/provision.ts` for the failure in full).\n *\n * So the pair is resolved ONCE per launch, through the stable relocated\n * launcher when one can be published, and every builder composes its command\n * from that single answer.\n */\n\nimport { readFileSync, realpathSync } from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport { PACKAGE_ROOT_FLAG } from \"../package-root\"\nimport { PATHS } from \"../paths\"\nimport { provisionHookLauncher } from \"./provision\"\n\n/** The binary + script + package root a persisted command should name. */\nexport interface SelfInvocation {\n /** The node/bun binary to exec. */\n execPath: string\n /** Script to pass to it, omitted for a packaged single-file build. */\n scriptPath?: string\n /**\n * The github-router package root, baked in only when `scriptPath` is the\n * relocated launcher. From `<APP_DIR>/hooks/` nothing can derive it: the\n * pre-existing walk in `browser-mcp/native-host-installer.ts` falls back to\n * `process.cwd()`, which would silently resolve to the USER'S WORKSPACE.\n */\n packageRoot?: string\n}\n\nfunction quote(s: string): string {\n return `\"${s}\"`\n}\n\n/**\n * Compose a persisted command string: the binary, the script when it differs\n * from the binary, the baked package root when there is one, then the args.\n *\n * Every builder in the codebase routes through here so the three parts cannot\n * drift apart — and so a future part is added in one place, not eleven.\n */\nexport function buildSelfCommand(inv: SelfInvocation, args: string): string {\n const parts = [quote(inv.execPath)]\n if (inv.scriptPath && inv.scriptPath !== inv.execPath) {\n parts.push(quote(inv.scriptPath))\n }\n if (inv.packageRoot) {\n parts.push(PACKAGE_ROOT_FLAG, quote(inv.packageRoot))\n }\n if (args) parts.push(args)\n return parts.join(\" \")\n}\n\n/** The invocation describing the currently running entrypoint, unrelocated. */\nexport function currentInvocation(): SelfInvocation {\n return { execPath: process.execPath, scriptPath: process.argv[1] }\n}\n\nlet _resolved: SelfInvocation | undefined\n\n/** @internal — reset module state between test cases. */\nexport function __resetSelfInvocationForTests(): void {\n _resolved = undefined\n}\n\n/**\n * Resolve the invocation to bake into every persisted command this launch\n * writes. Awaited BEFORE the settings are written, never fire-and-forget: a\n * command persisted while provisioning is still pending would name the very\n * temp path this exists to avoid, and the session would stay broken however\n * well provisioning later went.\n *\n * Falls back to the running entrypoint if the launcher cannot be published, so\n * a provisioning failure degrades rather than breaking a launch. The caller is\n * responsible for surfacing that fallback — see `hookLauncherDegradedWarning`.\n */\nexport async function resolveSelfInvocation(): Promise<SelfInvocation> {\n if (_resolved) return _resolved\n const launcher = await provisionHookLauncher()\n _resolved =\n launcher ?\n {\n execPath: process.execPath,\n scriptPath: launcher,\n packageRoot: currentPackageRoot(),\n }\n : currentInvocation()\n return _resolved\n}\n\n/**\n * A human-facing warning when this launch is about to persist hook commands\n * pointing into a directory the OS may reap — the exact configuration that\n * produced the original bug.\n *\n * Returns the message rather than logging it, mirroring\n * `colbertDegradedWarning`: `claude` and `codex` call `enableFileLogging()`,\n * which redirects `consola.warn` to the error log, so a warning logged here\n * would be invisible on the one path where it matters most. The caller writes\n * it to stderr next to the readiness line instead.\n *\n * Silent when running from source, which has no bundle to publish and is not a\n * degraded state.\n */\nexport function hookLauncherDegradedWarning(\n inv: SelfInvocation,\n): string | null {\n if (!inv.scriptPath) return null\n // Our own published launcher is checked FIRST, and is authoritative: we know\n // we published it, so it can never warrant the \"could not publish\" message\n // below even if APP_DIR itself sits somewhere unusual.\n if (inv.scriptPath.startsWith(PATHS.HOOK_LAUNCHER_DIR)) {\n if (inv.packageRoot) return null\n // Relocated but rootless. Today this only degrades `--version` inside the\n // launcher, because nothing in the hook graph consumes the package root for\n // anything else. It is reported anyway because it is a trap rather than a\n // bug: the day a hook subcommand pulls in code_search or the browser\n // installer, `packageRoot()` falls through to `process.cwd()` and starts\n // resolving against the user's REPO with no signal at all.\n return (\n \"Hook launcher published without a package root; hook version reporting \"\n + \"will be degraded. This usually means the install tree is partially \"\n + \"missing — reinstall github-router.\"\n )\n }\n if (isUnderVolatileRoot(inv.scriptPath)) {\n return (\n \"Hook commands point into a temporary directory and will stop working \"\n + \"when the OS reaps it (the stable launcher could not be published). \"\n + \"Reinstall with `npm install -g github-router` to avoid this.\"\n )\n }\n return null\n}\n\n/**\n * The package root of the RUNNING code, derived while we are still inside the\n * package tree — the one moment it is knowable without guessing.\n *\n * The name is checked, not just the presence of a package.json: this value is\n * baked into a persisted command and consumed by `packageRoot()` as the\n * highest-priority source, so handing it the WRONG root would be worse than\n * handing it none. Matches `findPackageRoot()` in\n * `browser-mcp/native-host-installer.ts`, whose fallbacks this supersedes.\n */\nfunction currentPackageRoot(): string | undefined {\n const entry = typeof process.argv[1] === \"string\" ? process.argv[1] : undefined\n if (!entry) return undefined\n let cur = path.dirname(entry)\n for (let i = 0; i < 10; i++) {\n try {\n const pkg = JSON.parse(\n readFileSync(path.join(cur, \"package.json\"), \"utf8\"),\n ) as { name?: unknown }\n if (typeof pkg.name === \"string\" && pkg.name.includes(\"github-router\")) {\n return cur\n }\n } catch {\n // Not here, or unreadable; walk up.\n }\n const parent = path.dirname(cur)\n if (parent === cur) break\n cur = parent\n }\n return undefined\n}\n\n/**\n * Whether a path sits under a directory the OS is entitled to clean out from\n * under a running session: the per-user temp dir on every platform, which is\n * where `bunx` (unlike `npx`, which uses a cache under $HOME) installs.\n *\n * Both sides are compared in resolved AND unresolved form. macOS reports\n * `$TMPDIR` as `/var/folders/…` while a process sees `/private/var/folders/…`,\n * so a single-form prefix test silently never matches; and `realpathSync`\n * fails on a path that does not exist, which would otherwise make the answer\n * depend on whether the file happens to be there.\n */\nexport function isUnderVolatileRoot(target: string): boolean {\n try {\n const prefixes = new Set<string>()\n for (const root of [os.tmpdir(), realpathIfPossible(os.tmpdir())]) {\n prefixes.add(root.endsWith(path.sep) ? root : root + path.sep)\n }\n const targets = [target, realpathIfPossible(target)]\n return targets.some((t) => [...prefixes].some((p) => t.startsWith(p)))\n } catch {\n return false\n }\n}\n\nfunction realpathIfPossible(p: string): string {\n try {\n return realpathSync(p)\n } catch {\n return p\n }\n}\n"],"mappings":"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"}
|