github-router 0.3.249 → 0.3.255

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/attribution-settings-CegtE0TY.js +2347 -0
  2. package/dist/attribution-settings-CegtE0TY.js.map +1 -0
  3. package/dist/auth-CP4q2CjO.js +44 -0
  4. package/dist/auth-CP4q2CjO.js.map +1 -0
  5. package/dist/browser-ext/background.js +8 -1
  6. package/dist/browser-ext/manifest.json +1 -1
  7. package/dist/check-usage-6PB5IUFb.js +43 -0
  8. package/dist/check-usage-6PB5IUFb.js.map +1 -0
  9. package/dist/claude-BiZxoNWq.js +593 -0
  10. package/dist/claude-BiZxoNWq.js.map +1 -0
  11. package/dist/client-CQMfroGV.js +413 -0
  12. package/dist/client-CQMfroGV.js.map +1 -0
  13. package/dist/codex-ST21X3WF.js +85 -0
  14. package/dist/codex-ST21X3WF.js.map +1 -0
  15. package/dist/debug-CjmE9sgG.js +81 -0
  16. package/dist/debug-CjmE9sgG.js.map +1 -0
  17. package/dist/engine-DNCX0Hfd.js +2 -0
  18. package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
  19. package/dist/exec-DTO2oin0.js.map +1 -0
  20. package/dist/file-log-reporter-IFQcL5IK.js +158 -0
  21. package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
  22. package/dist/gate-discovery-CepmbZnc.js +635 -0
  23. package/dist/gate-discovery-CepmbZnc.js.map +1 -0
  24. package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
  25. package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
  26. package/dist/hook-mcp-client-DBvm9608.js +136 -0
  27. package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
  28. package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
  29. package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
  30. package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
  31. package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
  32. package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
  33. package/dist/internal-plan-review-ipnT3bhj.js +114 -0
  34. package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
  35. package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
  36. package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
  37. package/dist/internal-session-bind-DwK2iGW0.js +133 -0
  38. package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
  39. package/dist/internal-stop-hook-CNPGJm63.js +212 -0
  40. package/dist/internal-stop-hook-CNPGJm63.js.map +1 -0
  41. package/dist/internal-stop-review-AUcFssrW.js +122 -0
  42. package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
  43. package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
  44. package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
  45. package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
  46. package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
  47. package/dist/lifecycle-Byw5s1G8.js +134 -0
  48. package/dist/lifecycle-Byw5s1G8.js.map +1 -0
  49. package/dist/lifecycle-D80CaWie.js +2 -0
  50. package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
  51. package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
  52. package/dist/lifecycle-DzqCqDlM.js +2 -0
  53. package/dist/main.js +43 -12884
  54. package/dist/main.js.map +1 -1
  55. package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
  56. package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
  57. package/dist/models-B6UnGyRo.js +158 -0
  58. package/dist/models-B6UnGyRo.js.map +1 -0
  59. package/dist/orchestration-hRU4TdWr.js +39 -0
  60. package/dist/orchestration-hRU4TdWr.js.map +1 -0
  61. package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
  62. package/dist/paths-C1cLZ7Ga.js.map +1 -0
  63. package/dist/paths-CwKwc2pi.js +2 -0
  64. package/dist/{peer-mcp-personas-DJzLpfDJ.js → peer-mcp-personas-BDet588n.js} +1006 -2488
  65. package/dist/peer-mcp-personas-BDet588n.js.map +1 -0
  66. package/dist/plan-review-hook-C8x0uKPf.js +159 -0
  67. package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
  68. package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
  69. package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
  70. package/dist/provision-DSkPa5hY.js +656 -0
  71. package/dist/provision-DSkPa5hY.js.map +1 -0
  72. package/dist/serve-sa_7c-yD.js +1511 -0
  73. package/dist/serve-sa_7c-yD.js.map +1 -0
  74. package/dist/server-setup-DcO9qvN3.js +4776 -0
  75. package/dist/server-setup-DcO9qvN3.js.map +1 -0
  76. package/dist/start-siGxuZhO.js +129 -0
  77. package/dist/start-siGxuZhO.js.map +1 -0
  78. package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
  79. package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
  80. package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
  81. package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
  82. package/dist/token-R2ocFC3M.js +1054 -0
  83. package/dist/token-R2ocFC3M.js.map +1 -0
  84. package/dist/version-_Q1WpsQp.js +32 -0
  85. package/dist/version-_Q1WpsQp.js.map +1 -0
  86. package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
  87. package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
  88. package/package.json +1 -1
  89. package/dist/engine-BuuHUb4t.js +0 -2
  90. package/dist/lifecycle-0rXrwjWk.js +0 -2
  91. package/dist/lifecycle-DHZsVN8j.js.map +0 -1
  92. package/dist/lifecycle-hzPwXc3I.js +0 -2
  93. package/dist/paths-BjTMI_xK.js.map +0 -1
  94. package/dist/paths-D3XgmTcQ.js +0 -2
  95. package/dist/peer-mcp-personas-DJzLpfDJ.js.map +0 -1
