github-router 0.3.302 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{attribution-settings-B1xutIts.js → attribution-settings-dta8MTnd.js} +183 -92
- package/dist/attribution-settings-dta8MTnd.js.map +1 -0
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{claude-uRuzEH0W.js → claude-kr-Augld.js} +62 -115
- package/dist/claude-kr-Augld.js.map +1 -0
- package/dist/{codex-D5ag-liR.js → codex-B1qnVlzG.js} +4 -4
- package/dist/{codex-D5ag-liR.js.map → codex-B1qnVlzG.js.map} +1 -1
- package/dist/engine-B3w1TDgR.js +2 -0
- package/dist/{fast-profile-contract-K3P58nj3.js → fast-profile-contract-B6xn-Rzo.js} +26 -23
- package/dist/fast-profile-contract-B6xn-Rzo.js.map +1 -0
- package/dist/{gate-discovery-BTRVyKWu.js → gate-discovery-DOIm3XHF.js} +2 -2
- package/dist/{gate-discovery-BTRVyKWu.js.map → gate-discovery-DOIm3XHF.js.map} +1 -1
- package/dist/hooks.mjs +117 -46
- package/dist/hooks.sha256 +1 -1
- package/dist/{internal-fast-dispatch-guard-D2HcfXZv.js → internal-fast-dispatch-guard-BhpZ6LvO.js} +71 -15
- package/dist/internal-fast-dispatch-guard-BhpZ6LvO.js.map +1 -0
- package/dist/{internal-fast-dispatch-guard-CYOWbxjX.js → internal-fast-dispatch-guard-C6BNoOnn.js} +1 -1
- package/dist/{internal-max-dispatch-guard-BjSVXMvC.js → internal-max-dispatch-guard-DhAr8N_o.js} +28 -11
- package/dist/internal-max-dispatch-guard-DhAr8N_o.js.map +1 -0
- package/dist/{internal-max-dispatch-guard-Bcjavapx.js → internal-max-dispatch-guard-k52H-PdC.js} +1 -1
- package/dist/{internal-stop-hook-BZg__ab6.js → internal-stop-hook-CWffknT2.js} +2 -2
- package/dist/{internal-stop-hook-BZg__ab6.js.map → internal-stop-hook-CWffknT2.js.map} +1 -1
- package/dist/main.js +7 -7
- package/dist/{max-profile-contract-BTXowh6k.js → max-profile-contract-Ct62mtAR.js} +16 -4
- package/dist/max-profile-contract-Ct62mtAR.js.map +1 -0
- package/dist/{peer-mcp-personas-DwEiYeJS.js → peer-mcp-personas-BU5dNLxQ.js} +390 -349
- package/dist/peer-mcp-personas-BU5dNLxQ.js.map +1 -0
- package/dist/{provision-BLT1pCFy.js → provision-qNJgF5XD.js} +2 -2
- package/dist/{provision-BLT1pCFy.js.map → provision-qNJgF5XD.js.map} +1 -1
- package/dist/{serve-wEZoOijw.js → serve-T64kVeQN.js} +5 -5
- package/dist/{serve-wEZoOijw.js.map → serve-T64kVeQN.js.map} +1 -1
- package/dist/{server-setup-Zn5wkosb.js → server-setup-C_LAE8NI.js} +17 -11
- package/dist/server-setup-C_LAE8NI.js.map +1 -0
- package/dist/{start-DSqU8d8h.js → start-DHRToEyb.js} +3 -3
- package/dist/{start-DSqU8d8h.js.map → start-DHRToEyb.js.map} +1 -1
- package/package.json +1 -1
- package/dist/attribution-settings-B1xutIts.js.map +0 -1
- package/dist/claude-uRuzEH0W.js.map +0 -1
- package/dist/engine-2uMJ4Isk.js +0 -2
- package/dist/fast-profile-contract-K3P58nj3.js.map +0 -1
- package/dist/internal-fast-dispatch-guard-D2HcfXZv.js.map +0 -1
- package/dist/internal-max-dispatch-guard-BjSVXMvC.js.map +0 -1
- package/dist/max-profile-contract-BTXowh6k.js.map +0 -1
- package/dist/peer-mcp-personas-DwEiYeJS.js.map +0 -1
- package/dist/server-setup-Zn5wkosb.js.map +0 -1
package/dist/{internal-fast-dispatch-guard-D2HcfXZv.js → internal-fast-dispatch-guard-BhpZ6LvO.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as FAST_PROFILE_DELEGATION_GRAPH, o as FAST_PROFILE_NATIVE_AGENT_NAMES } from "./fast-profile-contract-
|
|
1
|
+
import { i as FAST_PROFILE_DELEGATION_GRAPH, o as FAST_PROFILE_NATIVE_AGENT_NAMES } from "./fast-profile-contract-B6xn-Rzo.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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 = { ...
|
|
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 (!
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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-
|
|
247
|
+
//# sourceMappingURL=internal-fast-dispatch-guard-BhpZ6LvO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-fast-dispatch-guard-BhpZ6LvO.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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"}
|
package/dist/{internal-fast-dispatch-guard-CYOWbxjX.js → internal-fast-dispatch-guard-C6BNoOnn.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as internalFastDispatchGuard } from "./internal-fast-dispatch-guard-
|
|
1
|
+
import { n as internalFastDispatchGuard } from "./internal-fast-dispatch-guard-BhpZ6LvO.js";
|
|
2
2
|
export { internalFastDispatchGuard };
|
package/dist/{internal-max-dispatch-guard-BjSVXMvC.js → internal-max-dispatch-guard-DhAr8N_o.js}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { a as MAX_PROFILE_NATIVE_AGENT_NAMES, s as MAX_PROFILE_NATIVE_MODELS } from "./max-profile-contract-Ct62mtAR.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
|
|
@@ -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;
|
|
@@ -131,24 +131,25 @@ function decideMaxDispatchGuard(stdin) {
|
|
|
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 (
|
|
145
|
-
else updatedInput.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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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-
|
|
215
|
+
//# sourceMappingURL=internal-max-dispatch-guard-DhAr8N_o.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-max-dispatch-guard-DhAr8N_o.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.7-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.7-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.7 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":";;;;;AAOA,MAAa,4BAA4B;AAEzC,MAAa,4BAA4B;AAEzC,MAAM,uCAAuB,IAAI,IAAY,CAC3C,GAAG,gCACH,yBACF,CAAC;;AAGD,IAAa,+BAAb,cAAkD,MAAM;CACtD,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,gCACd,YACA,uBACM;CACN,IAAI,cAAc,CAAC,uBACjB,MAAM,IAAI,6BACR,iIACF;AAEJ;AAEA,MAAM,0CAA0B,IAAI,IAAI;CAAC;CAAgB;CAAoB;AAAU,CAAC;;;;;;;;;AAUxF,MAAa,iCAAiC,OAAO,OAAO;CAC1D;CACA;CACA;CACA;AACF,CAAU;AAEV,MAAM,mCAAmC,IAAI,IAC3C,8BACF;AAEA,SAAS,2BAA2B,OAAyB;CAC3D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,OAAO,iCAAiC,IAAI,UAAU,MAAM,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC;AACnF;AAiBA,SAAS,YAAY,OAAgC,GAAG,MAAoC;CAC1F,MAAM,SAAS,KACZ,QAAQ,QAAQ,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,CAAC,CAAC,CACjE,KAAK,QAAQ,MAAM,IAAI,CAAC,CACxB,QAAQ,UAAU,UAAU,KAAA,KAAa,UAAU,IAAI;CAC1D,IAAI,OAAO,WAAW,GAAG,OAAO,KAAA;CAChC,IAAI,OAAO,MAAM,UAAU,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,WAAW,CAAC,GAAG,OAAO,KAAA;CAC3F,MAAM,WAAW,CAAC,GAAG,IAAI,IAAK,OAAoB,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAAC;CAC/E,OAAO,SAAS,WAAW,IAAI,SAAS,KAAK,KAAA;AAC/C;AAEA,SAAS,UAAU,OAAuB;CACxC,OAAO,MAAM,QAAQ,iBAAiB,EAAE;AAC1C;AAEA,SAAgB,0BAA0B,OAAoC;CAC5E,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,WAAW,GAAG,OAAO,KAAA;CAEnE,MAAM,OAAO,UADG,MAAM,KACO,CAAC;CAC9B,OAAO,wBAAwB,IAAI,IAAI,IACnC,SAAS,aAAa,OAAO,GAAG,KAAK,QACrC,KAAA;AACN;AAEA,SAAgB,2BACd,OACA,QACoB;CACpB,IAAI,WAAW,KAAA,KAAa,WAAW,MAAM,OAAO,KAAA;CACpD,IAAI,OAAO,WAAW,UAAU,OAAO,KAAA;CAUvC,KATuC,UAAU,gBAC7C;EAAC;EAAQ;EAAS;CAAK,IACvB,UAAU,iBACR;EAAC;EAAQ;EAAO;EAAU;EAAQ;EAAS;CAAK,IAChD,UAAU,cAAc,UAAU,qBAGhC;EAAC;EAAO;EAAU;CAAM,IACxB,CAAC,EAAA,CACG,SAAS,MAAgB,GAAG,OAAO;AAEjD;;;;;;;;AASA,SAAgB,uBACd,OACA,OAAgC,CAAC,GACZ;CACrB,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,UAAU,WAAW,KAAK,MAAM,KAAK,IAAI;CAC3D,QAAQ;EACN,OAAO;GAAE,SAAS;GAAO,QAAQ;GAA+C,SAAS;EAAO;CAClG;CACA,IAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAC/D,OAAO;EAAE,SAAS;EAAO,QAAQ;EAAuD,SAAS;CAAO;CAE1G,MAAM,UAAU;CAChB,IAAI,OAAO,QAAQ,cAAc,UAC/B,OAAO;EAAE,SAAS;EAAO,QAAQ;EAAsD,SAAS;CAAO;CAEzG,IAAI,CAAC,iBAAiB,KAAK,QAAQ,SAAS,GAC1C,OAAO;EAAE,SAAS;EAAM,SAAS;CAAqB;CAExD,IAAI,CAAC,QAAQ,cAAc,OAAO,QAAQ,eAAe,YAAY,MAAM,QAAQ,QAAQ,UAAU,GACnG,OAAO;EAAE,SAAS;EAAO,QAAQ,wBAAwB,QAAQ,UAAU;EAAoC,SAAS;CAAO;CAEjI,MAAM,YAAY,QAAQ;CAC1B,MAAM,SAAS,YAAY,WAAW,iBAAiB,cAAc;CACrE,IAAI,CAAC,UAAU,CAAC,qBAAqB,IAAI,MAAM,GAC7C,OAAO;EAAE,SAAS;EAAO,QAAQ,8CAA8C,CAAC,GAAG,gCAAgC,yBAAyB,CAAC,CAAC,KAAK,IAAI;EAAK,SAAS;CAAO;CAE9K,MAAM,eAAe;CACrB,MAAM,aAAa,UAAU;CAC7B,MAAM,cAAc,2BAA2B,UAAU;CACzD,MAAM,kBAAkB,eAAe,KAAA,KAAa,cAChD,KAAA,IACA,0BAA0B,UAAU;CACxC,IAAI,eAAe,KAAA,KAAa,CAAC,eAAe,CAAC,iBAC/C,OAAO;EAAE,SAAS;EAAO,QAAQ;EAAyH;EAAQ,SAAS;CAAO;CAEpL,MAAM,kBAAkB,UAAU,YAAY,UAAU;CAUxD,MAAM,SAAS,2BATQ,kBACnB,UAAU,eAAe,IACzB,iBAAA,kBAGE,iBACA,iBAAiB,cAAc,KAAK,gBAClC,UAAU,KAAK,aAAa,IAC5B,0BAA0B,eACwB,eAAe;CACzE,KAAK,UAAU,aAAa,KAAA,KAAa,UAAU,WAAW,KAAA,MAAc,CAAC,QAC3E,OAAO;EAAE,SAAS;EAAO,QAAQ;EAAkF;EAAQ,SAAS;CAAO;CAO7I,MAAM,mBALsB,iBAAiB,cACxC,CAAC,mBACD,KAAK,gBACN,KAAK,mBAAmB,UAAU,KAAK,aAAa,MAAM,iBAAiB,QAAQ,UACnF,KAAA,MAC2C;CAC/C,MAAM,eAAe,EAAE,GAAG,UAAU;CACpC,IAAI,iBAAiB,aAAa,QAAQ;MACrC,OAAO,aAAa;CACzB,IAAI,iBAIF,IAAI,UAAU,aAAa,KAAA,GAAW,aAAa,WAAW;MACzD,aAAa,SAAS;CAE7B,IAAI,UAAU,aAAa,KAAA,KAAa,UAAU,WAAW,KAAA,GAC3D,OAAO,aAAa;CAEtB,IAAI,UAAU,UAAU,KAAA,KAAa,oBAAoB,KAAA,GAAW,OAAO,aAAa;CACxF,OAAO;EAAE,SAAS;EAAM;EAAQ,QAAQ;EAAiB;EAAc,SAAS;CAAQ;AAC1F;AAEA,SAAgB,uBAAuB,cAA+C;CACpF,OAAO,KAAK,UAAU,EACpB,oBAAoB;EAClB,eAAe;EACf,oBAAoB;EACpB;CACF,EACF,CAAC;AACH;AAEA,SAAgB,sBAAsB,QAAwB;CAC5D,OAAO,KAAK,UAAU,EACpB,oBAAoB;EAClB,eAAe;EACf,oBAAoB;EACpB,0BAA0B;CAC5B,EACF,CAAC;AACH;;;AChNA,SAAS,gBAAwB;CAC/B,IAAI;EACF,IAAI,QAAQ,MAAM,OAAO,OAAO;EAChC,OAAO,aAAa,GAAG,MAAM;CAC/B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAa,2BAA2B,cAAc;CACpD,MAAM;EACJ,MAAM;EACN,aAAa;CACf;CACA,MAAM;EACJ,eAAe;GACb,MAAM;GACN,aAAa;EACf;EACA,gBAAgB;GACd,MAAM;GACN,aAAa;EACf;CACF;CACA,IAAI,EAAE,QAAQ;EACZ,MAAM,WAAW,uBAAuB,cAAc,GAAG;GACvD,eAAe,KAAK;GACpB,gBAAgB,KAAK,mBAAmB,QAAQ,QAAQ;EAC1D,CAAC;EACD,IAAI,CAAC,SAAS,WAAW,SAAS,QAChC,QAAQ,OAAO,MAAM,sBAAsB,SAAS,MAAM,CAAC;OACtD,IAAI,SAAS,YAAY,WAAW,SAAS,cAClD,QAAQ,OAAO,MAAM,uBAAuB,SAAS,YAAY,CAAC;EAEpE,QAAQ,WAAW;CACrB;AACF,CAAC;AAED,SAAgB,iCACd,YACA,OAAoE,CAAC,GAC7D;CACR,MAAM,QAAkB,CAAC;CACzB,IAAI,KAAK,eAAe,MAAM,KAAK,oBAAoB,KAAK,cAAc,EAAE;CAC5E,IAAI,KAAK,gBAAgB,MAAM,KAAK,oBAAoB,KAAK,gBAAgB;CAC7E,OAAO,iBACL,YACA,CAAC,+BAA+B,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG,CACpD;AACF"}
|
package/dist/{internal-max-dispatch-guard-Bcjavapx.js → internal-max-dispatch-guard-k52H-PdC.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as internalMaxDispatchGuard } from "./internal-max-dispatch-guard-
|
|
1
|
+
import { n as internalMaxDispatchGuard } from "./internal-max-dispatch-guard-DhAr8N_o.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-
|
|
7
|
+
import { a as checksForDescriptor, n as readDiscoveredGate, o as descriptorHash, s as parseGateDescriptor } from "./gate-discovery-DOIm3XHF.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-
|
|
260
|
+
//# sourceMappingURL=internal-stop-hook-CWffknT2.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-CWffknT2.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-
|
|
51
|
-
claude: () => import("./claude-
|
|
52
|
-
codex: () => import("./codex-
|
|
53
|
-
serve: () => import("./serve-
|
|
50
|
+
start: () => import("./start-DHRToEyb.js").then((m) => m.start),
|
|
51
|
+
claude: () => import("./claude-kr-Augld.js").then((m) => m.claude),
|
|
52
|
+
codex: () => import("./codex-B1qnVlzG.js").then((m) => m.codex),
|
|
53
|
+
serve: () => import("./serve-T64kVeQN.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-
|
|
57
|
+
"internal-stop-hook": () => import("./internal-stop-hook-CWffknT2.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-
|
|
67
|
-
"internal-max-dispatch-guard": () => import("./internal-max-dispatch-guard-
|
|
66
|
+
"internal-fast-dispatch-guard": () => import("./internal-fast-dispatch-guard-C6BNoOnn.js").then((m) => m.internalFastDispatchGuard),
|
|
67
|
+
"internal-max-dispatch-guard": () => import("./internal-max-dispatch-guard-k52H-PdC.js").then((m) => m.internalMaxDispatchGuard)
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
/**
|
|
@@ -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.
|
|
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,6 +172,9 @@ 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
|
*
|
|
@@ -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 &&
|
|
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 {
|
|
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-
|
|
276
|
+
//# sourceMappingURL=max-profile-contract-Ct62mtAR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-profile-contract-Ct62mtAR.js","names":[],"sources":["../src/lib/model-suffix.ts","../src/lib/fast-endpoint.ts","../src/lib/max-profile-contract.ts"],"sourcesContent":["/**\n * Normalize Claude Code's local 1M-context decoration at model boundaries.\n *\n * `[1m]` is a client-side accounting marker, not part of a Copilot catalog id.\n * Claude Code can append its own marker to a picker value that is already\n * decorated by the router, producing a reachable doubled suffix. Keep the\n * helper dependency-free so launch/profile code and request preprocessing use\n * exactly the same trailing-suffix rule without an import cycle.\n */\nexport interface TrailingOneMSuffix {\n base: string\n hadSuffix: boolean\n}\n\n/** Strip every trailing `[1m]` marker, case-insensitively. */\nexport function stripTrailingOneMSuffix(id: string): TrailingOneMSuffix {\n const match = id.match(/(?:\\[1m\\])+$/i)\n if (!match || match.index === undefined) {\n return { base: id, hadSuffix: false }\n }\n return { base: id.slice(0, match.index), hadSuffix: true }\n}\n\n/** Reapply at most one canonical `[1m]` marker when the input had one. */\nexport function normalizeTrailingOneMSuffix(id: string): string {\n const { base, hadSuffix } = stripTrailingOneMSuffix(id)\n return hadSuffix ? `${base}[1m]` : base\n}\n\n/** Append one marker without ever creating a doubled suffix. */\nexport function withTrailingOneMSuffix(id: string): string {\n const { base } = stripTrailingOneMSuffix(id)\n return `${base}[1m]`\n}\n","import type { Model } from \"~/services/copilot/get-models\"\n\n// Fast endpoint policy is intentionally kept separate from the standard\n// pickEndpoint() resolver. See fastEndpointRequirement below.\n\n/** Endpoint clients used by the fast profile's fixed roster. */\nexport type FastEndpoint = \"chat\" | \"responses\" | \"messages\"\n\nconst ENDPOINTS: Readonly<Record<FastEndpoint, ReadonlySet<string>>> = {\n chat: new Set([\"/chat/completions\", \"/v1/chat/completions\"]),\n responses: new Set([\"/responses\", \"/v1/responses\"]),\n messages: new Set([\"/messages\", \"/v1/messages\"]),\n}\n\n/**\n * Return the endpoint mandated by a fast-profile roster model.\n *\n * This is intentionally independent of the standard `pickEndpoint`: that\n * resolver prefers chat when a catalog entry advertises both clients, while\n * fast roles have a fixed provider contract. Gemini stays on chat; Luna, Sol,\n * and Grok stay on Responses; the Opus Oracle stays on native Messages.\n * Unknown ids return undefined so a fast caller cannot guess a transport.\n */\nexport function fastEndpointRequirement(modelId: string): FastEndpoint | undefined {\n const id = modelId.replace(/(?:\\[1m\\])+$/i, \"\")\n if (id === \"gemini-3.7-flash\") return \"chat\"\n if (id === \"gpt-5.6-luna\" || id === \"gpt-5.6-sol\" || id === \"grok-4.6\") {\n return \"responses\"\n }\n if (id === \"claude-opus-5\") return \"messages\"\n return undefined\n}\n\n/**\n * Resolve the fixed fast transport only when the catalog explicitly advertises\n * the required endpoint. An entry that advertises both still resolves to the\n * policy endpoint, regardless of advertisement order.\n */\nexport function fastEndpointForModel(model: Model): FastEndpoint | undefined {\n const required = fastEndpointRequirement(model.id)\n if (!required) return undefined\n const advertised = model.supported_endpoints\n if (!Array.isArray(advertised) || advertised.length === 0) return undefined\n return advertised.some((endpoint) => ENDPOINTS[required].has(endpoint))\n ? required\n : undefined\n}\n\n/** Resolve a fast endpoint from a catalog id, without selecting a fallback. */\nexport function fastEndpointForCatalogId(\n modelId: string,\n catalog: ReadonlyArray<Model> | undefined,\n): FastEndpoint | undefined {\n const found = catalog?.find((model) => model.id === modelId)\n return found ? fastEndpointForModel(found) : undefined\n}\n\n/** Human-readable endpoint name for prerequisite diagnostics. */\nexport function fastEndpointLabel(endpoint: FastEndpoint): string {\n return endpoint === \"chat\"\n ? \"Chat Completions\"\n : endpoint === \"responses\"\n ? \"Responses\"\n : \"Messages\"\n}\n","import type { Model, ModelsResponse } from \"~/services/copilot/get-models\"\nimport type { Effort } from \"./reasoning-effort\"\nimport { fastEndpointForModel } from \"./fast-endpoint\"\nimport { stripTrailingOneMSuffix } from \"./model-suffix\"\nimport { state } from \"./state\"\n\n// Max uses the same context threshold as Claude Code's `[1m]` accounting.\nexport const MAX_REQUIRED_CONTEXT_TOKENS = 1_000_000\n\nconst ONE_M_TOKENS = MAX_REQUIRED_CONTEXT_TOKENS\n\nfunction catalogModels(): Array<Model> {\n return state.models?.data ?? []\n}\n\nfunction catalogModel(id: string): Model | undefined {\n return catalogModels().find((model) => model.id === id)\n}\n\n\nexport const MAX_PROFILE_MODELS = Object.freeze({\n sol: \"gpt-5.6-sol\",\n luna: \"gpt-5.6-luna\",\n gemini: \"gemini-3.7-flash\",\n grok: \"grok-4.6\",\n opus: \"claude-opus-5\",\n} as const)\n\nexport const MAX_PROFILE_NATIVE_AGENT_NAMES = [\n \"Explore\",\n \"Plan\",\n \"general-purpose\",\n \"implementer\",\n \"reviewer\",\n \"brainstorm\",\n \"peer-review-coordinator\",\n] as const\n\nexport type MaxProfileNativeAgentName =\n (typeof MAX_PROFILE_NATIVE_AGENT_NAMES)[number]\n\nexport const MAX_PROFILE_NATIVE_EFFORTS = Object.freeze({\n Explore: \"high\",\n Plan: \"high\",\n \"general-purpose\": \"max\",\n implementer: \"high\",\n reviewer: \"high\",\n brainstorm: \"medium\",\n \"peer-review-coordinator\": \"max\",\n} as const satisfies Record<MaxProfileNativeAgentName, Effort>)\n\n/** Default model family behind each native role's frontmatter. The optional\n * Grok brainstorm path may fall back to Gemini, whose allowed effort ladder is\n * identical for the values accepted by the max dispatch guard. */\nexport const MAX_PROFILE_NATIVE_MODELS = Object.freeze({\n Explore: MAX_PROFILE_MODELS.luna,\n Plan: MAX_PROFILE_MODELS.sol,\n \"general-purpose\": MAX_PROFILE_MODELS.luna,\n implementer: MAX_PROFILE_MODELS.gemini,\n reviewer: MAX_PROFILE_MODELS.grok,\n brainstorm: MAX_PROFILE_MODELS.grok,\n \"peer-review-coordinator\": MAX_PROFILE_MODELS.luna,\n} as const satisfies Record<MaxProfileNativeAgentName, string>)\n\nexport const MAX_PROFILE_LEAD_MODEL = MAX_PROFILE_MODELS.sol\nexport const MAX_PROFILE_ADVISOR_MODEL = MAX_PROFILE_MODELS.opus\nexport const MAX_PROFILE_ADVISOR_EFFORT = \"high\" as const\nexport const MAX_PROFILE_ADVISOR_INSTRUCTIONS = `# Advisor Tool\n\nYou have access to an optional, lead-only, transcript-aware Max Advisor. It offers a focused second opinion; it is not a supervisor, approver, workflow gate, or substitute for your own reasoning. You keep decision ownership.\n\nUse it only when a consequential uncertainty remains after direct investigation and cannot be settled by repository evidence, a focused command or test, Plan, reviewer, or a fresh-context peer: conflicting evidence, a materially changed assumption, a genuinely non-converging approach, or a hard-to-reverse trade-off. State the precise unresolved question immediately before calling it.\n\nDo not call it automatically before substantive work or completion, for routine progress, while waiting, after ordinary tool output, for directly verifiable facts, planner approval, reviewer verification, or as reassurance for a decision the evidence already supports.\n\nTreat the result as advice, not authority. Weigh it against the user's intent and verified evidence. Consult again only when materially new evidence creates a different question or directly conflicts with the earlier advice.`\n\nexport function maxAdvisorModelFromPin(pinned: string | undefined, opusModel?: string): string {\n const trimmed = pinned?.trim()\n const base = trimmed ? stripTrailingOneMSuffix(trimmed).base : undefined\n if (base === MAX_PROFILE_MODELS.sol || base === MAX_PROFILE_MODELS.opus) {\n return base\n }\n return opusModel ?? MAX_PROFILE_MODELS.opus\n}\n\nexport function maxAdvisorPinIsValid(pinned: string | undefined): boolean {\n const trimmed = pinned?.trim()\n const base = trimmed ? stripTrailingOneMSuffix(trimmed).base : trimmed\n return base === undefined\n || base === \"\"\n || base === MAX_PROFILE_MODELS.sol\n || base === MAX_PROFILE_MODELS.opus\n}\n\nexport function maxAdvisorModelFromPinStrict(pinned: string | undefined, opusModel?: string): string {\n if (!maxAdvisorPinIsValid(pinned)) return opusModel ?? MAX_PROFILE_MODELS.opus\n return maxAdvisorModelFromPin(pinned, opusModel)\n}\n\nexport function maxStandInAvailable(): boolean {\n return usableSol(catalogModel(MAX_PROFILE_MODELS.sol))\n && maxOpusModel() !== undefined\n && (maxGrokHighModel() !== undefined || maxGeminiModel() !== undefined)\n}\nexport function maxAdvisorEffortForModel(_model: string): Effort {\n return MAX_PROFILE_ADVISOR_EFFORT\n}\n\nexport const MAX_PROFILE_NATIVE_MODEL_IDS = Object.freeze([\n MAX_PROFILE_MODELS.luna,\n MAX_PROFILE_MODELS.gemini,\n MAX_PROFILE_MODELS.grok,\n] as const)\n\nexport const MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS = Object.freeze([\n MAX_PROFILE_MODELS.sol,\n MAX_PROFILE_MODELS.luna,\n MAX_PROFILE_MODELS.gemini,\n MAX_PROFILE_MODELS.opus,\n] as const)\n\nexport type MaxProfileAllowedLeadModel =\n (typeof MAX_PROFILE_ALLOWED_LEAD_MODEL_IDS)[number]\n\nexport interface MaxProfilePrerequisiteCheck {\n ok: boolean\n missing: ReadonlyArray<string>\n /** Resolved optional/third-lab assignments for the launch. */\n models: {\n sol?: Model\n luna?: Model\n gemini?: Model\n grok?: Model\n opus?: Model\n thirdLab?: \"gemini\" | \"grok\"\n }\n}\n\nfunction findModel(\n catalog: ModelsResponse | undefined,\n id: string,\n): Model | undefined {\n return catalog?.data?.find((model) => model.id === id)\n}\n\nfunction hasToolCalls(model: Model | undefined): boolean {\n return model?.capabilities?.supports?.tool_calls === true\n}\n\nfunction hasContextAtLeast(model: Model | undefined, tokens: number): boolean {\n return (model?.capabilities?.limits?.max_context_window_tokens ?? 0) >= tokens\n}\n\nfunction supportsEffort(model: Model | undefined, effort: Effort): boolean {\n const ladder = model?.capabilities?.supports?.reasoning_effort\n return Array.isArray(ladder) && ladder.includes(effort)\n}\n\nfunction hasUsableLimits(model: Model | undefined): boolean {\n const limits = model?.capabilities?.limits\n const prompt = limits?.max_prompt_tokens\n const output = limits?.max_output_tokens\n return (\n typeof prompt === \"number\"\n && Number.isFinite(prompt)\n && prompt > 0\n && typeof output === \"number\"\n && Number.isFinite(output)\n && output > 0\n && (limits?.max_context_window_tokens ?? 0) >= prompt\n )\n}\n\nfunction supportsEndpoint(\n model: Model | undefined,\n endpoint: \"chat\" | \"responses\" | \"messages\",\n): boolean {\n return model !== undefined && fastEndpointForModel(model) === endpoint\n}\n\nfunction validateRequiredModel(\n model: Model | undefined,\n id: string,\n requirements: ReadonlyArray<[string, boolean]>,\n missing: string[],\n): void {\n if (!model) {\n missing.push(`${id}: absent from the live catalog`)\n return\n }\n for (const [message, ok] of requirements) {\n if (!ok) missing.push(`${id}: ${message}`)\n }\n}\n\nfunction usableSol(model: Model | undefined): boolean {\n return Boolean(\n model\n && hasToolCalls(model)\n && hasContextAtLeast(model, ONE_M_TOKENS)\n && supportsEffort(model, \"high\")\n && supportsEndpoint(model, \"responses\")\n && hasUsableLimits(model)\n )\n}\n\nfunction usableGemini(model: Model | undefined): boolean {\n return Boolean(\n model\n && hasToolCalls(model)\n && hasContextAtLeast(model, ONE_M_TOKENS)\n && supportsEffort(model, \"high\")\n && supportsEndpoint(model, \"chat\")\n && hasUsableLimits(model),\n )\n}\n\nfunction usableGrok(model: Model | undefined): boolean {\n return Boolean(\n model\n && hasToolCalls(model)\n && supportsEffort(model, \"medium\")\n && supportsEndpoint(model, \"responses\")\n && hasUsableLimits(model),\n )\n}\n\nfunction usableGrokReviewer(model: Model | undefined): boolean {\n return Boolean(\n usableGrok(model)\n && supportsEffort(model, \"high\")\n && (model?.capabilities?.limits?.max_prompt_tokens ?? 0) >= 200_000,\n )\n}\n\n/**\n * Validate the fixed prerequisites for the raw `-m max` launch.\n *\n * Max is a deliberately strong, closed profile. Its lead, planning, reviewer,\n * and transcript-aware Advisor paths must all be present before any runtime\n * artifact is written: Sol, Luna, Gemini 3.7 Flash, and Opus 5 are mandatory.\n * Grok 4.6 is the only optional model. When present it supplies the preferred\n * medium-effort brainstorm/critic path, but it is never a max lead and its\n * sub-1M context must remain undecorated.\n */\nexport function validateMaxProfilePrerequisites(\n catalog: ModelsResponse | undefined,\n): MaxProfilePrerequisiteCheck {\n const missing: string[] = []\n const sol = findModel(catalog, MAX_PROFILE_MODELS.sol)\n const luna = findModel(catalog, MAX_PROFILE_MODELS.luna)\n const gemini = findModel(catalog, MAX_PROFILE_MODELS.gemini)\n const grok = findModel(catalog, MAX_PROFILE_MODELS.grok)\n const opus = findModel(catalog, MAX_PROFILE_MODELS.opus)\n\n validateRequiredModel(\n sol,\n MAX_PROFILE_MODELS.sol,\n [\n [\"does not advertise tool_calls\", hasToolCalls(sol)],\n [\"advertised context window is below 1M\", hasContextAtLeast(sol, ONE_M_TOKENS)],\n [\"does not advertise a high reasoning effort\", supportsEffort(sol, \"high\")],\n [\"does not advertise a supported Responses endpoint\", supportsEndpoint(sol, \"responses\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(sol)],\n ],\n missing,\n )\n validateRequiredModel(\n luna,\n MAX_PROFILE_MODELS.luna,\n [\n [\"does not advertise tool_calls\", hasToolCalls(luna)],\n [\"advertised context window is below 1M\", hasContextAtLeast(luna, ONE_M_TOKENS)],\n [\"does not advertise a high reasoning effort\", supportsEffort(luna, \"high\")],\n [\"does not advertise a max reasoning effort\", supportsEffort(luna, \"max\")],\n [\"does not advertise a supported Responses endpoint\", supportsEndpoint(luna, \"responses\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(luna)],\n ],\n missing,\n )\n validateRequiredModel(\n gemini,\n MAX_PROFILE_MODELS.gemini,\n [\n [\"does not advertise tool_calls\", hasToolCalls(gemini)],\n [\"advertised context window is below 1M\", hasContextAtLeast(gemini, ONE_M_TOKENS)],\n [\"does not advertise a high reasoning effort\", supportsEffort(gemini, \"high\")],\n [\"does not advertise a supported Chat Completions endpoint\", supportsEndpoint(gemini, \"chat\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(gemini)],\n ],\n missing,\n )\n validateRequiredModel(\n opus,\n MAX_PROFILE_MODELS.opus,\n [\n [\"advertised context window is below 1M\", hasContextAtLeast(opus, ONE_M_TOKENS)],\n [\"does not advertise adaptive_thinking\", opus?.capabilities?.supports?.adaptive_thinking === true],\n [\"does not advertise a high reasoning effort\", supportsEffort(opus, \"high\")],\n [\"does not advertise a supported Messages endpoint\", supportsEndpoint(opus, \"messages\")],\n [\"has no usable max_prompt_tokens/max_output_tokens limits\", hasUsableLimits(opus)],\n ],\n missing,\n )\n\n // Grok is intentionally optional. A malformed optional entry is simply\n // omitted from the max projection; it must never make launch fail or be\n // decorated as a 1M model.\n const geminiUsable = usableGemini(gemini)\n const grokUsable = usableGrok(grok)\n\n return {\n ok: missing.length === 0,\n missing,\n models: {\n sol: sol && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.sol}:`)) ? sol : undefined,\n luna: luna && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.luna}:`)) ? luna : undefined,\n gemini: geminiUsable ? gemini : undefined,\n grok: grokUsable ? grok : undefined,\n opus: opus && !missing.some((entry) => entry.startsWith(`${MAX_PROFILE_MODELS.opus}:`)) ? opus : undefined,\n thirdLab: grokUsable && supportsEffort(grok, \"high\")\n ? \"grok\"\n : geminiUsable\n ? \"gemini\"\n : undefined,\n },\n }\n}\n\nexport function formatMaxPrerequisiteFailure(\n missing: ReadonlyArray<string>,\n): string {\n return (\n \"github-router claude -m max requires the following live-catalog capabilities, \"\n + \"which this account's catalog does not fully provide:\\n\"\n + missing.map((entry) => ` - ${entry}`).join(\"\\n\")\n + \"\\n\\nMax launches do not fall back to arbitrary older models. \"\n + \"Use plain `github-router claude` for the standard profile.\"\n )\n}\n\nexport function maxModelIsOneM(\n model: Model | undefined,\n): boolean {\n return (\n (model?.capabilities?.limits?.max_context_window_tokens ?? 0) >= ONE_M_TOKENS\n )\n}\n\nexport function maxModelSupportsEffort(\n model: Model | undefined,\n effort: Effort,\n): boolean {\n return supportsEffort(model, effort)\n}\n\nexport function maxModelHasUsableLimits(model: Model | undefined): boolean {\n return hasUsableLimits(model)\n}\n\nexport function maxThirdLabModel(): string | undefined {\n const gemini = catalogModel(MAX_PROFILE_MODELS.gemini)\n if (usableGemini(gemini)) return MAX_PROFILE_MODELS.gemini\n const grok = catalogModel(MAX_PROFILE_MODELS.grok)\n return usableGrok(grok) ? MAX_PROFILE_MODELS.grok : undefined\n}\n\nexport function maxGeminiModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.gemini)\n return model && usableGemini(model) ? model.id : undefined\n}\n\nexport function maxGrokModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.grok)\n return model && usableGrok(model) ? model.id : undefined\n}\n\n/** Grok replacement for max-profile surfaces that require high effort. */\nexport function maxGrokHighModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.grok)\n return model && usableGrokReviewer(model) ? model.id : undefined\n}\n\n/** Max-only replacement for any standard Gemini Pro assignment. */\nexport function maxProReplacementModel():\n | typeof MAX_PROFILE_MODELS.grok\n | typeof MAX_PROFILE_MODELS.gemini\n | undefined {\n if (maxGrokHighModel()) return MAX_PROFILE_MODELS.grok\n if (maxGeminiModel()) return MAX_PROFILE_MODELS.gemini\n return undefined\n}\n\nexport function maxOpusModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.opus)\n return model && hasContextAtLeast(model, ONE_M_TOKENS)\n && model.capabilities?.supports?.adaptive_thinking === true\n && supportsEffort(model, \"high\")\n && supportsEndpoint(model, \"messages\")\n && hasUsableLimits(model)\n ? model.id\n : undefined\n}\n\nexport function maxLunaMaxModel(): string | undefined {\n const model = catalogModel(MAX_PROFILE_MODELS.luna)\n return model\n && hasToolCalls(model)\n && hasContextAtLeast(model, ONE_M_TOKENS)\n && supportsEffort(model, \"max\")\n && supportsEndpoint(model, \"responses\")\n && hasUsableLimits(model)\n ? model.id\n : undefined\n}\n\nexport function maxReviewerModel(): string | undefined {\n const grok = maxGrokHighModel()\n if (grok) return grok\n return maxLunaMaxModel()\n}\n\nexport function maxCatalogModel(id: string): Model | undefined {\n return catalogModel(id)\n}\n\nexport function maxCatalogModels(): ReadonlyArray<Model> {\n return catalogModels()\n}"],"mappings":"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"}
|