github-router 0.3.302 → 0.3.304

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 (46) hide show
  1. package/README.md +4 -4
  2. package/dist/{attribution-settings-B1xutIts.js → attribution-settings-C8iSwEAZ.js} +184 -93
  3. package/dist/attribution-settings-C8iSwEAZ.js.map +1 -0
  4. package/dist/browser-ext/manifest.json +1 -1
  5. package/dist/{claude-uRuzEH0W.js → claude-Drv0roq1.js} +63 -116
  6. package/dist/claude-Drv0roq1.js.map +1 -0
  7. package/dist/{codex-D5ag-liR.js → codex-kgVL5OyN.js} +4 -4
  8. package/dist/{codex-D5ag-liR.js.map → codex-kgVL5OyN.js.map} +1 -1
  9. package/dist/engine-xuIzQNV1.js +2 -0
  10. package/dist/{fast-profile-contract-K3P58nj3.js → fast-profile-contract-OuZQaGif.js} +26 -23
  11. package/dist/fast-profile-contract-OuZQaGif.js.map +1 -0
  12. package/dist/{gate-discovery-BTRVyKWu.js → gate-discovery-D4-F99e_.js} +2 -2
  13. package/dist/{gate-discovery-BTRVyKWu.js.map → gate-discovery-D4-F99e_.js.map} +1 -1
  14. package/dist/hooks.mjs +121 -50
  15. package/dist/hooks.sha256 +1 -1
  16. package/dist/{internal-fast-dispatch-guard-D2HcfXZv.js → internal-fast-dispatch-guard-BaUB-F8o.js} +71 -15
  17. package/dist/internal-fast-dispatch-guard-BaUB-F8o.js.map +1 -0
  18. package/dist/{internal-fast-dispatch-guard-CYOWbxjX.js → internal-fast-dispatch-guard-ByzwIsxA.js} +1 -1
  19. package/dist/{internal-max-dispatch-guard-BjSVXMvC.js → internal-max-dispatch-guard-CXRB-4a3.js} +31 -14
  20. package/dist/internal-max-dispatch-guard-CXRB-4a3.js.map +1 -0
  21. package/dist/{internal-max-dispatch-guard-Bcjavapx.js → internal-max-dispatch-guard-Sb6ACr_0.js} +1 -1
  22. package/dist/{internal-stop-hook-BZg__ab6.js → internal-stop-hook-D8LIWryj.js} +2 -2
  23. package/dist/{internal-stop-hook-BZg__ab6.js.map → internal-stop-hook-D8LIWryj.js.map} +1 -1
  24. package/dist/main.js +7 -7
  25. package/dist/{max-profile-contract-BTXowh6k.js → max-profile-contract-CiD5wyET.js} +19 -7
  26. package/dist/max-profile-contract-CiD5wyET.js.map +1 -0
  27. package/dist/{peer-mcp-personas-DwEiYeJS.js → peer-mcp-personas-D4bHdjOx.js} +420 -384
  28. package/dist/peer-mcp-personas-D4bHdjOx.js.map +1 -0
  29. package/dist/{provision-BLT1pCFy.js → provision-D3_GtXze.js} +2 -2
  30. package/dist/{provision-BLT1pCFy.js.map → provision-D3_GtXze.js.map} +1 -1
  31. package/dist/{serve-wEZoOijw.js → serve-DPRZX-2j.js} +5 -5
  32. package/dist/{serve-wEZoOijw.js.map → serve-DPRZX-2j.js.map} +1 -1
  33. package/dist/{server-setup-Zn5wkosb.js → server-setup-CSkdFAPP.js} +25 -19
  34. package/dist/server-setup-CSkdFAPP.js.map +1 -0
  35. package/dist/{start-DSqU8d8h.js → start-C6G5tu81.js} +3 -3
  36. package/dist/{start-DSqU8d8h.js.map → start-C6G5tu81.js.map} +1 -1
  37. package/package.json +1 -1
  38. package/dist/attribution-settings-B1xutIts.js.map +0 -1
  39. package/dist/claude-uRuzEH0W.js.map +0 -1
  40. package/dist/engine-2uMJ4Isk.js +0 -2
  41. package/dist/fast-profile-contract-K3P58nj3.js.map +0 -1
  42. package/dist/internal-fast-dispatch-guard-D2HcfXZv.js.map +0 -1
  43. package/dist/internal-max-dispatch-guard-BjSVXMvC.js.map +0 -1
  44. package/dist/max-profile-contract-BTXowh6k.js.map +0 -1
  45. package/dist/peer-mcp-personas-DwEiYeJS.js.map +0 -1
  46. package/dist/server-setup-Zn5wkosb.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { i as FAST_PROFILE_DELEGATION_GRAPH, o as FAST_PROFILE_NATIVE_AGENT_NAMES } from "./fast-profile-contract-K3P58nj3.js";
1
+ import { i as FAST_PROFILE_DELEGATION_GRAPH, o as FAST_PROFILE_NATIVE_AGENT_NAMES } from "./fast-profile-contract-OuZQaGif.js";
2
2
  import { t as buildSelfCommand } from "./self-invocation-BJWbA8ka.js";
3
3
  import { defineCommand } from "citty";
4
4
  import { readFileSync } from "node:fs";
@@ -11,15 +11,17 @@ import { readFileSync } from "node:fs";
11
11
  * It is an in-session Task/Agent ACL, not a shell sandbox: role tool lists
12
12
  * narrow the available tools while this graph narrows native dispatch edges.
13
13
  */
14
+ const FAST_BROWSE_DISPATCH_AGENT = "worker-browse";
14
15
  const FAST_NATIVE_AGENT_NAMES = FAST_PROFILE_NATIVE_AGENT_NAMES;
15
- const FAST_NATIVE_AGENT_SET = new Set(FAST_NATIVE_AGENT_NAMES);
16
+ new Set(FAST_NATIVE_AGENT_NAMES);
16
17
  /** Frozen authority graph: each key is a caller and each value its targets. */
17
18
  const FAST_DISPATCH_GRAPH = Object.freeze({
18
19
  Explore: new Set(FAST_PROFILE_DELEGATION_GRAPH.Explore),
20
+ Plan: new Set(FAST_PROFILE_DELEGATION_GRAPH.Plan),
21
+ "general-purpose": new Set(FAST_PROFILE_DELEGATION_GRAPH["general-purpose"]),
19
22
  implementer: new Set(FAST_PROFILE_DELEGATION_GRAPH.implementer),
20
23
  reviewer: new Set(FAST_PROFILE_DELEGATION_GRAPH.reviewer),
21
- planner: new Set(FAST_PROFILE_DELEGATION_GRAPH.planner),
22
- critic: new Set(FAST_PROFILE_DELEGATION_GRAPH.critic)
24
+ "worker-browse": /* @__PURE__ */ new Set([])
23
25
  });
24
26
  /** The hook matcher is intentionally limited to native dispatch tool names. */
25
27
  const FAST_DISPATCH_TOOL_MATCHER = "^(Task|Agent)$";
@@ -69,13 +71,41 @@ function deny(reason, target, caller) {
69
71
  };
70
72
  }
71
73
  /**
74
+ * Continuation / reattachment target selectors that must be rejected on Fast dispatch.
75
+ * Any non-null value in tool_input fails closed to prevent bypassing role edges.
76
+ */
77
+ const CONTINUATION_FIELDS = [
78
+ "resume",
79
+ "resume_session",
80
+ "resumeSession",
81
+ "resume_agent_id",
82
+ "resumeAgentId",
83
+ "continuation",
84
+ "continue",
85
+ "continue_session",
86
+ "continueSession",
87
+ "agent_id",
88
+ "agentId",
89
+ "session_id",
90
+ "sessionId",
91
+ "thread_id",
92
+ "threadId",
93
+ "task_id",
94
+ "taskId",
95
+ "target_agent_id",
96
+ "targetAgentId",
97
+ "reattach",
98
+ "reattach_session",
99
+ "reattachSession"
100
+ ];
101
+ /**
72
102
  * Evaluate one raw Claude Code PreToolUse payload.
73
103
  *
74
104
  * Classification happens before strict parsing. A malformed or unrelated
75
105
  * ordinary-tool hook therefore passes through, while a recognized Task/Agent
76
106
  * dispatch fails closed on malformed input or identity.
77
107
  */