@@ -0,0 +1,159 @@
1
+ import { r as parseBoolEnv } from "./exec-DTO2oin0.js";
2
+ import { a as isSubagentContext } from "./stop-gate-policy-CHwWbuF_.js";
3
+ import { n as callMcpTool } from "./hook-mcp-client-DBvm9608.js";
4
+ import { promises } from "node:fs";
5
+ import path from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ /** Cap the plan embedded in the critic brief so a huge plan file cannot dominate. */
8
+ const MAX_REVIEWED_PLAN_CHARS = 81920;
9
+ /** Default-on advisory plan review. Opt out with GH_ROUTER_DISABLE_PLAN_REVIEW=1. */
10
+ function planReviewEnabled(env = process.env) {
11
+ return parseBoolEnv(env.GH_ROUTER_DISABLE_PLAN_REVIEW) !== true;
12
+ }
13
+ /** File-backed per-session debounce for finalized plans, separate from Stop diff hashes. */
14
+ function filePlanReviewDebounce(stateDir) {
15
+ const fileFor = (sid) => path.join(stateDir, `plan-review-hash-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
16
+ const readLast = async (sid) => {
17
+ try {
18
+ return (await promises.readFile(fileFor(sid), "utf8")).trim();
19
+ } catch {
20
+ return "";
21
+ }
22
+ };
23
+ return {
24
+ async shouldReview(sid, planHash) {
25
+ if (planHash.length === 0) return false;
26
+ return await readLast(sid) !== planHash;
27
+ },
28
+ async markReviewed(sid, planHash) {
29
+ await promises.mkdir(stateDir, { recursive: true });
30
+ await promises.writeFile(fileFor(sid), planHash, { mode: 384 });
31
+ }
32
+ };
33
+ }
34
+ function parseObject(raw) {
35
+ try {
36
+ const parsed = JSON.parse(raw);
37
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
38
+ } catch {}
39
+ }
40
+ async function resolvePlanText(toolInput, readFile) {
41
+ if (!toolInput || typeof toolInput !== "object") return "";
42
+ const ti = toolInput;
43
+ const planFilePath = typeof ti.planFilePath === "string" && ti.planFilePath.trim().length > 0 ? ti.planFilePath.trim() : "";
44
+ if (planFilePath.length > 0) try {
45
+ const fromFile = await readFile(planFilePath);
46
+ if (fromFile.trim().length > 0) return fromFile;
47
+ } catch {}
48
+ return typeof ti.plan === "string" ? ti.plan : "";
49
+ }
50
+ /**
51
+ * Decide whether the ExitPlanMode PostToolUse hook should spawn a detached review.
52
+ * Pure aside from injected file reads + debounce store. Fail-open: any malformed
53
+ * payload, missing runtime, subagent context, trivial/absent plan, or store error
54
+ * returns a skip decision.
55
+ */
56
+ async function decidePlanReviewHook(input) {
57
+ try {
58
+ if (!input.runtimeAvailable) return {
59
+ kind: "skip",
60
+ reason: "missing-runtime"
61
+ };
62
+ const payload = parseObject(input.stdin);
63
+ if (!payload) return {
64
+ kind: "skip",
65
+ reason: "bad-payload"
66
+ };
67
+ if (isSubagentContext(payload)) return {
68
+ kind: "skip",
69
+ reason: "subagent"
70
+ };
71
+ const sessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : "";
72
+ if (!sessionId) return {
73
+ kind: "skip",
74
+ reason: "missing-session"
75
+ };
76
+ const cwd = typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd;
77
+ const trimmed = (await resolvePlanText(payload.tool_input, input.readFile ?? ((filePath) => promises.readFile(filePath, "utf8")))).trim();
78
+ if (trimmed.length < (input.minChars ?? 220)) return {
79
+ kind: "skip",
80
+ reason: "trivial-plan"
81
+ };
82
+ const planHash = createHash("sha256").update(trimmed).digest("hex");
83
+ if (!await input.debounce.shouldReview(sessionId, planHash)) return {
84
+ kind: "skip",
85
+ reason: "debounced"
86
+ };
87
+ await input.debounce.markReviewed(sessionId, planHash);
88
+ return {
89
+ kind: "spawn",
90
+ sessionId,
91
+ cwd,
92
+ plan: trimmed,
93
+ planHash
94
+ };
95
+ } catch {
96
+ return {
97
+ kind: "skip",
98
+ reason: "error"
99
+ };
100
+ }
101
+ }
102
+ function reviewBrief(plan, cwd) {
103
+ const embeddedPlan = plan.length > MAX_REVIEWED_PLAN_CHARS ? `${plan.slice(0, MAX_REVIEWED_PLAN_CHARS)}\n\n[plan truncated at ${MAX_REVIEWED_PLAN_CHARS} characters]` : plan;
104
+ return `You are an independent plan reviewer. A coding agent has just finalized a plan with ExitPlanMode. Your job is to find load-bearing risks before implementation starts: unstated assumptions, missing failure modes, gaps in verification, dangerous sequencing, or places where the plan could satisfy the letter of the request while missing the user's likely intent.
105
+
106
+ Be concise and skeptical. Do NOT rewrite the plan, do NOT praise it, and do NOT invent new product requirements. Report only material objections that would change what the agent should do next. Include file:line anchors when the plan names concrete files or when the risk depends on repo code. If the plan is sound, say exactly: "no material objection".
107
+
108
+ Workspace: ${cwd}\n\nFINALIZED PLAN:
109
+ ` + embeddedPlan;
110
+ }
111
+ function hasMaterialFinding(text) {
112
+ const normalized = text.trim().toLowerCase().replace(/[.!]+$/g, "");
113
+ return normalized.length > 0 && normalized !== "no material objection" && normalized !== "no material objections";
114
+ }
115
+ function framePlanFindings(text) {
116
+ return "PLAN REVIEW: an independent cross-lab critic reviewed the finalized plan and found the following material concern(s).\n" + text.trim();
117
+ }
118
+ /** Run the bounded critic call and write material findings to the shared findings store. */
119
+ async function runPlanReview(input) {
120
+ const timeoutMs = input.timeoutMs ?? 25e3;
121
+ let timer;
122
+ const controller = new AbortController();
123
+ try {
124
+ const brief = reviewBrief(input.plan, input.cwd);
125
+ const review = (input.callReview ?? ((prompt, signal) => callMcpTool({
126
+ runtime: input.runtime,
127
+ group: "peers",
128
+ tool: "codex_critic",
129
+ args: {
130
+ prompt,
131
+ effort: "high"
132
+ },
133
+ timeoutMs,
134
+ signal
135
+ })))(brief, controller.signal);
136
+ review.catch(() => {});
137
+ const raced = await Promise.race([review, new Promise((resolve) => {
138
+ timer = setTimeout(() => resolve("timeout"), timeoutMs);
139
+ })]);
140
+ if (raced === "timeout" || raced.isError || !hasMaterialFinding(raced.text)) return;
141
+ const framed = framePlanFindings(raced.text);
142
+ const existing = await input.findingsStore.read(input.sessionId).catch(() => null);
143
+ const next = existing && existing.trim().length > 0 ? `${existing.trim()}\n\n${framed}` : framed;
144
+ await input.findingsStore.write(input.sessionId, next);
145
+ } catch {} finally {
146
+ if (timer) clearTimeout(timer);
147
+ controller.abort();
148
+ }
149
+ }
150
+ /** Build the command registered for PostToolUse(ExitPlanMode). */
151
+ function buildPlanReviewHookCommand(execPath, scriptPath) {
152
+ const q = (s) => `"${s}"`;
153
+ if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-plan-review`;
154
+ return `${q(execPath)} internal-plan-review`;
155
+ }
156
+ //#endregion
157
+ export { runPlanReview as a, planReviewEnabled as i, decidePlanReviewHook as n, filePlanReviewDebounce as r, buildPlanReviewHookCommand as t };
158
+
159
+ //# sourceMappingURL=plan-review-hook-C8x0uKPf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-review-hook-C8x0uKPf.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\"\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(execPath: string, scriptPath: string | undefined): string {\n const q = (s: string): string => `\"${s}\"`\n if (scriptPath && scriptPath !== execPath) {\n return `${q(execPath)} ${q(scriptPath)} internal-plan-review`\n }\n return `${q(execPath)} internal-plan-review`\n}\n"],"mappings":"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"}
@@ -0,0 +1,153 @@
1
+ import { a as isSubagentContext } from "./stop-gate-policy-CHwWbuF_.js";
2
+ //#region src/lib/orchestration/prompt-submit-hook.ts
3
+ /**
4
+ * Pure decision logic for the `UserPromptSubmit` hook (the front-end of the
5
+ * floor-raising surface). It serves TWO duties on the one event:
6
+ *
7
+ * 1. Per-prompt budget reset — clears the Stop-gate's block counter for this
8
+ * session so `maxBlocks` is "per user prompt", not "per session lifetime"
9
+ * (the per-prompt scoping the Stop-gate relies on).
10
+ * 2. Goal steer — for a NON-trivial prompt, inject an advisory GOAL directive
11
+ * (additive context, never blocking) pointing the model at `/gh-research`
12
+ * then `/gh-orchestrate`. It raises the prior; it does not force execution.
13
+ *
14
+ * Top-level only: like the Stop-gate, it stands down in a subagent/teammate
15
+ * context (the payload carries `agent_type`/`agent_id` there) so it never
16
+ * recurses into spawned workers. Fail-open and never blocking: any error or a
17
+ * trivial prompt yields an empty injection.
18
+ */
19
+ /**
20
+ * The advisory goal injected for a non-trivial prompt. Uses the skills' slash
21
+ * invocation form. The model still decides whether to follow it; the Stop-gate
22
+ * backstops correctness at the output end.
23
+ */
24
+ const PROMPT_STEER_GOAL = "GOAL (advisory): for a non-trivial task, FIRST run /gh-research on this ask to information saturation — verify the load-bearing claims against the actual code before planning, and do not plan or write code until research is saturated. THEN, for an implementation or change task, run /gh-orchestrate to compose and run a floor-raising workflow (it checkpoints before expensive work). Skip both for a trivial ask; you may decline if they do not fit.";
25
+ /**
26
+ * Cheap, conservative complexity heuristic — a long prompt, an imperative
27
+ * build/change verb, or an explicit multi-file scope. Trivial prompts get no
28
+ * steer (no analysis-paralysis tax on quick asks).
29
+ */
30
+ function isNonTrivialPrompt(prompt) {
31
+ const p = prompt.trim();
32
+ if (p.length === 0) return false;
33
+ if (p.length >= 280) return true;
34
+ if (/\b(implement|build|refactor|migrate|fix|debug|diagnose|design|add|create|rewrite|optimi[sz]e|integrate|architect|investigate|audit)\b/i.test(p)) return true;
35
+ return /\b(across|throughout|every|all)\b.*\b(file|module|test|route|component)s?\b/i.test(p);
36
+ }
37
+ function decidePromptSubmit(input) {
38
+ let payload = {};
39
+ try {
40
+ const p = JSON.parse(input.stdin);
41
+ if (p && typeof p === "object") payload = p;
42
+ } catch {
43
+ return { inject: "" };
44
+ }
45
+ if (isSubagentContext(payload)) return { inject: "" };
46
+ const decision = { inject: "" };
47
+ const sessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : "";
48
+ if (sessionId) decision.resetSession = sessionId;
49
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
50
+ if (input.steerEnabled && isNonTrivialPrompt(prompt)) decision.inject = PROMPT_STEER_GOAL;
51
+ return decision;
52
+ }
53
+ /**
54
+ * Static encouragement injected only for a non-trivial prompt (no extra model
55
+ * call, no latency tax): nudge parallel lexical+semantic search before
56
+ * concluding. Mirrors the v1 advisory tone: additive, never blocking.
57
+ */
58
+ const PROMPT_SEARCH_TIP = "TIP (advisory): when this task needs code context, search lexical + semantic in parallel — one `mcp__search__code` call with mode:\"lexical\" and one with mode:\"semantic\", issued in the same turn — before concluding.";
59
+ /** System prompt for the single gpt-5.6-sol scope/goal inference. Steers a SHORT,
60
+ * user-derived (not invented) advisory note grounded in the search results. */
61
+ const PROMPT_SCOPE_SYSTEM = "You are a scoping assistant for a coding agent about to act on a user's request. You are given the user's request and the results of a lexical + semantic code search over the relevant repository. Produce a SHORT advisory note (<= 120 words), plain text only:\n1. SCOPE: one line — is this trivial, focused (one area), or large/cross-cutting — grounded in what the search surfaced (reference the most relevant file(s) by name).\n2. GOAL: restate the user's OWN ask as a single measurable objective, in THEIR terms. Do NOT invent new requirements or acceptance criteria beyond what they asked.\n3. Only if the task is large/cross-cutting, add a final line: \"Consider /gh-research first to saturate understanding, then /gh-orchestrate to compose a floor-raising workflow.\" Omit it for a focused or trivial task.\nThis is advisory — the agent decides whether to follow it. Be concrete and concise; no preamble.";
62
+ /** Max chars of each search-result blob fed into the scope inference. */
63
+ const SEARCH_CONTEXT_CAP = 6144;
64
+ /** Wrap the prior-turn review findings in an explicitly NON-AUTHORITATIVE frame. */
65
+ function framePendingFindings(findings) {
66
+ return "ADVISORY — independent review of your PREVIOUS change (NON-AUTHORITATIVE): an independent gpt-5.6-sol reviewer flagged the following. Evaluate each on its merits — fix the real ones, and ignore any wrong one with a one-line reason. You are NOT obligated to act on these.\n" + findings.trim();
67
+ }
68
+ function joinSections(sections) {
69
+ return sections.map((s) => s.trim()).filter((s) => s.length > 0).join("\n\n");
70
+ }
71
+ /**
72
+ * V2 decision: budget reset (via resetSession) + a grounded, user-derived scope
73
+ * note + surfaced prior-turn findings. ASYNC and IO-driven, but every IO is
74
+ * best-effort and the substantive enrichment is timeout-bounded with a fail-open
75
+ * to the v1 regex goal — so this never blocks and never wedges the prompt.
76
+ *
77
+ * - subagent/teammate -> empty (top-level only, like v1).
78
+ * - findings -> always surfaced (+ cleared) regardless of triviality.
79
+ * - trivial prompt -> findings only (no search tip, no model call).
80
+ * - substantive prompt -> static search tip + parallel lexical+semantic search -> ONE gpt-5.6-sol call
81
+ * -> grounded scope/goal note. Fail-open to PROMPT_STEER_GOAL.
82
+ * - steerEnabled=false -> findings only (no goal/tip).
83
+ */
84
+ async function decidePromptSubmitV2(input) {
85
+ let payload = {};
86
+ try {
87
+ const p = JSON.parse(input.stdin);
88
+ if (p && typeof p === "object") payload = p;
89
+ } catch {
90
+ return { inject: "" };
91
+ }
92
+ if (isSubagentContext(payload)) return { inject: "" };
93
+ const decision = { inject: "" };
94
+ const sessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : "";
95
+ if (sessionId) decision.resetSession = sessionId;
96
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
97
+ if (sessionId) await input.io.storePrompt(sessionId, prompt).catch(() => {});
98
+ let findingsBlock = "";
99
+ if (sessionId) {
100
+ const pending = await input.io.readFindings(sessionId).catch(() => null);
101
+ if (pending && pending.trim().length > 0) {
102
+ findingsBlock = framePendingFindings(pending);
103
+ await input.io.clearFindings(sessionId).catch(() => {});
104
+ }
105
+ }
106
+ if (!input.steerEnabled) {
107
+ decision.inject = findingsBlock;
108
+ return decision;
109
+ }
110
+ if (!isNonTrivialPrompt(prompt)) {
111
+ decision.inject = findingsBlock;
112
+ return decision;
113
+ }
114
+ const timeoutMs = input.io.timeoutMs ?? 22e3;
115
+ let goal = PROMPT_STEER_GOAL;
116
+ let timer;
117
+ const controller = new AbortController();
118
+ try {
119
+ const enrich = (async () => {
120
+ const [lexical, semantic] = await Promise.all([input.io.searchCode(prompt, "lexical", controller.signal).catch(() => ""), input.io.searchCode(prompt, "semantic", controller.signal).catch(() => "")]);
121
+ const searchContext = `Lexical search results:\n${lexical.slice(0, SEARCH_CONTEXT_CAP)}\n\nSemantic search results:\n${semantic.slice(0, SEARCH_CONTEXT_CAP)}`;
122
+ return (await input.io.infer(PROMPT_SCOPE_SYSTEM, `USER REQUEST:\n${prompt}\n\n${searchContext}`, controller.signal)).trim();
123
+ })();
124
+ enrich.catch(() => {});
125
+ const raced = await Promise.race([enrich, new Promise((resolve) => {
126
+ timer = setTimeout(() => resolve("__timeout__"), timeoutMs);
127
+ })]);
128
+ if (raced !== "__timeout__" && raced.length > 0) goal = raced;
129
+ } catch {} finally {
130
+ if (timer) clearTimeout(timer);
131
+ controller.abort();
132
+ }
133
+ decision.inject = joinSections([
134
+ PROMPT_SEARCH_TIP,
135
+ goal,
136
+ findingsBlock
137
+ ]);
138
+ return decision;
139
+ }
140
+ /**
141
+ * Build the shell command Claude Code runs for the `UserPromptSubmit` hook —
142
+ * the running github-router via its node/bun binary so it works regardless of
143
+ * PATH. Mirrors `buildStopHookCommand`.
144
+ */
145
+ function buildPromptSubmitHookCommand(execPath, scriptPath) {
146
+ const q = (s) => `"${s}"`;
147
+ if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-prompt-submit`;
148
+ return `${q(execPath)} internal-prompt-submit`;
149
+ }
150
+ //#endregion
151
+ export { decidePromptSubmit as n, decidePromptSubmitV2 as r, buildPromptSubmitHookCommand as t };
152
+
153
+ //# sourceMappingURL=prompt-submit-hook-BrkkLAAB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-submit-hook-BrkkLAAB.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 { 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/**\n * Build the shell command Claude Code runs for the `UserPromptSubmit` hook —\n * the running github-router via its node/bun binary so it works regardless of\n * PATH. Mirrors `buildStopHookCommand`.\n */\nexport function buildPromptSubmitHookCommand(execPath: string, scriptPath: string | undefined): string {\n const q = (s: string): string => `\"${s}\"`\n if (scriptPath && scriptPath !== execPath) {\n return `${q(execPath)} ${q(scriptPath)} internal-prompt-submit`\n }\n return `${q(execPath)} internal-prompt-submit`\n}\n"],"mappings":"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"}