78
- function decideFastDispatchGuard(stdin) {
108
+ function decideFastDispatchGuard(stdin, opts) {
79
109
  let parsed;
80
110
  try {
81
111
  parsed = typeof stdin === "string" ? JSON.parse(stdin) : stdin;
@@ -91,13 +121,19 @@ function decideFastDispatchGuard(stdin) {
91
121
  verdict: "allow-non-dispatch"
92
122
  };
93
123
  if (!payload.tool_input || typeof payload.tool_input !== "object" || Array.isArray(payload.tool_input)) return deny(`fast dispatch denied: ${toolName} payload has malformed tool_input`);
94
- const targetResult = resolveAliasedString(payload.tool_input, ["subagent_type", "subagentType"]);
124
+ const toolInput = payload.tool_input;
125
+ for (const field of CONTINUATION_FIELDS) if (hasOwn(toolInput, field)) {
126
+ const val = toolInput[field];
127
+ if (val !== void 0 && val !== null && val !== false) return deny(`fast dispatch denied: ${toolName} continuation/reattachment (${field}) is not permitted in fast profile`);
128
+ }
129
+ const targetResult = resolveAliasedString(toolInput, ["subagent_type", "subagentType"]);
95
130
  if (targetResult.malformed) return deny(`fast dispatch denied: ${toolName} target identity is malformed`);
96
131
  if (targetResult.conflict) return deny(`fast dispatch denied: ${toolName} target aliases conflict`);
97
132
  const targetValue = targetResult.value;
98
- if (!targetValue || !FAST_NATIVE_AGENT_SET.has(targetValue)) return deny(`fast dispatch denied: ${toolName} target must be one of ${FAST_NATIVE_AGENT_NAMES.join(", ")}`);
133
+ const effectiveAllowedTargets = opts?.allowedTargets ? new Set(opts.allowedTargets) : /* @__PURE__ */ new Set([...FAST_NATIVE_AGENT_NAMES, ...opts?.allowBrowse ? [FAST_BROWSE_DISPATCH_AGENT] : []]);
134
+ if (!targetValue || !effectiveAllowedTargets.has(targetValue)) return deny(`fast dispatch denied: ${toolName} target must be one of ${[...effectiveAllowedTargets].join(", ")}`);
99
135
  const target = targetValue;
100
- const updatedInput = { ...payload.tool_input };
136
+ const updatedInput = { ...toolInput };
101
137
  delete updatedInput.model;
102
138
  const callerType = resolveAliasedString(payload, ["agent_type", "agentType"]);
103
139
  const callerId = resolveAliasedString(payload, ["agent_id", "agentId"]);
@@ -125,9 +161,9 @@ function decideFastDispatchGuard(stdin) {
125
161
  verdict: "allow"
126
162
  };
127
163
  }
128
- if (!FAST_NATIVE_AGENT_SET.has(callerTypeValue)) return deny(`fast dispatch denied: unknown caller role ${JSON.stringify(callerTypeValue)}`, target);
164
+ if (!(/* @__PURE__ */ new Set([...FAST_NATIVE_AGENT_NAMES, "worker-browse"])).has(callerTypeValue)) return deny(`fast dispatch denied: unknown caller role ${JSON.stringify(callerTypeValue)}`, target);
129
165
  const caller = callerTypeValue;
130
- if (!FAST_DISPATCH_GRAPH[caller].has(target)) return deny(`fast dispatch denied: ${caller} cannot invoke ${target}`, target, caller);
166
+ if (!(FAST_DISPATCH_GRAPH[caller] ?? /* @__PURE__ */ new Set()).has(target)) return deny(`fast dispatch denied: ${caller} cannot invoke ${target}`, target, caller);
131
167
  return {
132
168
  allowed: true,
133
169
  caller,
@@ -174,18 +210,38 @@ const internalFastDispatchGuard = defineCommand({
174
210
  name: "internal-fast-dispatch-guard",
175
211
  description: "Internal: PreToolUse guard enforcing the fast-profile native Task/Agent ACL."
176
212
  },
177
- run() {
178
- const decision = decideFastDispatchGuard(readStdinSync());
213
+ args: {
214
+ allowBrowse: {
215
+ type: "boolean",
216
+ description: "Whether worker-browse is allowed as a target.",
217
+ default: false
218
+ },
219
+ allowedTargets: {
220
+ type: "string",
221
+ description: "Comma-separated list of allowed target subagents."
222
+ }
223
+ },
224
+ run({ args }) {
225
+ const allowedTargets = args.allowedTargets ? args.allowedTargets.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
226
+ const allowBrowse = args.allowBrowse === true || (allowedTargets ? allowedTargets.includes("worker-browse") : false);
227
+ const decision = decideFastDispatchGuard(readStdinSync(), {
228
+ allowedTargets,
229
+ allowBrowse
230
+ });
179
231
  if (!decision.allowed && decision.reason) process.stdout.write(fastDispatchDenyOutput(decision.reason));
180
232
  else if (decision.verdict === "allow" && decision.updatedInput) process.stdout.write(fastDispatchAllowOutput(decision.updatedInput));
181
233
  process.exitCode = 0;
182
234
  }
183
235
  });
184
236
  /** Build the persisted command for the fast-profile PreToolUse hook. */
185
- function buildFastDispatchGuardHookCommand(invocation) {
186
- return buildSelfCommand(invocation, "internal-fast-dispatch-guard");
237
+ function buildFastDispatchGuardHookCommand(invocation, opts) {
238
+ const flags = [];
239
+ if (opts?.allowBrowse) flags.push("--allowBrowse");
240
+ if (opts?.allowedTargets && opts.allowedTargets.length > 0) flags.push(`--allowedTargets "${opts.allowedTargets.join(",")}"`);
241
+ const args = ["internal-fast-dispatch-guard", ...flags].join(" ");
242
+ return buildSelfCommand(invocation, args);
187
243
  }
188
244
  //#endregion
189
245
  export { internalFastDispatchGuard as n, FAST_DISPATCH_TOOL_MATCHER as r, buildFastDispatchGuardHookCommand as t };
190
246
 
191
- //# sourceMappingURL=internal-fast-dispatch-guard-D2HcfXZv.js.map
247
+ //# sourceMappingURL=internal-fast-dispatch-guard-BaUB-F8o.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal-fast-dispatch-guard-BaUB-F8o.js","names":[],"sources":["../src/lib/fast-dispatch-acl.ts","../src/internal-fast-dispatch-guard.ts"],"sourcesContent":["/**\n * Pure policy for the fast profile's native Task/Agent delegation graph.\n *\n * This policy is compiled into the hook rather than read from per-launch files,\n * so cleanup or a partial runtime-file write cannot silently disarm the guard.\n * It is an in-session Task/Agent ACL, not a shell sandbox: role tool lists\n * narrow the available tools while this graph narrows native dispatch edges.\n */\n\nimport {\n FAST_PROFILE_DELEGATION_GRAPH,\n FAST_PROFILE_NATIVE_AGENT_NAMES,\n type FastProfileNativeAgentName,\n} from \"./fast-profile-contract\"\n\nexport const FAST_BROWSE_DISPATCH_AGENT = \"worker-browse\" as const\n\nexport const FAST_NATIVE_AGENT_NAMES = FAST_PROFILE_NATIVE_AGENT_NAMES\n\nexport type FastNativeAgentName = FastProfileNativeAgentName\n\nexport type FastDispatchTargetName = FastNativeAgentName | typeof FAST_BROWSE_DISPATCH_AGENT\nexport type FastDispatchCallerName = FastNativeAgentName | typeof FAST_BROWSE_DISPATCH_AGENT\n\nexport const FAST_NATIVE_AGENT_SET = new Set<string>(FAST_NATIVE_AGENT_NAMES)\n\n/** Frozen authority graph: each key is a caller and each value its targets. */\nexport const FAST_DISPATCH_GRAPH: Readonly<\n Record<FastDispatchCallerName, ReadonlySet<FastDispatchTargetName>>\n> = Object.freeze({\n Explore: new Set<FastDispatchTargetName>(FAST_PROFILE_DELEGATION_GRAPH.Explore),\n Plan: new Set<FastDispatchTargetName>(FAST_PROFILE_DELEGATION_GRAPH.Plan),\n \"general-purpose\": new Set<FastDispatchTargetName>(\n FAST_PROFILE_DELEGATION_GRAPH[\"general-purpose\"],\n ),\n implementer: new Set<FastDispatchTargetName>(\n FAST_PROFILE_DELEGATION_GRAPH.implementer,\n ),\n reviewer: new Set<FastDispatchTargetName>(FAST_PROFILE_DELEGATION_GRAPH.reviewer),\n \"worker-browse\": new Set<FastDispatchTargetName>([]),\n})\n\n/** The hook matcher is intentionally limited to native dispatch tool names. */\nexport const FAST_DISPATCH_TOOL_MATCHER = \"^(Task|Agent)$\"\n\ninterface DispatchPayload {\n tool_name?: unknown\n tool_input?: unknown\n agent_type?: unknown\n agent_id?: unknown\n parent_tool_use_id?: unknown\n agentType?: unknown\n agentId?: unknown\n parentToolUseId?: unknown\n}\n\nexport interface FastDispatchGuardOptions {\n /** Optional restriction on allowed target subagents (e.g. from launch profile or hook CLI). */\n allowedTargets?: ReadonlySet<string> | ReadonlyArray<string>\n /** Whether worker-browse is permitted as a target for the lead. */\n allowBrowse?: boolean\n}\n\nexport interface FastDispatchGuardResult {\n allowed: boolean\n reason?: string\n caller?: FastDispatchCallerName | \"lead\"\n target?: FastDispatchTargetName\n /** Cloned Agent/Task input with a caller model override removed. */\n updatedInput?: Record<string, unknown>\n verdict: \"allow-non-dispatch\" | \"allow\" | \"deny\"\n}\n\nfunction hasOwn(obj: object, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(obj, key)\n}\n\nfunction valuesForAliases(obj: Record<string, unknown>, names: ReadonlyArray<string>): Array<unknown> {\n return names.filter((name) => hasOwn(obj, name)).map((name) => obj[name])\n}\n\n/**\n * Resolve an aliased string field. Null and undefined are marker values. One\n * concrete value is accepted, while different concrete aliases are a conflict.\n */\nfunction resolveAliasedString(\n obj: Record<string, unknown>,\n names: ReadonlyArray<string>,\n): { present: boolean; value?: string; malformed: boolean; conflict: boolean } {\n const values = valuesForAliases(obj, names)\n if (values.length === 0) return { present: false, malformed: false, conflict: false }\n const concrete = values.filter((value) => value !== undefined && value !== null)\n if (concrete.some((value) => typeof value !== \"string\" || value.trim().length === 0)) {\n return { present: true, malformed: true, conflict: false }\n }\n const distinct = [...new Set((concrete as Array<string>).map((value) => value.trim()))]\n if (distinct.length > 1) return { present: true, malformed: false, conflict: true }\n return { present: true, value: distinct[0], malformed: false, conflict: false }\n}\n\nfunction deny(\n reason: string,\n target?: FastDispatchTargetName,\n caller?: FastDispatchCallerName | \"lead\",\n): FastDispatchGuardResult {\n return { allowed: false, reason, target, caller, verdict: \"deny\" }\n}\n\n/**\n * Continuation / reattachment target selectors that must be rejected on Fast dispatch.\n * Any non-null value in tool_input fails closed to prevent bypassing role edges.\n */\nconst CONTINUATION_FIELDS: ReadonlyArray<string> = [\n \"resume\",\n \"resume_session\",\n \"resumeSession\",\n \"resume_agent_id\",\n \"resumeAgentId\",\n \"continuation\",\n \"continue\",\n \"continue_session\",\n \"continueSession\",\n \"agent_id\",\n \"agentId\",\n \"session_id\",\n \"sessionId\",\n \"thread_id\",\n \"threadId\",\n \"task_id\",\n \"taskId\",\n \"target_agent_id\",\n \"targetAgentId\",\n \"reattach\",\n \"reattach_session\",\n \"reattachSession\",\n]\n\n/**\n * Evaluate one raw Claude Code PreToolUse payload.\n *\n * Classification happens before strict parsing. A malformed or unrelated\n * ordinary-tool hook therefore passes through, while a recognized Task/Agent\n * dispatch fails closed on malformed input or identity.\n */\nexport function decideFastDispatchGuard(\n stdin: string | unknown,\n opts?: FastDispatchGuardOptions,\n): FastDispatchGuardResult {\n let parsed: unknown\n try {\n parsed = typeof stdin === \"string\" ? JSON.parse(stdin) : stdin\n } catch {\n return deny(\"fast dispatch denied: malformed hook payload\")\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return deny(\"fast dispatch denied: hook payload must be an object\")\n }\n\n const payload = parsed as DispatchPayload\n if (typeof payload.tool_name !== \"string\") {\n return deny(\"fast dispatch denied: hook payload has no tool_name\")\n }\n const toolName = payload.tool_name\n if (!/^(Task|Agent)$/.test(toolName)) {\n return { allowed: true, verdict: \"allow-non-dispatch\" }\n }\n\n if (!payload.tool_input || typeof payload.tool_input !== \"object\" || Array.isArray(payload.tool_input)) {\n return deny(`fast dispatch denied: ${toolName} payload has malformed tool_input`)\n }\n const toolInput = payload.tool_input as Record<string, unknown>\n\n // Reject non-null continuation / reattachment / agent-id target selectors that could bypass role edges\n for (const field of CONTINUATION_FIELDS) {\n if (hasOwn(toolInput, field)) {\n const val = toolInput[field]\n if (val !== undefined && val !== null && val !== false) {\n return deny(`fast dispatch denied: ${toolName} continuation/reattachment (${field}) is not permitted in fast profile`)\n }\n }\n }\n\n const targetResult = resolveAliasedString(\n toolInput,\n [\"subagent_type\", \"subagentType\"],\n )\n if (targetResult.malformed) {\n return deny(`fast dispatch denied: ${toolName} target identity is malformed`)\n }\n if (targetResult.conflict) {\n return deny(`fast dispatch denied: ${toolName} target aliases conflict`)\n }\n const targetValue = targetResult.value\n\n const effectiveAllowedTargets = opts?.allowedTargets\n ? new Set(opts.allowedTargets)\n : new Set<string>([\n ...FAST_NATIVE_AGENT_NAMES,\n ...(opts?.allowBrowse ? [FAST_BROWSE_DISPATCH_AGENT] : []),\n ])\n\n if (!targetValue || !effectiveAllowedTargets.has(targetValue)) {\n return deny(\n `fast dispatch denied: ${toolName} target must be one of ${[...effectiveAllowedTargets].join(\", \")}`,\n )\n }\n const target = targetValue as FastDispatchTargetName\n\n // Invocation-level `model` outranks custom-agent frontmatter in Claude Code.\n // The fast profile promises fixed role models, so allowed dispatches must\n // remove that one caller-owned field while preserving every other input.\n const updatedInput = { ...toolInput }\n delete updatedInput.model\n\n const callerType = resolveAliasedString(payload as Record<string, unknown>, [\"agent_type\", \"agentType\"])\n const callerId = resolveAliasedString(payload as Record<string, unknown>, [\"agent_id\", \"agentId\"])\n const parentMarker = resolveAliasedString(payload as Record<string, unknown>, [\n \"parent_tool_use_id\",\n \"parentToolUseId\",\n ])\n if (callerType.malformed || callerId.malformed || parentMarker.malformed) {\n return deny(`fast dispatch denied: ${toolName} caller identity is malformed`, target)\n }\n if (callerType.conflict || callerId.conflict || parentMarker.conflict) {\n return deny(`fast dispatch denied: ${toolName} caller identity aliases conflict`, target)\n }\n\n const callerTypeValue = callerType.value\n const callerIdValue = callerId.value\n const parentValue = parentMarker.value\n\n // A non-null agent id or parent marker cannot impersonate a role by itself.\n // A valid role may be accompanied by either marker as supplemental evidence.\n if (!callerTypeValue && callerIdValue) {\n return deny(`fast dispatch denied: ${toolName} has agent_id without agent_type`, target)\n }\n if (!callerTypeValue && parentValue) {\n return deny(`fast dispatch denied: ${toolName} has only a parent execution marker`, target)\n }\n\n // Absent or null identity fields are the only lead representation. Empty\n // strings and non-string values were rejected above; unknown roles fail closed.\n if (!callerTypeValue) {\n const hasPresentMarker = callerType.present || callerId.present || parentMarker.present\n const hasUnresolvedNonNullMarker = [callerType, callerId, parentMarker].some(\n (marker) => marker.value !== undefined,\n )\n if (hasPresentMarker && hasUnresolvedNonNullMarker) {\n return deny(`fast dispatch denied: ${toolName} has no resolvable caller role`, target)\n }\n return {\n allowed: true,\n caller: \"lead\",\n target,\n updatedInput,\n verdict: \"allow\",\n }\n }\n\n const validCallers = new Set<string>([\n ...FAST_NATIVE_AGENT_NAMES,\n FAST_BROWSE_DISPATCH_AGENT,\n ])\n\n if (!validCallers.has(callerTypeValue)) {\n return deny(`fast dispatch denied: unknown caller role ${JSON.stringify(callerTypeValue)}`, target)\n }\n const caller = callerTypeValue as FastDispatchCallerName\n const allowedTargets = FAST_DISPATCH_GRAPH[caller] ?? new Set<FastDispatchTargetName>()\n if (!allowedTargets.has(target)) {\n return deny(`fast dispatch denied: ${caller} cannot invoke ${target}`, target, caller)\n }\n return { allowed: true, caller, target, updatedInput, verdict: \"allow\" }\n}\n\n/** JSON response consumed by Claude Code for an allowed, model-pinned dispatch. */\nexport function fastDispatchAllowOutput(\n updatedInput: Record<string, unknown>,\n): string {\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"allow\",\n updatedInput,\n },\n })\n}\n\n/** JSON response consumed by Claude Code's PreToolUse hook protocol. */\nexport function fastDispatchDenyOutput(reason: string): string {\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: reason,\n },\n })\n}\n","/**\n * Internal PreToolUse guard for the `-m fast` native Task/Agent ACL.\n *\n * The policy is intentionally pure and compiled into this hook. This entrypoint\n * only performs synchronous stdin I/O, emits Claude Code's deny envelope, and\n * exits naturally so Windows libuv teardown is not raced by process.exit().\n */\n\nimport { defineCommand } from \"citty\"\nimport { readFileSync } from \"node:fs\"\n\nimport {\n decideFastDispatchGuard,\n fastDispatchAllowOutput,\n fastDispatchDenyOutput,\n} from \"./lib/fast-dispatch-acl\"\nimport { buildSelfCommand, type SelfInvocation } from \"./lib/hook-launcher/self-invocation\"\n\nfunction readStdinSync(): string {\n try {\n if (process.stdin.isTTY) return \"\"\n return readFileSync(0, \"utf8\")\n } catch {\n return \"\"\n }\n}\n\nexport const internalFastDispatchGuard = defineCommand({\n meta: {\n name: \"internal-fast-dispatch-guard\",\n description:\n \"Internal: PreToolUse guard enforcing the fast-profile native Task/Agent ACL.\",\n },\n args: {\n allowBrowse: {\n type: \"boolean\",\n description: \"Whether worker-browse is allowed as a target.\",\n default: false,\n },\n allowedTargets: {\n type: \"string\",\n description: \"Comma-separated list of allowed target subagents.\",\n },\n },\n run({ args }) {\n const allowedTargets = args.allowedTargets\n ? args.allowedTargets.split(\",\").map((s) => s.trim()).filter(Boolean)\n : undefined\n const allowBrowse = args.allowBrowse === true || (allowedTargets ? allowedTargets.includes(\"worker-browse\") : false)\n const decision = decideFastDispatchGuard(readStdinSync(), {\n allowedTargets,\n allowBrowse,\n })\n if (!decision.allowed && decision.reason) {\n process.stdout.write(fastDispatchDenyOutput(decision.reason))\n } else if (decision.verdict === \"allow\" && decision.updatedInput) {\n process.stdout.write(fastDispatchAllowOutput(decision.updatedInput))\n }\n // Natural exit is required on Windows. Claude Code consumes the JSON\n // permission decision; an exit code is not used to deny this hook.\n process.exitCode = 0\n },\n})\n\n/** Build the persisted command for the fast-profile PreToolUse hook. */\nexport function buildFastDispatchGuardHookCommand(\n invocation: SelfInvocation,\n opts?: { allowBrowse?: boolean; allowedTargets?: ReadonlyArray<string> },\n): string {\n const flags: string[] = []\n if (opts?.allowBrowse) flags.push(\"--allowBrowse\")\n if (opts?.allowedTargets && opts.allowedTargets.length > 0) {\n flags.push(`--allowedTargets \"${opts.allowedTargets.join(\",\")}\"`)\n }\n const args = [\"internal-fast-dispatch-guard\", ...flags].join(\" \")\n return buildSelfCommand(invocation, args)\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { n as internalFastDispatchGuard } from "./internal-fast-dispatch-guard-D2HcfXZv.js";
1
+ import { n as internalFastDispatchGuard } from "./internal-fast-dispatch-guard-BaUB-F8o.js";
2
2
  export { internalFastDispatchGuard };
@@ -1,5 +1,5 @@
1
+ import { a as MAX_PROFILE_NATIVE_AGENT_NAMES, s as MAX_PROFILE_NATIVE_MODELS } from "./max-profile-contract-CiD5wyET.js";
1
2
  import { t as buildSelfCommand } from "./self-invocation-BJWbA8ka.js";
2
- import { a as MAX_PROFILE_NATIVE_AGENT_NAMES, s as MAX_PROFILE_NATIVE_MODELS } from "./max-profile-contract-BTXowh6k.js";
3
3
  import { defineCommand } from "citty";
4
4
  import { readFileSync } from "node:fs";
5
5
  //#region src/lib/max-dispatch-acl.ts
@@ -18,7 +18,7 @@ function assertMaxDispatchGuardInstalled(maxProfile, installationSucceeded) {
18
18
  }
19
19
  const MAX_ALLOWED_MODEL_BASES = /* @__PURE__ */ new Set([
20
20
  "gpt-5.6-luna",
21
- "gemini-3.7-flash",
21
+ "gemini-3.8-flash",
22
22
  "grok-4.6"
23
23
  ]);
24
24
  /**
@@ -69,7 +69,7 @@ function normalizeMaxDispatchEffort(model, effort) {
69
69
  "high",
70
70
  "xhigh",
71
71
  "max"
72
- ] : model === "grok-4.6" || model === "gemini-3.7-flash" ? [
72
+ ] : model === "grok-4.6" || model === "gemini-3.8-flash" ? [
73
73
  "low",
74
74
  "medium",
75
75
  "high"
@@ -82,7 +82,7 @@ function normalizeMaxDispatchEffort(model, effort) {
82
82
  * live-ladder effort. Sol/Codex/unknown/private ids are rejected. The lead may
83
83
  * target any emitted max role; role-to-role edges are unrestricted here.
84
84
  */
85
- function decideMaxDispatchGuard(stdin) {
85
+ function decideMaxDispatchGuard(stdin, opts = {}) {
86
86
  let parsed;
87
87
  try {
88
88
  parsed = typeof stdin === "string" ? JSON.parse(stdin) : stdin;
@@ -126,29 +126,30 @@ function decideMaxDispatchGuard(stdin) {
126
126
  const normalizedModel = modelValue === void 0 || schemaAlias ? void 0 : normalizeMaxDispatchModel(modelValue);
127
127
  if (modelValue !== void 0 && !schemaAlias && !normalizedModel) return {
128
128
  allowed: false,
129
- reason: "max dispatch denied: model must be a Claude Code Agent schema alias, Luna[1m], Gemini 3.7 Flash[1m], or bare Grok 4.6",
129
+ reason: "max dispatch denied: model must be a Claude Code Agent schema alias, Luna[1m], Gemini 3.8 Flash[1m], or bare Grok 4.6",
130
130
  target,
131
131
  verdict: "deny"
132
132
  };
133
133
  const requestedEffort = toolInput.thinking ?? toolInput.effort;
134
- const effort = normalizeMaxDispatchEffort(normalizedModel ? baseModel(normalizedModel) : nativeTarget === "worker-browse" ? "gpt-5.6-luna" : MAX_PROFILE_NATIVE_MODELS[nativeTarget], requestedEffort);
134
+ const effort = normalizeMaxDispatchEffort(normalizedModel ? baseModel(normalizedModel) : nativeTarget === "worker-browse" ? "gpt-5.6-luna" : nativeTarget === "reviewer" && opts.reviewerModel ? baseModel(opts.reviewerModel) : MAX_PROFILE_NATIVE_MODELS[nativeTarget], requestedEffort);
135
135
  if ((toolInput.thinking !== void 0 || toolInput.effort !== void 0) && !effort) return {
136
136
  allowed: false,
137
137
  reason: "max dispatch denied: reasoning override is not supported by the selected model",
138
138
  target,
139
139
  verdict: "deny"
140
140
  };
141
+ const effectiveEffort = (nativeTarget === "reviewer" && !normalizedModel && opts.reviewerModel ? opts.reviewerEffort ?? (baseModel(opts.reviewerModel) === "gpt-5.6-luna" ? "max" : "high") : void 0) ?? effort;
141
142
  const updatedInput = { ...toolInput };
142
143
  if (normalizedModel) updatedInput.model = normalizedModel;
143
144
  else delete updatedInput.model;
144
- if (effort) if (toolInput.thinking !== void 0) updatedInput.thinking = effort;
145
- else updatedInput.effort = effort;
145
+ if (effectiveEffort) if (toolInput.thinking !== void 0) updatedInput.thinking = effectiveEffort;
146
+ else updatedInput.effort = effectiveEffort;
146
147
  if (toolInput.thinking !== void 0 && toolInput.effort !== void 0) delete updatedInput.effort;
147
148
  if (toolInput.model === void 0 && normalizedModel === void 0) delete updatedInput.model;
148
149
  return {
149
150
  allowed: true,
150
151
  target,
151
- effort,
152
+ effort: effectiveEffort,
152
153
  updatedInput,
153
154
  verdict: "allow"
154
155
  };
@@ -182,17 +183,33 @@ const internalMaxDispatchGuard = defineCommand({
182
183
  name: "internal-max-dispatch-guard",
183
184
  description: "Internal: PreToolUse guard enforcing the max-profile native Task/Agent policy."
184
185
  },
185
- run() {
186
- const decision = decideMaxDispatchGuard(readStdinSync());
186
+ args: {
187
+ reviewerModel: {
188
+ type: "string",
189
+ description: "Catalog-resolved Max reviewer model for this launch."
190
+ },
191
+ reviewerEffort: {
192
+ type: "string",
193
+ description: "Fixed Max reviewer effort for this launch."
194
+ }
195
+ },
196
+ run({ args }) {
197
+ const decision = decideMaxDispatchGuard(readStdinSync(), {
198
+ reviewerModel: args.reviewerModel,
199
+ reviewerEffort: args.reviewerEffort === "max" ? "max" : "high"
200
+ });
187
201
  if (!decision.allowed && decision.reason) process.stdout.write(maxDispatchDenyOutput(decision.reason));
188
202
  else if (decision.verdict === "allow" && decision.updatedInput) process.stdout.write(maxDispatchAllowOutput(decision.updatedInput));
189
203
  process.exitCode = 0;
190
204
  }
191
205
  });
192
- function buildMaxDispatchGuardHookCommand(invocation) {
193
- return buildSelfCommand(invocation, "internal-max-dispatch-guard");
206
+ function buildMaxDispatchGuardHookCommand(invocation, opts = {}) {
207
+ const flags = [];
208
+ if (opts.reviewerModel) flags.push(`--reviewerModel "${opts.reviewerModel}"`);
209
+ if (opts.reviewerEffort) flags.push(`--reviewerEffort ${opts.reviewerEffort}`);
210
+ return buildSelfCommand(invocation, ["internal-max-dispatch-guard", ...flags].join(" "));
194
211
  }
195
212
  //#endregion
196
213
  export { assertMaxDispatchGuardInstalled as i, internalMaxDispatchGuard as n, MAX_DISPATCH_TOOL_MATCHER as r, buildMaxDispatchGuardHookCommand as t };
197
214
 
198
- //# sourceMappingURL=internal-max-dispatch-guard-BjSVXMvC.js.map
215
+ //# sourceMappingURL=internal-max-dispatch-guard-CXRB-4a3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal-max-dispatch-guard-CXRB-4a3.js","names":[],"sources":["../src/lib/max-dispatch-acl.ts","../src/internal-max-dispatch-guard.ts"],"sourcesContent":["import {\n MAX_PROFILE_NATIVE_AGENT_NAMES,\n MAX_PROFILE_NATIVE_MODELS,\n type MaxProfileNativeAgentName,\n} from \"./max-profile-contract\"\nimport type { Effort } from \"./reasoning-effort\"\n\nexport const MAX_DISPATCH_TOOL_MATCHER = \"^(Task|Agent)$\"\n\nexport const MAX_BROWSE_DISPATCH_AGENT = \"worker-browse\" as const\n\nconst MAX_NATIVE_AGENT_SET = new Set<string>([\n ...MAX_PROFILE_NATIVE_AGENT_NAMES,\n MAX_BROWSE_DISPATCH_AGENT,\n])\n\n/** Assert max launches never start without their native Task/Agent ACL. */\nexport class MaxDispatchGuardInstallError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"MaxDispatchGuardInstallError\"\n }\n}\n\nexport function assertMaxDispatchGuardInstalled(\n maxProfile: boolean,\n installationSucceeded: boolean,\n): void {\n if (maxProfile && !installationSucceeded) {\n throw new MaxDispatchGuardInstallError(\n \"max profile requires the native Task/Agent ACL hook, but it could not be installed; refusing to start an unguarded max session.\",\n )\n }\n}\n\nconst MAX_ALLOWED_MODEL_BASES = new Set([\"gpt-5.6-luna\", \"gemini-3.8-flash\", \"grok-4.6\"])\n\n/**\n * Claude Code's public Agent schema currently requires one of these built-in\n * model aliases even when the selected custom agent already owns its model in\n * frontmatter. They are transport placeholders in max, not model overrides.\n * Strip them so the role's catalog-validated max model remains authoritative.\n * Case and a trailing `[1m]` are ignored deliberately: neither can turn a\n * client-owned placeholder into an Opus override in the closed max roster.\n */\nexport const MAX_AGENT_SCHEMA_MODEL_ALIASES = Object.freeze([\n \"sonnet\",\n \"opus\",\n \"haiku\",\n \"fable\",\n] as const)\n\nconst MAX_AGENT_SCHEMA_MODEL_ALIAS_SET = new Set<string>(\n MAX_AGENT_SCHEMA_MODEL_ALIASES,\n)\n\nfunction isMaxAgentSchemaModelAlias(model: unknown): boolean {\n if (typeof model !== \"string\") return false\n return MAX_AGENT_SCHEMA_MODEL_ALIAS_SET.has(baseModel(model.trim()).toLowerCase())\n}\n\nexport interface MaxDispatchGuardOptions {\n /** Catalog-resolved reviewer model for this launch (Grok/high or Luna/max). */\n reviewerModel?: string\n reviewerEffort?: Extract<Effort, \"high\" | \"max\">\n}\n\nexport interface MaxDispatchDecision {\n allowed: boolean\n reason?: string\n updatedInput?: Record<string, unknown>\n target?: string\n effort?: Effort\n verdict: \"allow\" | \"deny\" | \"allow-non-dispatch\"\n}\n\nfunction stringField(input: Record<string, unknown>, ...keys: string[]): string | undefined {\n const values = keys\n .filter((key) => Object.prototype.hasOwnProperty.call(input, key))\n .map((key) => input[key])\n .filter((value) => value !== undefined && value !== null)\n if (values.length === 0) return undefined\n if (values.some((value) => typeof value !== \"string\" || value.trim().length === 0)) return undefined\n const distinct = [...new Set((values as string[]).map((value) => value.trim()))]\n return distinct.length === 1 ? distinct[0] : undefined\n}\n\nfunction baseModel(model: string): string {\n return model.replace(/(?:\\[1m\\])+$/i, \"\")\n}\n\nexport function normalizeMaxDispatchModel(model: unknown): string | undefined {\n if (typeof model !== \"string\" || model.trim().length === 0) return undefined\n const trimmed = model.trim()\n const base = baseModel(trimmed)\n return MAX_ALLOWED_MODEL_BASES.has(base)\n ? base === \"grok-4.6\" ? base : `${base}[1m]`\n : undefined\n}\n\nexport function normalizeMaxDispatchEffort(\n model: string,\n effort: unknown,\n): Effort | undefined {\n if (effort === undefined || effort === null) return undefined\n if (typeof effort !== \"string\") return undefined\n const allowed: ReadonlyArray<Effort> = model === \"gpt-5.6-sol\"\n ? [\"high\", \"xhigh\", \"max\"]\n : model === \"gpt-5.6-luna\"\n ? [\"none\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"]\n : model === \"grok-4.6\" || model === \"gemini-3.8-flash\"\n // The optional reviewer/brainstorm fallback swaps these two models;\n // their max-profile effort intersection is deliberately identical.\n ? [\"low\", \"medium\", \"high\"]\n : []\n if (allowed.includes(effort as Effort)) return effort as Effort\n return undefined\n}\n\n/**\n * Native max Task/Agent ACL. Role frontmatter owns the fixed model. Required\n * public-schema aliases are stripped as transport placeholders; a client that\n * can express custom ids may override only to Luna, Gemini, or Grok with a\n * live-ladder effort. Sol/Codex/unknown/private ids are rejected. The lead may\n * target any emitted max role; role-to-role edges are unrestricted here.\n */\nexport function decideMaxDispatchGuard(\n stdin: string | unknown,\n opts: MaxDispatchGuardOptions = {},\n): MaxDispatchDecision {\n let parsed: unknown\n try {\n parsed = typeof stdin === \"string\" ? JSON.parse(stdin) : stdin\n } catch {\n return { allowed: false, reason: \"max dispatch denied: malformed hook payload\", verdict: \"deny\" }\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return { allowed: false, reason: \"max dispatch denied: hook payload must be an object\", verdict: \"deny\" }\n }\n const payload = parsed as Record<string, unknown>\n if (typeof payload.tool_name !== \"string\") {\n return { allowed: false, reason: \"max dispatch denied: hook payload has no tool_name\", verdict: \"deny\" }\n }\n if (!/^(Task|Agent)$/.test(payload.tool_name)) {\n return { allowed: true, verdict: \"allow-non-dispatch\" }\n }\n if (!payload.tool_input || typeof payload.tool_input !== \"object\" || Array.isArray(payload.tool_input)) {\n return { allowed: false, reason: `max dispatch denied: ${payload.tool_name} payload has malformed tool_input`, verdict: \"deny\" }\n }\n const toolInput = payload.tool_input as Record<string, unknown>\n const target = stringField(toolInput, \"subagent_type\", \"subagentType\")\n if (!target || !MAX_NATIVE_AGENT_SET.has(target)) {\n return { allowed: false, reason: `max dispatch denied: target must be one of ${[...MAX_PROFILE_NATIVE_AGENT_NAMES, MAX_BROWSE_DISPATCH_AGENT].join(\", \")}`, verdict: \"deny\" }\n }\n const nativeTarget = target as MaxProfileNativeAgentName | typeof MAX_BROWSE_DISPATCH_AGENT\n const modelValue = toolInput.model\n const schemaAlias = isMaxAgentSchemaModelAlias(modelValue)\n const normalizedModel = modelValue === undefined || schemaAlias\n ? undefined\n : normalizeMaxDispatchModel(modelValue)\n if (modelValue !== undefined && !schemaAlias && !normalizedModel) {\n return { allowed: false, reason: \"max dispatch denied: model must be a Claude Code Agent schema alias, Luna[1m], Gemini 3.8 Flash[1m], or bare Grok 4.6\", target, verdict: \"deny\" }\n }\n const requestedEffort = toolInput.thinking ?? toolInput.effort\n const effectiveModel = normalizedModel\n ? baseModel(normalizedModel)\n : nativeTarget === MAX_BROWSE_DISPATCH_AGENT\n // Keep this aligned with BROWSE_DEFAULT_MODEL. Importing worker-agent/engine\n // here would pull the worker runtime into the small hook bundle.\n ? \"gpt-5.6-luna\"\n : nativeTarget === \"reviewer\" && opts.reviewerModel\n ? baseModel(opts.reviewerModel)\n : MAX_PROFILE_NATIVE_MODELS[nativeTarget]\n const effort = normalizeMaxDispatchEffort(effectiveModel, requestedEffort)\n if ((toolInput.thinking !== undefined || toolInput.effort !== undefined) && !effort) {\n return { allowed: false, reason: \"max dispatch denied: reasoning override is not supported by the selected model\", target, verdict: \"deny\" }\n }\n const fixedReviewerEffort = nativeTarget === \"reviewer\"\n && !normalizedModel\n && opts.reviewerModel\n ? opts.reviewerEffort ?? (baseModel(opts.reviewerModel) === \"gpt-5.6-luna\" ? \"max\" : \"high\")\n : undefined\n const effectiveEffort = fixedReviewerEffort ?? effort\n const updatedInput = { ...toolInput }\n if (normalizedModel) updatedInput.model = normalizedModel\n else delete updatedInput.model\n if (effectiveEffort) {\n // Claude Code's native Agent payload uses `model` and `effort`; preserve\n // an explicit field when possible, and emit the catalog-resolved reviewer\n // fallback effort so Luna cannot inherit Grok's high frontmatter default.\n if (toolInput.thinking !== undefined) updatedInput.thinking = effectiveEffort\n else updatedInput.effort = effectiveEffort\n }\n if (toolInput.thinking !== undefined && toolInput.effort !== undefined) {\n delete updatedInput.effort\n }\n if (toolInput.model === undefined && normalizedModel === undefined) delete updatedInput.model\n return { allowed: true, target, effort: effectiveEffort, updatedInput, verdict: \"allow\" }\n}\n\nexport function maxDispatchAllowOutput(updatedInput: Record<string, unknown>): string {\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"allow\",\n updatedInput,\n },\n })\n}\n\nexport function maxDispatchDenyOutput(reason: string): string {\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: reason,\n },\n })\n}\n","import { defineCommand } from \"citty\"\nimport { readFileSync } from \"node:fs\"\n\nimport {\n decideMaxDispatchGuard,\n maxDispatchAllowOutput,\n maxDispatchDenyOutput,\n} from \"./lib/max-dispatch-acl\"\nimport { buildSelfCommand, type SelfInvocation } from \"./lib/hook-launcher/self-invocation\"\n\nfunction readStdinSync(): string {\n try {\n if (process.stdin.isTTY) return \"\"\n return readFileSync(0, \"utf8\")\n } catch {\n return \"\"\n }\n}\n\nexport const internalMaxDispatchGuard = defineCommand({\n meta: {\n name: \"internal-max-dispatch-guard\",\n description: \"Internal: PreToolUse guard enforcing the max-profile native Task/Agent policy.\",\n },\n args: {\n reviewerModel: {\n type: \"string\",\n description: \"Catalog-resolved Max reviewer model for this launch.\",\n },\n reviewerEffort: {\n type: \"string\",\n description: \"Fixed Max reviewer effort for this launch.\",\n },\n },\n run({ args }) {\n const decision = decideMaxDispatchGuard(readStdinSync(), {\n reviewerModel: args.reviewerModel,\n reviewerEffort: args.reviewerEffort === \"max\" ? \"max\" : \"high\",\n })\n if (!decision.allowed && decision.reason) {\n process.stdout.write(maxDispatchDenyOutput(decision.reason))\n } else if (decision.verdict === \"allow\" && decision.updatedInput) {\n process.stdout.write(maxDispatchAllowOutput(decision.updatedInput))\n }\n process.exitCode = 0\n },\n})\n\nexport function buildMaxDispatchGuardHookCommand(\n invocation: SelfInvocation,\n opts: { reviewerModel?: string; reviewerEffort?: \"high\" | \"max\" } = {},\n): string {\n const flags: string[] = []\n if (opts.reviewerModel) flags.push(`--reviewerModel \"${opts.reviewerModel}\"`)\n if (opts.reviewerEffort) flags.push(`--reviewerEffort ${opts.reviewerEffort}`)\n return buildSelfCommand(\n invocation,\n [\"internal-max-dispatch-guard\", ...flags].join(\" \"),\n )\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { n as internalMaxDispatchGuard } from "./internal-max-dispatch-guard-BjSVXMvC.js";
1
+ import { n as internalMaxDispatchGuard } from "./internal-max-dispatch-guard-CXRB-4a3.js";
2
2
  export { internalMaxDispatchGuard };
@@ -4,7 +4,7 @@ import { a as decideStopHook, c as launchBaselineKey, d as stopReviewEnabled, o
4
4
  import { t as liveExec } from "./orchestration-CYyp9w-t.js";
5
5
  import { c as repoRoot, i as fileReviewDebounce, l as stopGateEnabledForRepo, r as fileLastPromptStore, t as fileBaselineStore, u as stopReviewStateDir } from "./stop-gate-policy-zhzRIj6G.js";
6
6
  import { r as hookMcpRuntimeFromEnv } from "./hook-mcp-client-DBvm9608.js";
7
- import { a as checksForDescriptor, n as readDiscoveredGate, o as descriptorHash, s as parseGateDescriptor } from "./gate-discovery-BTRVyKWu.js";
7
+ import { a as checksForDescriptor, n as readDiscoveredGate, o as descriptorHash, s as parseGateDescriptor } from "./gate-discovery-D4-F99e_.js";
8
8
  import { defineCommand } from "citty";
9
9
  import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import path from "node:path";
@@ -257,4 +257,4 @@ const internalStopHook = defineCommand({
257
257
  //#endregion
258
258
  export { internalStopHook };
259
259
 
260
- //# sourceMappingURL=internal-stop-hook-BZg__ab6.js.map
260
+ //# sourceMappingURL=internal-stop-hook-D8LIWryj.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal-stop-hook-BZg__ab6.js","names":[],"sources":["../src/internal-stop-hook.ts"],"sourcesContent":["/**\n * The internal `internal-stop-hook` subcommand: the executable a spawned Claude\n * Code session's Stop hook invokes (registered into the mirrored settings.json by\n * the launcher when `GH_ROUTER_ENABLE_STOP_GATE` is set). It reads Claude Code's\n * hook payload from stdin, runs the SEALED structural gate over the session's\n * working-tree diff, and maps the result to the hook exit contract: exit 2 (with\n * the reason on stderr) blocks the stop so the model fixes the failure; exit 0\n * allows it. The `stop_hook_active` loop guard (in `decideStopHook`) guarantees\n * it can never wedge the session.\n *\n * All decision logic lives in `decideStopHook` (pure, unit-tested); this wrapper\n * only does stdin read + the live `git diff` capture + the exit. The live firing\n * is verified by the gated E2E (it needs a real spawned session).\n */\n\nimport { defineCommand } from \"citty\"\n\nimport { spawn } from \"node:child_process\"\nimport { randomBytes } from \"node:crypto\"\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\n\nimport { parseBoolEnv, runCommandCapture } from \"./lib/exec\"\nimport { PACKAGE_ROOT_FLAG, explicitPackageRoot } from \"./lib/package-root\"\nimport { liveExec } from \"./lib/orchestration\"\nimport { readDiscoveredGate } from \"./lib/orchestration/gate-discovery\"\nimport { checksForDescriptor, descriptorHash, parseGateDescriptor, type GateDescriptor } from \"./lib/orchestration/harness-parse\"\nimport { hookMcpRuntimeFromEnv } from \"./lib/orchestration/hook-mcp-client\"\nimport {\n decideStopHook,\n fileBlockBudget,\n launchBaselineKey,\n stopGateId,\n stopReviewEnabled,\n type StopReviewContext,\n} from \"./lib/orchestration/stop-gate-hook\"\nimport {\n fileBaselineStore,\n fileLastPromptStore,\n fileReviewDebounce,\n repoRoot,\n stopGateEnabledForRepo,\n stopReviewStateDir,\n} from \"./lib/orchestration/stop-gate-policy\"\nimport { parseIntEnv } from \"./lib/exec\"\n\n/**\n * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`). An async\n * stdin read leaves an in-flight libuv FS request that, on Windows, races the\n * process teardown and trips a `uv_async_send` assertion; a synchronous read has\n * no such handle. Hooks always receive piped/redirected stdin, so this never\n * blocks (guarded against an interactive TTY, and any error -> \"\").\n */\nfunction readStdin(): string {\n try {\n if (process.stdin.isTTY) return \"\"\n return readFileSync(0, \"utf8\")\n } catch {\n return \"\"\n }\n}\n\n/** Max diff bytes scanned for gate-weakening: a hard cap so a huge generated diff\n * (e.g. a lockfile) can never OOM or stall the hook. */\nconst MAX_DIFF_BYTES = 2 * 1024 * 1024\n\n/** Build a tiny synthetic diff for untracked paths so the weakening scan sees\n * their contents too. No index mutation (`git add -N`) is needed. */\nasync function captureUntrackedDiff(cwd: string, paths: string[]): Promise<string> {\n const chunks: string[] = []\n let bytes = 0\n for (const untrackedPath of paths) {\n const result = await runCommandCapture(\n [\"git\", \"diff\", \"--no-index\", \"--\", \"/dev/null\", untrackedPath],\n { cwd, timeoutMs: 5_000, maxStdoutBytes: MAX_DIFF_BYTES },\n )\n // `git diff --no-index` returns 1 when differences exist; anything else is an\n // actual failure. A truncated untracked diff is unknown, never \"no diff\".\n if (result.code !== 1 || result.truncated) throw new Error(`git diff for untracked file failed with exit ${result.code}`)\n bytes += Buffer.byteLength(result.stdout, \"utf8\")\n if (bytes > MAX_DIFF_BYTES) throw new Error(\"git working-tree capture exceeded its size limit\")\n chunks.push(result.stdout)\n }\n return chunks.join(\"\")\n}\n\n/** Capture every working-tree change WITHOUT mutating the user's index (no\n * `git add -N`). `git diff HEAD` covers staged + unstaged tracked changes;\n * `git ls-files --others` catches untracked files and each is diffed against an\n * empty file so gate-weakening in new tests is visible. Git failures/truncation\n * reject, so the decision layer runs the full checks instead of mistaking an\n * unknown tree for a no-diff turn. */\nexport async function captureStopGateDiff(cwd: string): Promise<string> {\n const [diffResult, untrackedResult] = await Promise.all([\n runCommandCapture([\"git\", \"diff\", \"HEAD\"], { cwd, timeoutMs: 5_000, maxStdoutBytes: MAX_DIFF_BYTES }),\n runCommandCapture([\"git\", \"ls-files\", \"--others\", \"--exclude-standard\", \"-z\"], {\n cwd,\n timeoutMs: 5_000,\n maxStdoutBytes: MAX_DIFF_BYTES,\n }),\n ])\n if (diffResult.code !== 0) throw new Error(`git diff failed with exit ${diffResult.code}`)\n if (untrackedResult.code !== 0) throw new Error(`git ls-files failed with exit ${untrackedResult.code}`)\n if (diffResult.truncated || untrackedResult.truncated) throw new Error(\"git working-tree capture exceeded its size limit\")\n\n const untrackedPaths = untrackedResult.stdout.split(\"\\0\").filter(Boolean)\n if (untrackedPaths.length === 0) return diffResult.stdout\n const untrackedDiff = await captureUntrackedDiff(cwd, untrackedPaths)\n if (Buffer.byteLength(diffResult.stdout, \"utf8\") + Buffer.byteLength(untrackedDiff, \"utf8\") > MAX_DIFF_BYTES) {\n throw new Error(\"git working-tree capture exceeded its size limit\")\n }\n return diffResult.stdout + untrackedDiff\n}\n\n/** Flush a message to stderr before exiting (process.exit can drop an unflushed\n * write; the model reads this stderr on a block). */\nasync function writeStderr(msg: string): Promise<void> {\n await new Promise<void>((resolve) => {\n process.stderr.write(msg, () => resolve())\n })\n}\n\n/**\n * Fire-and-forget spawn of the detached background reviewer. The payload (which\n * includes the up-to-2-MiB diff) is written to a temp file SYNCHRONOUSLY before\n * the spawn — a pipe to the child's stdin would race the parent's `process.exit`\n * and could deliver a truncated diff. The child reads the file (path passed via\n * `GH_ROUTER_STOP_REVIEW_PAYLOAD`), unlinks it, and inherits the proxy URL/nonce\n * env. Everything is swallowed: the advisory layer never affects the stop.\n */\nfunction spawnStopReview(ctx: StopReviewContext, extras: { prompt: string; transcriptPath: string }): void {\n let payloadPath: string | undefined\n try {\n const dir = stopReviewStateDir()\n mkdirSync(dir, { recursive: true })\n payloadPath = path.join(dir, `payload-${process.pid}-${randomBytes(4).toString(\"hex\")}.json`)\n writeFileSync(\n payloadPath,\n JSON.stringify({\n session_id: ctx.sessionId,\n cwd: ctx.cwd,\n diff: ctx.diff,\n prompt: extras.prompt,\n transcript_path: extras.transcriptPath,\n }),\n { mode: 0o600 },\n )\n // Invoke the same binary's `internal-stop-review` subcommand. Preserve the\n // package root when this hook itself runs from the relocated launcher.\n const scriptArgs = process.argv[1] && process.argv[1] !== process.execPath ? [process.argv[1]] : []\n const root = explicitPackageRoot()\n const packageRootArgs = root ? [PACKAGE_ROOT_FLAG, root] : []\n const child = spawn(process.execPath, [...scriptArgs, ...packageRootArgs, \"internal-stop-review\"], {\n detached: true,\n windowsHide: true,\n stdio: \"ignore\",\n env: { ...process.env, GH_ROUTER_STOP_REVIEW_PAYLOAD: payloadPath },\n })\n // A spawn failure (EAGAIN / EACCES / fork limit) is delivered ASYNCHRONOUSLY\n // via the child's 'error' event, AFTER this synchronous try block exits. With\n // no listener Node escalates it to an uncaughtException — which main.ts turns\n // into process.exit(1), corrupting the Stop hook's exit code. Swallow it here\n // (and drop the now-orphaned payload the child never read) so the advisory\n // spawn truly never affects the stop.\n const orphan = payloadPath\n child.on(\"error\", () => {\n if (orphan) {\n try {\n unlinkSync(orphan)\n } catch {\n /* best-effort */\n }\n }\n })\n child.unref()\n } catch {\n // Advisory spawn is best-effort; never disrupt the stop. If we wrote the\n // payload file but the spawn failed, drop it so it doesn't orphan (the\n // child — which would normally unlink it — never started).\n if (payloadPath) {\n try {\n unlinkSync(payloadPath)\n } catch {\n /* best-effort */\n }\n }\n }\n}\n\n/** The checks for a resolved descriptor: sealed descriptors resolve their sealed\n * command set; parsed/discovered carry their own. */\ntype ResolveChecks = NonNullable<Parameters<typeof decideStopHook>[0][\"resolveChecks\"]>\n\n/**\n * Build the dynamic `resolveChecks` for the live Stop hook, per the env the\n * launcher set:\n * - GH_ROUTER_STOP_GATE_PARSED → re-derive the deterministic parser at the\n * stop-time tree (stateless, no cache);\n * - GH_ROUTER_STOP_GATE_DISCOVERED → read the cached evidence-pinned record.\n * Both pin the checks to the descriptor's `workdir` (the repo root) and compute\n * the launch-stable baseline key so the launch-captured (pre-mutation) baseline\n * is read. Any miss → null → the hook fails OPEN.\n */\nfunction buildResolveChecks(mode: \"parsed\" | \"discovered\"): ResolveChecks {\n const includeTests = parseBoolEnv(process.env.GH_ROUTER_STOP_GATE_RUN_TESTS) === true\n return async (cwd: string) => {\n const root = await repoRoot(cwd).catch(() => cwd)\n let descriptor: GateDescriptor | null = null\n if (mode === \"parsed\") {\n descriptor = await parseGateDescriptor(root, { includeTests }).catch(() => null)\n } else {\n const rec = await readDiscoveredGate(root).catch(() => null)\n if (rec) {\n descriptor = {\n kind: \"discovered\",\n checks: rec.checks,\n ecosystem: rec.ecosystem,\n workdir: root,\n evidence: rec.evidence,\n }\n }\n }\n if (!descriptor) return null\n const checks = checksForDescriptor(descriptor)\n if (checks.length === 0) return null\n const descriptorKey = descriptorHash(descriptor)\n const workdir = descriptor.workdir || root\n const token = process.env.GH_ROUTER_STOP_GATE_BASELINE_TOKEN || undefined\n return { checks, workdir, descriptorKey, baselineKey: launchBaselineKey(workdir, descriptorKey, token) }\n }\n}\n\n/** Which dynamic gate mode the launcher armed (sealed → undefined). */\nfunction dynamicMode(): \"parsed\" | \"discovered\" | undefined {\n if (parseBoolEnv(process.env.GH_ROUTER_STOP_GATE_PARSED) === true) return \"parsed\"\n if (parseBoolEnv(process.env.GH_ROUTER_STOP_GATE_DISCOVERED) === true) return \"discovered\"\n return undefined\n}\n\nexport const internalStopHook = defineCommand({\n meta: {\n name: \"internal-stop-hook\",\n description:\n \"Internal: the structural-gate Stop hook. Reads the Claude Code hook payload on stdin, \"\n + \"runs the sealed gate, exits 2 (blocks the stop) on a red gate or gate-weakening diff.\",\n },\n async run() {\n const stdin = readStdin()\n // The advisory review (hook V2) is wired only when it's enabled AND the\n // launcher injected the proxy URL/nonce. It is side-effect-only: the\n // deterministic gate below is unchanged and remains the only blocker.\n const reviewEnabled = stopReviewEnabled() && hookMcpRuntimeFromEnv() !== undefined\n let transcriptPath = \"\"\n let userPrompt = \"\"\n if (reviewEnabled) {\n // Parse the payload once for the transcript pointer + the session id used\n // to look up the user's last prompt (the Stop payload carries no prompt;\n // the UserPromptSubmit hook stashed it). Best-effort — a parse miss just\n // means the reviewer judges against the diff alone.\n try {\n const p: unknown = JSON.parse(stdin)\n if (p && typeof p === \"object\") {\n const obj = p as { transcript_path?: unknown; session_id?: unknown }\n transcriptPath = typeof obj.transcript_path === \"string\" ? obj.transcript_path : \"\"\n const sid = typeof obj.session_id === \"string\" ? obj.session_id : \"\"\n if (sid) {\n userPrompt = (await fileLastPromptStore(stopReviewStateDir()).read(sid).catch(() => null)) ?? \"\"\n }\n }\n } catch {\n /* tolerate a non-JSON stdin */\n }\n }\n\n let decision: { exitCode: 0 | 2; stderr?: string }\n try {\n const timeoutEnv = parseIntEnv(process.env.GH_ROUTER_STOP_GATE_TIMEOUT_MS)\n const mode = dynamicMode()\n decision = await decideStopHook({\n stdin,\n gateId: stopGateId(),\n exec: liveExec,\n captureDiff: captureStopGateDiff,\n fallbackCwd: process.cwd(),\n budget: fileBlockBudget(path.join(tmpdir(), \"gh-router-stopgate\")),\n baseline: fileBaselineStore(path.join(tmpdir(), \"gh-router-stopgate-baseline\")),\n isEnabledForRepo: (cwd) => stopGateEnabledForRepo(cwd),\n resolveChecks: mode ? buildResolveChecks(mode) : undefined,\n // parseIntEnv already rejects non-positive / non-integer / truncating\n // values, so undefined here means \"use the built-in default\".\n timeoutMs: timeoutEnv,\n reviewDebounce: reviewEnabled ? fileReviewDebounce(stopReviewStateDir()) : undefined,\n spawnReview: reviewEnabled\n ? (ctx) => spawnStopReview(ctx, { prompt: userPrompt, transcriptPath })\n : undefined,\n })\n } catch {\n // Fail OPEN on ANY unexpected error: a Stop hook must never wedge the\n // session, so an internal crash allows the stop (exit 0) rather than\n // surfacing a non-blocking error or a hang.\n process.exitCode = 0\n return\n }\n // Write any stderr the decision carries — both the exit-2 block reason AND\n // the LOUD exit-0 stand-down (max-block budget reached). A clean green stop\n // carries no stderr, so this stays silent on the common path.\n if (decision.stderr) {\n await writeStderr(`${decision.stderr}\\n`)\n }\n // Natural exit: set the code and return. A hard process.exit() races libuv's\n // stdio teardown on Windows (uv_async_send assertion) on the fast-return\n // paths. The detached review child is unref'd and the gate's children are\n // reaped, so no handle keeps the loop alive — the process exits with this\n // code. The exit-2 block contract is preserved (stderr is flushed above).\n process.exitCode = decision.exitCode\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"internal-stop-hook-D8LIWryj.js","names":[],"sources":["../src/internal-stop-hook.ts"],"sourcesContent":["/**\n * The internal `internal-stop-hook` subcommand: the executable a spawned Claude\n * Code session's Stop hook invokes (registered into the mirrored settings.json by\n * the launcher when `GH_ROUTER_ENABLE_STOP_GATE` is set). It reads Claude Code's\n * hook payload from stdin, runs the SEALED structural gate over the session's\n * working-tree diff, and maps the result to the hook exit contract: exit 2 (with\n * the reason on stderr) blocks the stop so the model fixes the failure; exit 0\n * allows it. The `stop_hook_active` loop guard (in `decideStopHook`) guarantees\n * it can never wedge the session.\n *\n * All decision logic lives in `decideStopHook` (pure, unit-tested); this wrapper\n * only does stdin read + the live `git diff` capture + the exit. The live firing\n * is verified by the gated E2E (it needs a real spawned session).\n */\n\nimport { defineCommand } from \"citty\"\n\nimport { spawn } from \"node:child_process\"\nimport { randomBytes } from \"node:crypto\"\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\n\nimport { parseBoolEnv, runCommandCapture } from \"./lib/exec\"\nimport { PACKAGE_ROOT_FLAG, explicitPackageRoot } from \"./lib/package-root\"\nimport { liveExec } from \"./lib/orchestration\"\nimport { readDiscoveredGate } from \"./lib/orchestration/gate-discovery\"\nimport { checksForDescriptor, descriptorHash, parseGateDescriptor, type GateDescriptor } from \"./lib/orchestration/harness-parse\"\nimport { hookMcpRuntimeFromEnv } from \"./lib/orchestration/hook-mcp-client\"\nimport {\n decideStopHook,\n fileBlockBudget,\n launchBaselineKey,\n stopGateId,\n stopReviewEnabled,\n type StopReviewContext,\n} from \"./lib/orchestration/stop-gate-hook\"\nimport {\n fileBaselineStore,\n fileLastPromptStore,\n fileReviewDebounce,\n repoRoot,\n stopGateEnabledForRepo,\n stopReviewStateDir,\n} from \"./lib/orchestration/stop-gate-policy\"\nimport { parseIntEnv } from \"./lib/exec\"\n\n/**\n * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`). An async\n * stdin read leaves an in-flight libuv FS request that, on Windows, races the\n * process teardown and trips a `uv_async_send` assertion; a synchronous read has\n * no such handle. Hooks always receive piped/redirected stdin, so this never\n * blocks (guarded against an interactive TTY, and any error -> \"\").\n */\nfunction readStdin(): string {\n try {\n if (process.stdin.isTTY) return \"\"\n return readFileSync(0, \"utf8\")\n } catch {\n return \"\"\n }\n}\n\n/** Max diff bytes scanned for gate-weakening: a hard cap so a huge generated diff\n * (e.g. a lockfile) can never OOM or stall the hook. */\nconst MAX_DIFF_BYTES = 2 * 1024 * 1024\n\n/** Build a tiny synthetic diff for untracked paths so the weakening scan sees\n * their contents too. No index mutation (`git add -N`) is needed. */\nasync function captureUntrackedDiff(cwd: string, paths: string[]): Promise<string> {\n const chunks: string[] = []\n let bytes = 0\n for (const untrackedPath of paths) {\n const result = await runCommandCapture(\n [\"git\", \"diff\", \"--no-index\", \"--\", \"/dev/null\", untrackedPath],\n { cwd, timeoutMs: 5_000, maxStdoutBytes: MAX_DIFF_BYTES },\n )\n // `git diff --no-index` returns 1 when differences exist; anything else is an\n // actual failure. A truncated untracked diff is unknown, never \"no diff\".\n if (result.code !== 1 || result.truncated) throw new Error(`git diff for untracked file failed with exit ${result.code}`)\n bytes += Buffer.byteLength(result.stdout, \"utf8\")\n if (bytes > MAX_DIFF_BYTES) throw new Error(\"git working-tree capture exceeded its size limit\")\n chunks.push(result.stdout)\n }\n return chunks.join(\"\")\n}\n\n/** Capture every working-tree change WITHOUT mutating the user's index (no\n * `git add -N`). `git diff HEAD` covers staged + unstaged tracked changes;\n * `git ls-files --others` catches untracked files and each is diffed against an\n * empty file so gate-weakening in new tests is visible. Git failures/truncation\n * reject, so the decision layer runs the full checks instead of mistaking an\n * unknown tree for a no-diff turn. */\nexport async function captureStopGateDiff(cwd: string): Promise<string> {\n const [diffResult, untrackedResult] = await Promise.all([\n runCommandCapture([\"git\", \"diff\", \"HEAD\"], { cwd, timeoutMs: 5_000, maxStdoutBytes: MAX_DIFF_BYTES }),\n runCommandCapture([\"git\", \"ls-files\", \"--others\", \"--exclude-standard\", \"-z\"], {\n cwd,\n timeoutMs: 5_000,\n maxStdoutBytes: MAX_DIFF_BYTES,\n }),\n ])\n if (diffResult.code !== 0) throw new Error(`git diff failed with exit ${diffResult.code}`)\n if (untrackedResult.code !== 0) throw new Error(`git ls-files failed with exit ${untrackedResult.code}`)\n if (diffResult.truncated || untrackedResult.truncated) throw new Error(\"git working-tree capture exceeded its size limit\")\n\n const untrackedPaths = untrackedResult.stdout.split(\"\\0\").filter(Boolean)\n if (untrackedPaths.length === 0) return diffResult.stdout\n const untrackedDiff = await captureUntrackedDiff(cwd, untrackedPaths)\n if (Buffer.byteLength(diffResult.stdout, \"utf8\") + Buffer.byteLength(untrackedDiff, \"utf8\") > MAX_DIFF_BYTES) {\n throw new Error(\"git working-tree capture exceeded its size limit\")\n }\n return diffResult.stdout + untrackedDiff\n}\n\n/** Flush a message to stderr before exiting (process.exit can drop an unflushed\n * write; the model reads this stderr on a block). */\nasync function writeStderr(msg: string): Promise<void> {\n await new Promise<void>((resolve) => {\n process.stderr.write(msg, () => resolve())\n })\n}\n\n/**\n * Fire-and-forget spawn of the detached background reviewer. The payload (which\n * includes the up-to-2-MiB diff) is written to a temp file SYNCHRONOUSLY before\n * the spawn — a pipe to the child's stdin would race the parent's `process.exit`\n * and could deliver a truncated diff. The child reads the file (path passed via\n * `GH_ROUTER_STOP_REVIEW_PAYLOAD`), unlinks it, and inherits the proxy URL/nonce\n * env. Everything is swallowed: the advisory layer never affects the stop.\n */\nfunction spawnStopReview(ctx: StopReviewContext, extras: { prompt: string; transcriptPath: string }): void {\n let payloadPath: string | undefined\n try {\n const dir = stopReviewStateDir()\n mkdirSync(dir, { recursive: true })\n payloadPath = path.join(dir, `payload-${process.pid}-${randomBytes(4).toString(\"hex\")}.json`)\n writeFileSync(\n payloadPath,\n JSON.stringify({\n session_id: ctx.sessionId,\n cwd: ctx.cwd,\n diff: ctx.diff,\n prompt: extras.prompt,\n transcript_path: extras.transcriptPath,\n }),\n { mode: 0o600 },\n )\n // Invoke the same binary's `internal-stop-review` subcommand. Preserve the\n // package root when this hook itself runs from the relocated launcher.\n const scriptArgs = process.argv[1] && process.argv[1] !== process.execPath ? [process.argv[1]] : []\n const root = explicitPackageRoot()\n const packageRootArgs = root ? [PACKAGE_ROOT_FLAG, root] : []\n const child = spawn(process.execPath, [...scriptArgs, ...packageRootArgs, \"internal-stop-review\"], {\n detached: true,\n windowsHide: true,\n stdio: \"ignore\",\n env: { ...process.env, GH_ROUTER_STOP_REVIEW_PAYLOAD: payloadPath },\n })\n // A spawn failure (EAGAIN / EACCES / fork limit) is delivered ASYNCHRONOUSLY\n // via the child's 'error' event, AFTER this synchronous try block exits. With\n // no listener Node escalates it to an uncaughtException — which main.ts turns\n // into process.exit(1), corrupting the Stop hook's exit code. Swallow it here\n // (and drop the now-orphaned payload the child never read) so the advisory\n // spawn truly never affects the stop.\n const orphan = payloadPath\n child.on(\"error\", () => {\n if (orphan) {\n try {\n unlinkSync(orphan)\n } catch {\n /* best-effort */\n }\n }\n })\n child.unref()\n } catch {\n // Advisory spawn is best-effort; never disrupt the stop. If we wrote the\n // payload file but the spawn failed, drop it so it doesn't orphan (the\n // child — which would normally unlink it — never started).\n if (payloadPath) {\n try {\n unlinkSync(payloadPath)\n } catch {\n /* best-effort */\n }\n }\n }\n}\n\n/** The checks for a resolved descriptor: sealed descriptors resolve their sealed\n * command set; parsed/discovered carry their own. */\ntype ResolveChecks = NonNullable<Parameters<typeof decideStopHook>[0][\"resolveChecks\"]>\n\n/**\n * Build the dynamic `resolveChecks` for the live Stop hook, per the env the\n * launcher set:\n * - GH_ROUTER_STOP_GATE_PARSED → re-derive the deterministic parser at the\n * stop-time tree (stateless, no cache);\n * - GH_ROUTER_STOP_GATE_DISCOVERED → read the cached evidence-pinned record.\n * Both pin the checks to the descriptor's `workdir` (the repo root) and compute\n * the launch-stable baseline key so the launch-captured (pre-mutation) baseline\n * is read. Any miss → null → the hook fails OPEN.\n */\nfunction buildResolveChecks(mode: \"parsed\" | \"discovered\"): ResolveChecks {\n const includeTests = parseBoolEnv(process.env.GH_ROUTER_STOP_GATE_RUN_TESTS) === true\n return async (cwd: string) => {\n const root = await repoRoot(cwd).catch(() => cwd)\n let descriptor: GateDescriptor | null = null\n if (mode === \"parsed\") {\n descriptor = await parseGateDescriptor(root, { includeTests }).catch(() => null)\n } else {\n const rec = await readDiscoveredGate(root).catch(() => null)\n if (rec) {\n descriptor = {\n kind: \"discovered\",\n checks: rec.checks,\n ecosystem: rec.ecosystem,\n workdir: root,\n evidence: rec.evidence,\n }\n }\n }\n if (!descriptor) return null\n const checks = checksForDescriptor(descriptor)\n if (checks.length === 0) return null\n const descriptorKey = descriptorHash(descriptor)\n const workdir = descriptor.workdir || root\n const token = process.env.GH_ROUTER_STOP_GATE_BASELINE_TOKEN || undefined\n return { checks, workdir, descriptorKey, baselineKey: launchBaselineKey(workdir, descriptorKey, token) }\n }\n}\n\n/** Which dynamic gate mode the launcher armed (sealed → undefined). */\nfunction dynamicMode(): \"parsed\" | \"discovered\" | undefined {\n if (parseBoolEnv(process.env.GH_ROUTER_STOP_GATE_PARSED) === true) return \"parsed\"\n if (parseBoolEnv(process.env.GH_ROUTER_STOP_GATE_DISCOVERED) === true) return \"discovered\"\n return undefined\n}\n\nexport const internalStopHook = defineCommand({\n meta: {\n name: \"internal-stop-hook\",\n description:\n \"Internal: the structural-gate Stop hook. Reads the Claude Code hook payload on stdin, \"\n + \"runs the sealed gate, exits 2 (blocks the stop) on a red gate or gate-weakening diff.\",\n },\n async run() {\n const stdin = readStdin()\n // The advisory review (hook V2) is wired only when it's enabled AND the\n // launcher injected the proxy URL/nonce. It is side-effect-only: the\n // deterministic gate below is unchanged and remains the only blocker.\n const reviewEnabled = stopReviewEnabled() && hookMcpRuntimeFromEnv() !== undefined\n let transcriptPath = \"\"\n let userPrompt = \"\"\n if (reviewEnabled) {\n // Parse the payload once for the transcript pointer + the session id used\n // to look up the user's last prompt (the Stop payload carries no prompt;\n // the UserPromptSubmit hook stashed it). Best-effort — a parse miss just\n // means the reviewer judges against the diff alone.\n try {\n const p: unknown = JSON.parse(stdin)\n if (p && typeof p === \"object\") {\n const obj = p as { transcript_path?: unknown; session_id?: unknown }\n transcriptPath = typeof obj.transcript_path === \"string\" ? obj.transcript_path : \"\"\n const sid = typeof obj.session_id === \"string\" ? obj.session_id : \"\"\n if (sid) {\n userPrompt = (await fileLastPromptStore(stopReviewStateDir()).read(sid).catch(() => null)) ?? \"\"\n }\n }\n } catch {\n /* tolerate a non-JSON stdin */\n }\n }\n\n let decision: { exitCode: 0 | 2; stderr?: string }\n try {\n const timeoutEnv = parseIntEnv(process.env.GH_ROUTER_STOP_GATE_TIMEOUT_MS)\n const mode = dynamicMode()\n decision = await decideStopHook({\n stdin,\n gateId: stopGateId(),\n exec: liveExec,\n captureDiff: captureStopGateDiff,\n fallbackCwd: process.cwd(),\n budget: fileBlockBudget(path.join(tmpdir(), \"gh-router-stopgate\")),\n baseline: fileBaselineStore(path.join(tmpdir(), \"gh-router-stopgate-baseline\")),\n isEnabledForRepo: (cwd) => stopGateEnabledForRepo(cwd),\n resolveChecks: mode ? buildResolveChecks(mode) : undefined,\n // parseIntEnv already rejects non-positive / non-integer / truncating\n // values, so undefined here means \"use the built-in default\".\n timeoutMs: timeoutEnv,\n reviewDebounce: reviewEnabled ? fileReviewDebounce(stopReviewStateDir()) : undefined,\n spawnReview: reviewEnabled\n ? (ctx) => spawnStopReview(ctx, { prompt: userPrompt, transcriptPath })\n : undefined,\n })\n } catch {\n // Fail OPEN on ANY unexpected error: a Stop hook must never wedge the\n // session, so an internal crash allows the stop (exit 0) rather than\n // surfacing a non-blocking error or a hang.\n process.exitCode = 0\n return\n }\n // Write any stderr the decision carries — both the exit-2 block reason AND\n // the LOUD exit-0 stand-down (max-block budget reached). A clean green stop\n // carries no stderr, so this stays silent on the common path.\n if (decision.stderr) {\n await writeStderr(`${decision.stderr}\\n`)\n }\n // Natural exit: set the code and return. A hard process.exit() races libuv's\n // stdio teardown on Windows (uv_async_send assertion) on the fast-return\n // paths. The detached review child is unref'd and the gate's children are\n // reaped, so no handle keeps the loop alive — the process exits with this\n // code. The exit-2 block contract is preserved (stderr is flushed above).\n process.exitCode = decision.exitCode\n },\n})\n"],"mappings":"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"}
package/dist/main.js CHANGED
@@ -47,14 +47,14 @@ const main = defineCommand({
47
47
  },
48
48
  subCommands: {
49
49
  auth: () => import("./auth-CpjOjoUG.js").then((m) => m.auth),
50
- start: () => import("./start-DSqU8d8h.js").then((m) => m.start),
51
- claude: () => import("./claude-uRuzEH0W.js").then((m) => m.claude),
52
- codex: () => import("./codex-D5ag-liR.js").then((m) => m.codex),
53
- serve: () => import("./serve-wEZoOijw.js").then((m) => m.serve),
50
+ start: () => import("./start-C6G5tu81.js").then((m) => m.start),
51
+ claude: () => import("./claude-Drv0roq1.js").then((m) => m.claude),
52
+ codex: () => import("./codex-kgVL5OyN.js").then((m) => m.codex),
53
+ serve: () => import("./serve-DPRZX-2j.js").then((m) => m.serve),
54
54
  models: () => import("./models-1KwEUgzd.js").then((m) => m.models),
55
55
  "check-usage": () => import("./check-usage-BsN3S4Fu.js").then((m) => m.checkUsage),
56
56
  debug: () => import("./debug-BXRPyeqO.js").then((m) => m.debug),
57
- "internal-stop-hook": () => import("./internal-stop-hook-BZg__ab6.js").then((m) => m.internalStopHook),
57
+ "internal-stop-hook": () => import("./internal-stop-hook-D8LIWryj.js").then((m) => m.internalStopHook),
58
58
  "internal-prompt-submit": () => import("./internal-prompt-submit-BTqRn4FW.js").then((m) => m.internalPromptSubmit),
59
59
  "internal-stop-review": () => import("./internal-stop-review-B8MmEyQ1.js").then((m) => m.internalStopReview),
60
60
  "internal-plan-review": () => import("./internal-plan-review-DVY-iV2x.js").then((m) => m.internalPlanReview),
@@ -63,8 +63,8 @@ const main = defineCommand({
63
63
  "internal-artifact-open": () => import("./internal-artifact-open-DRSxShAW.js").then((m) => m.internalArtifactOpen),
64
64
  "internal-first-mate-guard": () => import("./internal-first-mate-guard-Brh_uois.js").then((m) => m.internalFirstMateGuard),
65
65
  "internal-worker-guard": () => import("./internal-worker-guard-Dr6W83TC.js").then((m) => m.internalWorkerGuard),
66
- "internal-fast-dispatch-guard": () => import("./internal-fast-dispatch-guard-CYOWbxjX.js").then((m) => m.internalFastDispatchGuard),
67
- "internal-max-dispatch-guard": () => import("./internal-max-dispatch-guard-Bcjavapx.js").then((m) => m.internalMaxDispatchGuard)
66
+ "internal-fast-dispatch-guard": () => import("./internal-fast-dispatch-guard-ByzwIsxA.js").then((m) => m.internalFastDispatchGuard),
67
+ "internal-max-dispatch-guard": () => import("./internal-max-dispatch-guard-Sb6ACr_0.js").then((m) => m.internalMaxDispatchGuard)
68
68
  }
69
69
  });
70
70
  /**
@@ -35,7 +35,7 @@ const ENDPOINTS = {
35
35
  */
36
36
  function fastEndpointRequirement(modelId) {
37
37
  const id = modelId.replace(/(?:\[1m\])+$/i, "");
38
- if (id === "gemini-3.7-flash") return "chat";
38
+ if (id === "gemini-3.8-flash") return "chat";
39
39
  if (id === "gpt-5.6-luna" || id === "gpt-5.6-sol" || id === "grok-4.6") return "responses";
40
40
  if (id === "claude-opus-5") return "messages";
41
41
  }
@@ -66,7 +66,7 @@ function catalogModel(id) {
66
66
  const MAX_PROFILE_MODELS = Object.freeze({
67
67
  sol: "gpt-5.6-sol",
68
68
  luna: "gpt-5.6-luna",
69
- gemini: "gemini-3.7-flash",
69
+ gemini: "gemini-3.8-flash",
70
70
  grok: "grok-4.6",
71
71
  opus: "claude-opus-5"
72
72
  });
@@ -96,7 +96,7 @@ const MAX_PROFILE_NATIVE_MODELS = Object.freeze({
96
96
  Plan: MAX_PROFILE_MODELS.sol,
97
97
  "general-purpose": MAX_PROFILE_MODELS.luna,
98
98
  implementer: MAX_PROFILE_MODELS.gemini,
99
- reviewer: MAX_PROFILE_MODELS.gemini,
99
+ reviewer: MAX_PROFILE_MODELS.grok,
100
100
  brainstorm: MAX_PROFILE_MODELS.grok,
101
101
  "peer-review-coordinator": MAX_PROFILE_MODELS.luna
102
102
  });
@@ -172,12 +172,15 @@ function usableGemini(model) {
172
172
  function usableGrok(model) {
173
173
  return Boolean(model && hasToolCalls(model) && supportsEffort(model, "medium") && supportsEndpoint(model, "responses") && hasUsableLimits(model));
174
174
  }
175
+ function usableGrokReviewer(model) {
176
+ return Boolean(usableGrok(model) && supportsEffort(model, "high") && (model?.capabilities?.limits?.max_prompt_tokens ?? 0) >= 2e5);
177
+ }
175
178
  /**
176
179
  * Validate the fixed prerequisites for the raw `-m max` launch.
177
180
  *
178
181
  * Max is a deliberately strong, closed profile. Its lead, planning, reviewer,
179
182
  * and transcript-aware Advisor paths must all be present before any runtime
180
- * artifact is written: Sol, Luna, Gemini 3.7 Flash, and Opus 5 are mandatory.
183
+ * artifact is written: Sol, Luna, Gemini 3.8 Flash, and Opus 5 are mandatory.
181
184
  * Grok 4.6 is the only optional model. When present it supplies the preferred
182
185
  * medium-effort brainstorm/critic path, but it is never a max lead and its
183
186
  * sub-1M context must remain undecorated.
@@ -247,7 +250,7 @@ function maxGrokModel() {
247
250
  /** Grok replacement for max-profile surfaces that require high effort. */
248
251
  function maxGrokHighModel() {
249
252
  const model = catalogModel(MAX_PROFILE_MODELS.grok);
250
- return model && usableGrok(model) && supportsEffort(model, "high") ? model.id : void 0;
253
+ return model && usableGrokReviewer(model) ? model.id : void 0;
251
254
  }
252
255
  /** Max-only replacement for any standard Gemini Pro assignment. */
253
256
  function maxProReplacementModel() {
@@ -258,7 +261,16 @@ function maxOpusModel() {
258
261
  const model = catalogModel(MAX_PROFILE_MODELS.opus);
259
262
  return model && hasContextAtLeast(model, ONE_M_TOKENS) && model.capabilities?.supports?.adaptive_thinking === true && supportsEffort(model, "high") && supportsEndpoint(model, "messages") && hasUsableLimits(model) ? model.id : void 0;
260
263
  }
264
+ function maxLunaMaxModel() {
265
+ const model = catalogModel(MAX_PROFILE_MODELS.luna);
266
+ return model && hasToolCalls(model) && hasContextAtLeast(model, ONE_M_TOKENS) && supportsEffort(model, "max") && supportsEndpoint(model, "responses") && hasUsableLimits(model) ? model.id : void 0;
267
+ }
268
+ function maxReviewerModel() {
269
+ const grok = maxGrokHighModel();
270
+ if (grok) return grok;
271
+ return maxLunaMaxModel();
272
+ }
261
273
  //#endregion
262
- export { fastEndpointForCatalogId as _, MAX_PROFILE_NATIVE_AGENT_NAMES as a, stripTrailingOneMSuffix as b, formatMaxPrerequisiteFailure as c, maxAdvisorPinIsValid as d, maxGeminiModel as f, validateMaxProfilePrerequisites as g, maxProReplacementModel as h, MAX_PROFILE_MODELS as i, maxAdvisorEffortForModel as l, maxOpusModel as m, MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS as n, MAX_PROFILE_NATIVE_EFFORTS as o, maxGrokModel as p, MAX_PROFILE_LEAD_MODEL as r, MAX_PROFILE_NATIVE_MODELS as s, MAX_PROFILE_ADVISOR_INSTRUCTIONS as t, maxAdvisorModelFromPin as u, fastEndpointForModel as v, normalizeTrailingOneMSuffix as y };
274
+ export { validateMaxProfilePrerequisites as _, MAX_PROFILE_NATIVE_AGENT_NAMES as a, normalizeTrailingOneMSuffix as b, formatMaxPrerequisiteFailure as c, maxAdvisorPinIsValid as d, maxGeminiModel as f, maxReviewerModel as g, maxProReplacementModel as h, MAX_PROFILE_MODELS as i, maxAdvisorEffortForModel as l, maxOpusModel as m, MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS as n, MAX_PROFILE_NATIVE_EFFORTS as o, maxGrokModel as p, MAX_PROFILE_LEAD_MODEL as r, MAX_PROFILE_NATIVE_MODELS as s, MAX_PROFILE_ADVISOR_INSTRUCTIONS as t, maxAdvisorModelFromPin as u, fastEndpointForCatalogId as v, stripTrailingOneMSuffix as x, fastEndpointForModel as y };
263
275
 
264
- //# sourceMappingURL=max-profile-contract-BTXowh6k.js.map
276
+ //# sourceMappingURL=max-profile-contract-CiD5wyET.js.map