github-router 0.3.309 → 0.3.311
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-Ce6s_01h.js → attribution-settings-jLxSnmZT.js} +21 -20
- package/dist/attribution-settings-jLxSnmZT.js.map +1 -0
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{claude-i4L8GZbI.js → claude-DhvQETaa.js} +21 -11
- package/dist/claude-DhvQETaa.js.map +1 -0
- package/dist/{codex-GZGqYj0M.js → codex-RJQPJ--K.js} +4 -4
- package/dist/{codex-GZGqYj0M.js.map → codex-RJQPJ--K.js.map} +1 -1
- package/dist/engine-DuiPBB4G.js +2 -0
- package/dist/{fast-profile-contract-BWCJy6iL.js → fast-profile-contract-_DhIWank.js} +7 -3
- package/dist/fast-profile-contract-_DhIWank.js.map +1 -0
- package/dist/{gate-discovery-DkJf7d-y.js → gate-discovery-DDkRvyYL.js} +2 -2
- package/dist/{gate-discovery-DkJf7d-y.js.map → gate-discovery-DDkRvyYL.js.map} +1 -1
- package/dist/hooks.mjs +3 -1
- package/dist/hooks.sha256 +1 -1
- package/dist/{internal-fast-dispatch-guard-kzSKB0yV.js → internal-fast-dispatch-guard-19xQEouu.js} +2 -2
- package/dist/{internal-fast-dispatch-guard-kzSKB0yV.js.map → internal-fast-dispatch-guard-19xQEouu.js.map} +1 -1
- package/dist/{internal-fast-dispatch-guard-CrsZI0pO.js → internal-fast-dispatch-guard-D8YNlnuQ.js} +1 -1
- package/dist/{internal-max-dispatch-guard-lS4usOV5.js → internal-max-dispatch-guard-DZ7KLWxP.js} +2 -2
- package/dist/{internal-max-dispatch-guard-lS4usOV5.js.map → internal-max-dispatch-guard-DZ7KLWxP.js.map} +1 -1
- package/dist/{internal-max-dispatch-guard-DF-f8VwE.js → internal-max-dispatch-guard-_Pp4LltM.js} +1 -1
- package/dist/{internal-stop-hook-XRtB33hW.js → internal-stop-hook-YyYVQbbN.js} +2 -2
- package/dist/{internal-stop-hook-XRtB33hW.js.map → internal-stop-hook-YyYVQbbN.js.map} +1 -1
- package/dist/main.js +7 -7
- package/dist/{max-profile-contract-v2Q6eN5w.js → max-profile-contract-BcrMlrmo.js} +2 -2
- package/dist/max-profile-contract-BcrMlrmo.js.map +1 -0
- package/dist/{peer-mcp-personas-C_38ckts.js → peer-mcp-personas-O35uSWsX.js} +183 -31
- package/dist/{peer-mcp-personas-C_38ckts.js.map → peer-mcp-personas-O35uSWsX.js.map} +1 -1
- package/dist/{provision-BoOza223.js → provision-Ho5v-tt-.js} +2 -2
- package/dist/{provision-BoOza223.js.map → provision-Ho5v-tt-.js.map} +1 -1
- package/dist/{serve-DpS_lCbm.js → serve-Cb-sUTL8.js} +5 -5
- package/dist/{serve-DpS_lCbm.js.map → serve-Cb-sUTL8.js.map} +1 -1
- package/dist/{server-setup-3nCFVn79.js → server-setup-OTy-qHfv.js} +37 -17
- package/dist/server-setup-OTy-qHfv.js.map +1 -0
- package/dist/{start-CX92d6cy.js → start-Ch_iOkQE.js} +3 -3
- package/dist/{start-CX92d6cy.js.map → start-Ch_iOkQE.js.map} +1 -1
- package/dist/state-B8adscCv.js.map +1 -1
- package/package.json +1 -1
- package/dist/attribution-settings-Ce6s_01h.js.map +0 -1
- package/dist/claude-i4L8GZbI.js.map +0 -1
- package/dist/engine-DMnMGgeZ.js +0 -2
- package/dist/fast-profile-contract-BWCJy6iL.js.map +0 -1
- package/dist/max-profile-contract-v2Q6eN5w.js.map +0 -1
- package/dist/server-setup-3nCFVn79.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-discovery-DkJf7d-y.js","names":["fs","nodePath","fs","nodePath"],"sources":["../src/lib/orchestration/harness-parse.ts","../src/lib/orchestration/gate-discovery.ts"],"sourcesContent":["/**\n * Deterministic, language-agnostic harness PARSER for the structural Stop-gate.\n *\n * The gate runs a repo's own checks (typecheck/lint/test/build) when the agent\n * tries to finish. Historically it only auto-enabled for Bun/TS repos via a\n * hard-coded sealed gate. This module generalizes detection by PARSING the\n * project's own authoritative config — `package.json` scripts, CI `run:` steps,\n * Make/just/task targets, and language manifests — and emitting the canonical\n * check commands it finds.\n *\n * Design properties (a cross-lab panel chose a parser over a model for these):\n * - EVIDENCE-PINNED BY CONSTRUCTION: every emitted command is either lifted\n * verbatim from a parsed source file (a package.json script, a CI `run:`\n * line, a Make target invocation) or is a fixed canonical manifest command\n * for a detected ecosystem (`cargo check`, `go vet ./...`). The parser never\n * invents a command, so it has no hallucination / prompt-injection surface.\n * - PURE + DETERMINISTIC: a function of the repo's files + the live PATH. The\n * runtime Stop hook re-derives it at each stop (no cache), so it always\n * reflects the current tree and fails open for free when a tool vanished.\n * - SHELL-SAFE: `liveExec` runs `command.trim().split(/\\s+/)` (naive argv\n * split, no shell), so every emitted command is validated to be plain\n * space-separated tokens with no shell operators and no `%` (cmd.exe quoting\n * throws on `%`). See `isSafeCommand`.\n *\n * This is consumed ONLY by the local Stop hook. The sealed-gate kernel\n * (`run_workflow`) is never fed a parsed command — `GateDescriptor.kind` is\n * `\"parsed\"`, never a sealed id, and `sealedGateIds()` is unchanged.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { existsSync, promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { resolveExecutable } from \"~/lib/exec\"\n\nimport { resolveSealedGate } from \"./gate-registry\"\nimport { type CheckSpec } from \"./gate-runner\"\n\n/** Canonical check ids — stable across ecosystems so baseline isolation and the\n * selector key on the same names regardless of language. `build` is deliberately\n * NOT a check: build scripts are too variable in cost to auto-run on every stop\n * (a full bundle/SEA build is the slow-command / false-red footgun the design\n * avoids). Compile-checking is covered under `typecheck` (`go vet`, `cargo\n * check`, `tsc`). */\nexport type CheckId = \"typecheck\" | \"lint\" | \"test\"\n\n/** The fast static checks that are always-on; `test` is opt-in (it runs project\n * code and can be slow on every stop). */\nconst STATIC_IDS: ReadonlySet<CheckId> = new Set([\"typecheck\", \"lint\"])\n\n/**\n * The resolved gate source for a repo.\n * - `sealed` — the bun/TS fast-path (a sealed gate id, byte-identical to\n * the legacy behavior; the runtime hook resolves it via the\n * sealed registry).\n * - `parsed` — deterministic parser output (this module).\n * - `discovered` — the evidence-pinned model fallback (see `gate-discovery`).\n * `workdir` is the directory the checks run in (the repo/package root where the\n * evidence was found), NOT the Stop payload's cwd — so a monorepo stop from a\n * nested dir still runs the root's checks.\n */\nexport type GateDescriptor =\n | { kind: \"sealed\"; gateId: string; workdir: string }\n | { kind: \"parsed\"; checks: CheckSpec[]; ecosystem: string; workdir: string; evidence: string[] }\n | { kind: \"discovered\"; checks: CheckSpec[]; ecosystem: string; workdir: string; evidence: string[] }\n\ninterface Candidate {\n id: CheckId\n command: string\n /** The source file the command was lifted from (for evidence + messages). */\n source: string\n}\n\n/** Reject anything that is not a plain, shell-free, `%`-free argv line. This is\n * the load-bearing guard for `liveExec`'s naive whitespace split. */\nexport function isSafeCommand(command: string): boolean {\n const c = command.trim()\n if (c.length === 0 || c.length > 200) return false\n if (/[\\r\\n]/.test(c)) return false\n // Each token may contain only these chars: letters/digits and a small set of\n // path/flag punctuation. This rejects shell metacharacters (; & | < > ( ) $\n // backtick \" ' * ? ! ^ \\ %), env expansion, and quoting in one shot.\n const tokens = c.split(/\\s+/).filter(Boolean)\n if (tokens.length === 0) return false\n return tokens.every((t) => /^[A-Za-z0-9._/:@=+-]+$/.test(t))\n}\n\n/** First token of a command (the executable), for a PATH-presence probe. */\nfunction firstToken(command: string): string {\n return command.trim().split(/\\s+/)[0] ?? \"\"\n}\n\n/** True when a command would MUTATE the tree and so must never be a gate: a\n * `--fix`/`--write` flag or a `:fix`/`:write` script-name suffix (e.g.\n * `npm run lint:fix`, `eslint --fix`). Whitespace is normalized first so a\n * tab/double-space can't slip a flag past the scan. Exported + shared with the\n * discovered-command sanitizer. */\nexport function isMutatingCommand(command: string): boolean {\n const c = command.trim().replace(/\\s+/g, \" \")\n if (/(^| )(--fix|--write|-w|--watch|--serve|--interactive|-i)( |=|$)/i.test(c)) return true\n if (/(^| |:|-)(fix|write)\\b/i.test(c)) return true\n return false\n}\n\n/** True when the command is safe AND its executable resolves on PATH (a missing\n * tool → drop the check rather than guarantee a false-red). */\nfunction commandRunnable(command: string): boolean {\n if (!isSafeCommand(command)) return false\n return resolveExecutable(firstToken(command), { env: process.env }) !== null\n}\n\nasync function readJsonFile(file: string): Promise<unknown | undefined> {\n try {\n return JSON.parse(await fs.readFile(file, \"utf8\")) as unknown\n } catch {\n return undefined\n }\n}\n\nasync function readTextFile(file: string): Promise<string | undefined> {\n try {\n return await fs.readFile(file, \"utf8\")\n } catch {\n return undefined\n }\n}\n\n/** package.json `scripts` as a plain string→string map (best-effort). */\nasync function readScripts(root: string): Promise<Record<string, string>> {\n const pkg = await readJsonFile(nodePath.join(root, \"package.json\"))\n const scripts = pkg && typeof pkg === \"object\" ? (pkg as { scripts?: unknown }).scripts : undefined\n const out: Record<string, string> = {}\n if (scripts && typeof scripts === \"object\") {\n for (const [k, v] of Object.entries(scripts as Record<string, unknown>)) {\n if (typeof v === \"string\") out[k] = v\n }\n }\n return out\n}\n\n/** Pick the JS package runner from the lockfile present at the root. */\nfunction nodeRunner(root: string): \"bun\" | \"pnpm\" | \"yarn\" | \"npm\" {\n if (existsSync(nodePath.join(root, \"bun.lockb\")) || existsSync(nodePath.join(root, \"bun.lock\"))) return \"bun\"\n if (existsSync(nodePath.join(root, \"pnpm-lock.yaml\"))) return \"pnpm\"\n if (existsSync(nodePath.join(root, \"yarn.lock\"))) return \"yarn\"\n return \"npm\"\n}\n\n/** Map a package.json script NAME to a canonical id (read-only checks only — a\n * `:fix`/`:write` variant mutates the tree and is never a gate). */\nfunction scriptNameToId(name: string): CheckId | null {\n if (/:(fix|write)$/i.test(name)) return null\n if (/^(typecheck|type-check|check-types|tsc|types)$/i.test(name)) return \"typecheck\"\n if (/^(lint|lint:check|eslint)$/i.test(name)) return \"lint\"\n if (/^test$/i.test(name)) return \"test\"\n return null\n}\n\nasync function collectNodeChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"package.json\"))) return []\n const runner = nodeRunner(root)\n if (resolveExecutable(runner, { env: process.env }) === null) return []\n const scripts = await readScripts(root)\n const out: Candidate[] = []\n for (const [name] of Object.entries(scripts)) {\n const id = scriptNameToId(name)\n if (!id) continue\n if (id === \"test\" && !includeTests) continue\n const command = `${runner} run ${name}`\n if (isSafeCommand(command)) out.push({ id, command, source: \"package.json\" })\n }\n return out\n}\n\n/**\n * Best-effort CI gap-filler: scan GitHub Actions / GitLab CI YAML for single-line\n * `run:` static checks (typecheck/lint/build) the project actually runs. We do\n * NOT lift `test` from CI — CI test jobs frequently need services/secrets and\n * would false-red on every stop. Multi-line / scripted `run:` blocks are skipped\n * (they can't be a single argv). Any parse failure yields nothing.\n */\nasync function collectCiChecks(root: string): Promise<Candidate[]> {\n const files: string[] = []\n const wfDir = nodePath.join(root, \".github\", \"workflows\")\n try {\n for (const name of await fs.readdir(wfDir)) {\n if (/\\.ya?ml$/i.test(name)) files.push(nodePath.join(wfDir, name))\n }\n } catch {\n /* no workflows dir */\n }\n const gitlab = nodePath.join(root, \".gitlab-ci.yml\")\n if (existsSync(gitlab)) files.push(gitlab)\n if (files.length === 0) return []\n\n let parseYaml: (s: string) => unknown\n try {\n // Lazy import so the parser module has no hard dep when CI files are absent.\n ;({ parse: parseYaml } = await import(\"yaml\"))\n } catch {\n return []\n }\n\n const out: Candidate[] = []\n const seen = new Set<CheckId>()\n const classify = (cmd: string): CheckId | null => {\n if (/\\b(typecheck|type-check|tsc|mypy|pyright)\\b/i.test(cmd)) return \"typecheck\"\n if (/\\b(lint|eslint|ruff|clippy|golangci|vet)\\b/i.test(cmd)) return \"lint\"\n return null\n }\n const consider = (run: unknown, source: string): void => {\n if (typeof run !== \"string\") return\n const cmd = run.trim()\n if (cmd.includes(\"\\n\")) return // multi-line script block — not a single argv.\n if (!isSafeCommand(cmd) || isMutatingCommand(cmd) || !commandRunnable(cmd)) return\n const id = classify(cmd)\n if (!id || seen.has(id)) return\n seen.add(id)\n out.push({ id, command: cmd, source })\n }\n // Walk the parsed YAML for any `run:` string anywhere (GH `steps[].run`, GitLab\n // `<job>.script[]`). A generic deep walk tolerates both schemas + future shapes.\n const walk = (node: unknown, source: string): void => {\n if (!node || typeof node !== \"object\") return\n if (Array.isArray(node)) {\n for (const v of node) walk(v, source)\n return\n }\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n if (k === \"run\" && typeof v === \"string\") consider(v, source)\n else if (k === \"script\") {\n // GitLab `script:` is a string or string[].\n if (typeof v === \"string\") consider(v, source)\n else if (Array.isArray(v)) for (const s of v) consider(s, source)\n } else walk(v, source)\n }\n }\n for (const f of files) {\n const text = await readTextFile(f)\n if (text === undefined) continue\n try {\n walk(parseYaml(text), nodePath.relative(root, f) || nodePath.basename(f))\n } catch {\n /* unparseable workflow → skip */\n }\n }\n return out\n}\n\n/** Make / just / Taskfile targets matching canonical ids → `<tool> <target>`. */\nasync function collectTaskChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n const out: Candidate[] = []\n const wantId = (target: string): CheckId | null => scriptNameToId(target)\n // Makefile / justfile: a line `target:` (Make) or `target:` (just recipe head).\n for (const [file, tool] of [\n [\"Makefile\", \"make\"],\n [\"makefile\", \"make\"],\n [\"justfile\", \"just\"],\n [\".justfile\", \"just\"],\n ] as const) {\n const text = await readTextFile(nodePath.join(root, file))\n if (text === undefined) continue\n if (resolveExecutable(tool, { env: process.env }) === null) continue\n for (const line of text.split(/\\r?\\n/)) {\n const m = /^([A-Za-z0-9._-]+)\\s*:/.exec(line)\n if (!m) continue\n const id = wantId(m[1])\n if (!id || (id === \"test\" && !includeTests)) continue\n const command = `${tool} ${m[1]}`\n if (isSafeCommand(command)) out.push({ id, command, source: file })\n }\n }\n // Taskfile.yml: top-level `tasks:` keys.\n const taskfile = [\"Taskfile.yml\", \"Taskfile.yaml\"].map((n) => nodePath.join(root, n)).find((p) => existsSync(p))\n if (taskfile && resolveExecutable(\"task\", { env: process.env }) !== null) {\n const text = await readTextFile(taskfile)\n if (text !== undefined) {\n try {\n const { parse } = await import(\"yaml\")\n const doc = parse(text) as { tasks?: Record<string, unknown> } | undefined\n for (const name of Object.keys(doc?.tasks ?? {})) {\n const id = wantId(name)\n if (!id || (id === \"test\" && !includeTests)) continue\n const command = `task ${name}`\n if (isSafeCommand(command)) out.push({ id, command, source: nodePath.basename(taskfile) })\n }\n } catch {\n /* skip */\n }\n }\n }\n return out\n}\n\nasync function collectRustChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"Cargo.toml\"))) return []\n if (resolveExecutable(\"cargo\", { env: process.env }) === null) return []\n const out: Candidate[] = [{ id: \"typecheck\", command: \"cargo check\", source: \"Cargo.toml\" }]\n // clippy is a separate component; only emit it if `cargo-clippy` resolves.\n if (resolveExecutable(\"cargo-clippy\", { env: process.env }) !== null) {\n out.push({ id: \"lint\", command: \"cargo clippy\", source: \"Cargo.toml\" })\n }\n if (includeTests) out.push({ id: \"test\", command: \"cargo test\", source: \"Cargo.toml\" })\n return out\n}\n\nasync function collectGoChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"go.mod\"))) return []\n if (resolveExecutable(\"go\", { env: process.env }) === null) return []\n // `go vet` compiles + reports suspect constructs — the typecheck-equivalent.\n const out: Candidate[] = [{ id: \"typecheck\", command: \"go vet ./...\", source: \"go.mod\" }]\n if (includeTests) out.push({ id: \"test\", command: \"go test ./...\", source: \"go.mod\" })\n return out\n}\n\nasync function collectPythonChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n const configFiles = [\"pyproject.toml\", \"setup.cfg\", \"pytest.ini\", \"tox.ini\", \"ruff.toml\", \"mypy.ini\"]\n const present = configFiles.filter((f) => existsSync(nodePath.join(root, f)))\n if (present.length === 0) return []\n const evidence = present[0]\n const out: Candidate[] = []\n const configText = (await readTextFile(nodePath.join(root, present[0]))) ?? \"\"\n const mentions = (tool: string): boolean => present.some((f) => f.startsWith(tool)) || configText.includes(tool)\n // Only emit a Python tool when BOTH config evidence AND the tool resolve.\n if (mentions(\"mypy\") && resolveExecutable(\"mypy\", { env: process.env }) !== null) {\n out.push({ id: \"typecheck\", command: \"mypy .\", source: evidence })\n }\n if (mentions(\"ruff\") && resolveExecutable(\"ruff\", { env: process.env }) !== null) {\n out.push({ id: \"lint\", command: \"ruff check .\", source: evidence })\n }\n if (includeTests && (mentions(\"pytest\") || existsSync(nodePath.join(root, \"pytest.ini\")))) {\n if (resolveExecutable(\"pytest\", { env: process.env }) !== null) {\n out.push({ id: \"test\", command: \"pytest -q\", source: evidence })\n }\n }\n return out\n}\n\n/** Pick the first runnable candidate per id, in source-priority order. */\nfunction pickByPriority(candidates: Candidate[]): { checks: CheckSpec[]; evidence: string[] } {\n const byId = new Map<CheckId, Candidate>()\n for (const c of candidates) {\n if (!byId.has(c.id) && commandRunnable(c.command)) byId.set(c.id, c)\n }\n const order: CheckId[] = [\"typecheck\", \"lint\", \"test\"]\n const checks: CheckSpec[] = []\n const evidence = new Set<string>()\n for (const id of order) {\n const c = byId.get(id)\n if (c) {\n checks.push({ id: c.id, command: c.command })\n evidence.add(c.source)\n }\n }\n return { checks, evidence: [...evidence] }\n}\n\n/**\n * Resolve the gate descriptor for an already-resolved repo `root`.\n * 1. bun/TS sealed fast-path — byte-identical to the legacy `detectHarnessGateId`\n * (bun on PATH + a `typecheck` script → sealed `default-ci`/`typecheck-test`).\n * 2. else the deterministic parser: collect candidates from package.json\n * scripts (primary, self-contained), Make/just/task, language manifests, and\n * a CI gap-filler for static checks; pick one per id by priority. A `parsed`\n * descriptor is returned only when at least one STATIC check survives (a\n * test-only set would either be off-by-default or risk false-reds).\n * 3. else null (the launcher prints why and the gate stays off).\n */\nexport async function parseGateDescriptor(\n root: string,\n opts: { includeTests: boolean },\n): Promise<GateDescriptor | null> {\n // (1) bun/TS sealed parity.\n if (resolveExecutable(\"bun\", { env: process.env }) !== null) {\n const scripts = await readScripts(root)\n if (typeof scripts.typecheck === \"string\") {\n const gateId = typeof scripts.lint === \"string\" ? \"default-ci\" : \"typecheck-test\"\n return { kind: \"sealed\", gateId, workdir: root }\n }\n }\n\n // (2) deterministic parser. package.json scripts first (self-contained), then\n // task runners + manifests, then a CI gap-filler for static checks.\n const groups = await Promise.all([\n collectNodeChecks(root, opts.includeTests),\n collectTaskChecks(root, opts.includeTests),\n collectRustChecks(root, opts.includeTests),\n collectGoChecks(root, opts.includeTests),\n collectPythonChecks(root, opts.includeTests),\n collectCiChecks(root),\n ])\n const candidates = groups.flat()\n if (candidates.length === 0) return null\n\n const ecosystem =\n candidates.find((c) => c.source === \"package.json\") ? \"node\"\n : candidates.find((c) => c.source === \"Cargo.toml\") ? \"rust\"\n : candidates.find((c) => c.source === \"go.mod\") ? \"go\"\n : candidates.find((c) => /^(pyproject|setup|pytest|tox|ruff|mypy)/.test(c.source)) ? \"python\"\n : candidates.find((c) => /^(Makefile|makefile|justfile|\\.justfile|Taskfile)/.test(c.source)) ? \"make\"\n : \"ci\"\n\n const { checks, evidence } = pickByPriority(candidates)\n if (checks.length === 0) return null\n // Require at least one STATIC check (typecheck/lint/build) unless the caller\n // opted into running the full test suite — a test-only set is otherwise either\n // off-by-default or a slow/false-red risk on every stop.\n const hasStatic = checks.some((c) => STATIC_IDS.has(c.id as CheckId))\n if (!hasStatic && !opts.includeTests) return null\n return { kind: \"parsed\", checks, ecosystem, workdir: root, evidence }\n}\n\n/** The checks a descriptor runs: a sealed descriptor resolves its sealed command\n * set from the registry; parsed/discovered carry their own. Fresh array. */\nexport function checksForDescriptor(d: GateDescriptor): CheckSpec[] {\n if (d.kind === \"sealed\") {\n const sealed = resolveSealedGate(d.gateId)\n return sealed ? sealed.checks.map((c) => ({ id: c.id, command: c.command })) : []\n }\n return d.checks.map((c) => ({ id: c.id, command: c.command }))\n}\n\n/** A stable key over a descriptor's effective check set, for baseline isolation.\n * Sealed descriptors key on their gate id (preserving legacy baseline keys);\n * parsed/discovered key on the canonicalized (id,command) set, so a changed\n * command set yields a fresh baseline instead of masking/inventing regressions. */\nexport function descriptorHash(d: GateDescriptor): string {\n if (d.kind === \"sealed\") return `sealed:${d.gateId}`\n const canon = [...d.checks]\n .map((c) => `${c.id}\u0000${c.command.trim().replace(/\\s+/g, \" \")}`)\n .sort()\n .join(\"\u0001\")\n return `${d.kind}:${createHash(\"sha256\").update(canon).digest(\"hex\").slice(0, 32)}`\n}\n","/**\n * Evidence-pinned model FALLBACK for the structural Stop-gate — the last resort\n * when the deterministic parser (`harness-parse`) finds no runnable checks.\n *\n * A read-only worker reads the repo's own config/docs and proposes the canonical\n * check commands. Two guards keep this safe despite being model-authored:\n * 1. SANITIZE — `sanitizeDiscoveredCheck` rejects shell metacharacters (so a\n * command can never chain/redirect/expand), destructive/stateful verbs,\n * interactive/watch shapes, and an executable that isn't on PATH. What\n * survives is a single plain argv line, safe for `liveExec`'s naive split.\n * 2. EVIDENCE-PIN — a surviving command must appear (whitespace-normalized)\n * VERBATIM in one of the collected source files. The model cannot invent a\n * command or be prompt-injected into emitting one that isn't already a real\n * command in the repo (and a real command in a user-trusted repo is the\n * same authority the existing gate already runs).\n *\n * Discovery runs ONCE at launch and the result is cached per (repoFingerprint,\n * sourcesHash) in a human-readable record. The runtime Stop hook only READS the\n * cached record (no model call at stop). This is consumed ONLY by the local Stop\n * hook; the sealed-gate kernel never sees a discovered command.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { existsSync, promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { resolveExecutable } from \"~/lib/exec\"\nimport { PATHS } from \"~/lib/paths\"\n\nimport { type CheckSpec } from \"./gate-runner\"\nimport { isMutatingCommand, isSafeCommand, type CheckId } from \"./harness-parse\"\nimport { repoFingerprint, repoRoot } from \"./stop-gate-policy\"\n\n/** The allowlist of files the discovery worker is steered to read — config +\n * docs that legitimately describe how to check a project. Secret files are\n * additionally blocked at the worker IO layer (`.env*`/`*.pem`/`id_*`/…). */\nconst SIGNAL_FILES: ReadonlyArray<string> = [\n \"package.json\",\n \"Makefile\",\n \"makefile\",\n \"justfile\",\n \".justfile\",\n \"Taskfile.yml\",\n \"Taskfile.yaml\",\n \"Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"setup.cfg\",\n \"tox.ini\",\n \"pytest.ini\",\n \"CONTRIBUTING.md\",\n \"CONTRIBUTING\",\n \"README.md\",\n \"README\",\n \"DEVELOPING.md\",\n \"mix.exs\",\n \"build.gradle\",\n \"pom.xml\",\n \"composer.json\",\n]\nconst SIGNAL_DIRS: ReadonlyArray<string> = [\".github/workflows\"]\n\n/** A discovered check command after sanitization + evidence-pinning. */\nexport interface DiscoveredRecord {\n root: string\n fingerprint: string\n sourcesHash: string\n discoveredAt: string\n model: string\n ecosystem: string\n checks: CheckSpec[]\n confidence: string\n evidence: string[]\n}\n\nconst MAX_SIGNAL_BYTES = 64 * 1024\n/** Global caps so a repo with many workflow files can't inflate discovery /\n * hashing cost: at most this many files and this much total text. */\nconst MAX_SIGNAL_FILES = 40\nconst MAX_TOTAL_SIGNAL_BYTES = 512 * 1024\n\n/** Words that mark a command as destructive, stateful, or non-terminating — a\n * \"check\" must never do any of these. (Shell operators are already rejected by\n * `isSafeCommand`, so chaining can't smuggle them past this word scan.) */\nconst DENY_WORD =\n /\\b(rm|rmdir|mv|dd|mkfs|sudo|chmod|chown|publish|push|deploy|migrate|kubectl|terraform|docker|curl|wget|ssh|scp|rsync|nc|eval|npm i|npm install|yarn add|pip install|apt|brew|watch|serve|repl|dev|start)\\b/i\n\n/**\n * True when `command` is safe to auto-run as a check: a plain argv line (no\n * shell metacharacters), no destructive/stateful verb, no mutating/interactive\n * shape, and its executable resolves on PATH. The deny-word scan runs on a\n * whitespace-NORMALIZED copy so a tab / double-space can't split `npm install`\n * past the literal-space patterns.\n */\nexport function sanitizeDiscoveredCheck(command: string): boolean {\n if (!isSafeCommand(command)) return false\n const norm = normalizeWs(command)\n if (DENY_WORD.test(norm)) return false\n if (isMutatingCommand(norm)) return false // --fix/--write/--watch + :fix/:write scripts\n const first = norm.split(\" \")[0] ?? \"\"\n return resolveExecutable(first, { env: process.env }) !== null\n}\n\nfunction normalizeWs(s: string): string {\n return s.replace(/\\s+/g, \" \").trim()\n}\n\n/** Collect the text of the allowlisted signal files (capped), for evidence-pin +\n * the sources hash. Returns the concatenated text and the relative file list. */\nasync function collectSignals(root: string): Promise<{ text: string; files: string[] }> {\n const parts: string[] = []\n const files: string[] = []\n let total = 0\n const readCapped = async (abs: string, rel: string): Promise<void> => {\n if (files.length >= MAX_SIGNAL_FILES || total >= MAX_TOTAL_SIGNAL_BYTES) return\n try {\n const raw = await fs.readFile(abs, \"utf8\")\n const slice = raw.length > MAX_SIGNAL_BYTES ? raw.slice(0, MAX_SIGNAL_BYTES) : raw\n parts.push(slice)\n files.push(rel)\n total += slice.length\n } catch {\n /* unreadable → skip */\n }\n }\n for (const f of SIGNAL_FILES) {\n const abs = nodePath.join(root, f)\n if (existsSync(abs)) await readCapped(abs, f)\n }\n for (const d of SIGNAL_DIRS) {\n const dir = nodePath.join(root, d)\n try {\n for (const name of await fs.readdir(dir)) {\n if (/\\.(ya?ml)$/i.test(name)) await readCapped(nodePath.join(dir, name), nodePath.join(d, name))\n }\n } catch {\n /* no dir */\n }\n }\n return { text: parts.join(\"\\n\"), files }\n}\n\n/** A freshness hash over the signal files' contents + relative paths. A change\n * to how the project checks itself flips this → re-discovery on next launch. */\nexport async function sourcesHash(root: string): Promise<string> {\n const { text, files } = await collectSignals(root)\n return createHash(\"sha256\")\n .update(files.sort().join(\"\\n\"))\n .update(\"\\0\")\n .update(text)\n .digest(\"hex\")\n}\n\nfunction discoveredDir(): string {\n return nodePath.join(PATHS.APP_DIR, \"stop-gate\", \"discovered\")\n}\nfunction recordPathFor(root: string): string {\n return nodePath.join(discoveredDir(), createHash(\"sha256\").update(nodePath.resolve(root)).digest(\"hex\").slice(0, 32))\n}\n\n/** Read the cached discovered record for `root`, verifying it still matches the\n * live repo identity AND the live sources hash. Any mismatch / unreadable /\n * empty-checks record → null (re-discover or stay off; never run a stale set). */\nexport async function readDiscoveredGate(root: string): Promise<DiscoveredRecord | null> {\n let rec: DiscoveredRecord\n try {\n rec = JSON.parse(await fs.readFile(recordPathFor(root), \"utf8\")) as DiscoveredRecord\n } catch {\n return null\n }\n if (!rec || !Array.isArray(rec.checks) || rec.checks.length === 0) return null\n const fp = await repoFingerprint(root).catch(() => \"\")\n if (fp.length === 0 || fp !== rec.fingerprint) return null // identity drift → deny.\n const sh = await sourcesHash(root).catch(() => \"\")\n if (sh.length === 0 || sh !== rec.sourcesHash) return null // config changed → stale.\n // Re-validate id AND re-sanitize the command at read time (defense-in-depth\n // against a tampered/corrupt record).\n const checks = rec.checks.filter(\n (c) => c && typeof c.id === \"string\" && VALID_IDS.has(c.id) && typeof c.command === \"string\" && sanitizeDiscoveredCheck(c.command),\n )\n if (checks.length === 0) return null\n return { ...rec, checks }\n}\n\nexport async function writeDiscoveredGate(rec: DiscoveredRecord): Promise<void> {\n await fs.mkdir(discoveredDir(), { recursive: true })\n const tmp = `${recordPathFor(rec.root)}.${process.pid}.tmp`\n await fs.writeFile(tmp, `${JSON.stringify(rec, null, 2)}\\n`, { mode: 0o600 })\n await fs.rename(tmp, recordPathFor(rec.root))\n}\n\nconst DISCOVERY_PROMPT = (files: string[]): string =>\n `You are configuring an automated pre-finish CHECK gate for this repository. Read ONLY these `\n + `already-present config/doc files to learn how the project checks itself: ${files.join(\", \")}. `\n + `Identify the canonical FAST static check command (typecheck and/or lint) and, if obvious, the test `\n + `command. Rules: (1) return a command ONLY if it appears VERBATIM in one of those files — never invent `\n + `one; (2) commands must be non-interactive, self-terminating, read-only verification (NO install / `\n + `publish / push / deploy / migrate / format-in-place / watch / serve / dev-server / delete); (3) at most `\n + `3 commands; (4) if unsure, return an empty list. Respond with ONLY a fenced \\`\\`\\`json block of the shape `\n + `{\"ecosystem\":\"<label>\",\"checks\":[{\"id\":\"typecheck|lint|test\",\"command\":\"<single-line command>\"}],`\n + `\"confidence\":\"high|low\"} and NOTHING else.`\n\ninterface DiscoverResult {\n ecosystem: string\n checks: CheckSpec[]\n confidence: string\n evidence: string[]\n}\n\n/** Extract the first fenced/bare JSON object from the worker's text. */\nfunction extractJson(text: string): unknown {\n const fenced = /```(?:json)?\\s*([\\s\\S]*?)```/i.exec(text)\n const body = fenced ? fenced[1] : text\n const start = body.indexOf(\"{\")\n const end = body.lastIndexOf(\"}\")\n if (start < 0 || end <= start) return undefined\n try {\n return JSON.parse(body.slice(start, end + 1)) as unknown\n } catch {\n return undefined\n }\n}\n\nconst VALID_IDS: ReadonlySet<string> = new Set<CheckId>([\"typecheck\", \"lint\", \"test\"])\n\n/**\n * Sanitize + EVIDENCE-PIN the model's raw checks against the collected source\n * text. Pure (no IO) so it is unit-testable without the worker. A check survives\n * only if: its id is canonical, it isn't a `test` while tests are off, its id\n * hasn't already been taken, it passes `sanitizeDiscoveredCheck`, AND its command\n * appears (whitespace-normalized) VERBATIM in `evidenceText`. The evidence-pin is\n * the load-bearing guard: the model cannot invent a command or be prompt-injected\n * into one that isn't already a real command in the (user-trusted) repo.\n */\nexport function filterDiscoveredChecks(\n rawChecks: unknown,\n evidenceText: string,\n includeTests: boolean,\n): CheckSpec[] {\n const evidenceNorm = normalizeWs(evidenceText)\n const seen = new Set<string>()\n const checks: CheckSpec[] = []\n for (const c of Array.isArray(rawChecks) ? rawChecks : []) {\n if (!c || typeof c !== \"object\") continue\n const id = (c as { id?: unknown }).id\n const command = (c as { command?: unknown }).command\n if (typeof id !== \"string\" || !VALID_IDS.has(id)) continue\n if (typeof command !== \"string\") continue\n if (id === \"test\" && !includeTests) continue\n if (seen.has(id)) continue\n if (!sanitizeDiscoveredCheck(command)) continue\n if (!evidenceNorm.includes(normalizeWs(command))) continue\n seen.add(id)\n checks.push({ id, command: command.trim() })\n }\n return checks\n}\n\n/**\n * Run the read-only worker to discover check commands for `cwd`. Returns the\n * sanitized + evidence-pinned result, or null (worker unavailable / errored /\n * nothing survived). NEVER throws.\n */\nexport async function discoverGateCommands(\n cwd: string,\n opts: { signal?: AbortSignal; includeTests: boolean },\n): Promise<DiscoverResult | null> {\n const root = await repoRoot(cwd).catch(() => cwd)\n const { text: evidenceText, files } = await collectSignals(root)\n if (files.length === 0) return null\n\n let result: { text: string; isError?: boolean }\n try {\n // Lazy-import the heavy worker-agent engine ONLY when discovery actually\n // runs — keeping it out of the module-load graph of `claude.ts` (which\n // imports this module), so the mock.module-isolated CLI tests don't deadlock\n // pulling the Pi runtime at load time.\n const { runWorkerAgent } = await import(\"~/lib/worker-agent/engine\")\n // Discovery is advisory setup, not a verified workflow producer/checker,\n // so honoring the operator's explore default here is intentional.\n result = await runWorkerAgent({\n mode: \"explore\",\n workspace: root,\n prompt: DISCOVERY_PROMPT(files),\n signal: opts.signal,\n })\n } catch {\n return null\n }\n if (result.isError) return null\n const parsed = extractJson(result.text)\n if (!parsed || typeof parsed !== \"object\") return null\n const obj = parsed as { ecosystem?: unknown; checks?: unknown; confidence?: unknown }\n const checks = filterDiscoveredChecks(obj.checks, evidenceText, opts.includeTests)\n if (checks.length === 0) return null\n const ecosystem = typeof obj.ecosystem === \"string\" && obj.ecosystem.length > 0 ? obj.ecosystem : \"discovered\"\n const confidence = typeof obj.confidence === \"string\" ? obj.confidence : \"low\"\n return { ecosystem, checks, confidence, evidence: files }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gate-discovery-DDkRvyYL.js","names":["fs","nodePath","fs","nodePath"],"sources":["../src/lib/orchestration/harness-parse.ts","../src/lib/orchestration/gate-discovery.ts"],"sourcesContent":["/**\n * Deterministic, language-agnostic harness PARSER for the structural Stop-gate.\n *\n * The gate runs a repo's own checks (typecheck/lint/test/build) when the agent\n * tries to finish. Historically it only auto-enabled for Bun/TS repos via a\n * hard-coded sealed gate. This module generalizes detection by PARSING the\n * project's own authoritative config — `package.json` scripts, CI `run:` steps,\n * Make/just/task targets, and language manifests — and emitting the canonical\n * check commands it finds.\n *\n * Design properties (a cross-lab panel chose a parser over a model for these):\n * - EVIDENCE-PINNED BY CONSTRUCTION: every emitted command is either lifted\n * verbatim from a parsed source file (a package.json script, a CI `run:`\n * line, a Make target invocation) or is a fixed canonical manifest command\n * for a detected ecosystem (`cargo check`, `go vet ./...`). The parser never\n * invents a command, so it has no hallucination / prompt-injection surface.\n * - PURE + DETERMINISTIC: a function of the repo's files + the live PATH. The\n * runtime Stop hook re-derives it at each stop (no cache), so it always\n * reflects the current tree and fails open for free when a tool vanished.\n * - SHELL-SAFE: `liveExec` runs `command.trim().split(/\\s+/)` (naive argv\n * split, no shell), so every emitted command is validated to be plain\n * space-separated tokens with no shell operators and no `%` (cmd.exe quoting\n * throws on `%`). See `isSafeCommand`.\n *\n * This is consumed ONLY by the local Stop hook. The sealed-gate kernel\n * (`run_workflow`) is never fed a parsed command — `GateDescriptor.kind` is\n * `\"parsed\"`, never a sealed id, and `sealedGateIds()` is unchanged.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { existsSync, promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { resolveExecutable } from \"~/lib/exec\"\n\nimport { resolveSealedGate } from \"./gate-registry\"\nimport { type CheckSpec } from \"./gate-runner\"\n\n/** Canonical check ids — stable across ecosystems so baseline isolation and the\n * selector key on the same names regardless of language. `build` is deliberately\n * NOT a check: build scripts are too variable in cost to auto-run on every stop\n * (a full bundle/SEA build is the slow-command / false-red footgun the design\n * avoids). Compile-checking is covered under `typecheck` (`go vet`, `cargo\n * check`, `tsc`). */\nexport type CheckId = \"typecheck\" | \"lint\" | \"test\"\n\n/** The fast static checks that are always-on; `test` is opt-in (it runs project\n * code and can be slow on every stop). */\nconst STATIC_IDS: ReadonlySet<CheckId> = new Set([\"typecheck\", \"lint\"])\n\n/**\n * The resolved gate source for a repo.\n * - `sealed` — the bun/TS fast-path (a sealed gate id, byte-identical to\n * the legacy behavior; the runtime hook resolves it via the\n * sealed registry).\n * - `parsed` — deterministic parser output (this module).\n * - `discovered` — the evidence-pinned model fallback (see `gate-discovery`).\n * `workdir` is the directory the checks run in (the repo/package root where the\n * evidence was found), NOT the Stop payload's cwd — so a monorepo stop from a\n * nested dir still runs the root's checks.\n */\nexport type GateDescriptor =\n | { kind: \"sealed\"; gateId: string; workdir: string }\n | { kind: \"parsed\"; checks: CheckSpec[]; ecosystem: string; workdir: string; evidence: string[] }\n | { kind: \"discovered\"; checks: CheckSpec[]; ecosystem: string; workdir: string; evidence: string[] }\n\ninterface Candidate {\n id: CheckId\n command: string\n /** The source file the command was lifted from (for evidence + messages). */\n source: string\n}\n\n/** Reject anything that is not a plain, shell-free, `%`-free argv line. This is\n * the load-bearing guard for `liveExec`'s naive whitespace split. */\nexport function isSafeCommand(command: string): boolean {\n const c = command.trim()\n if (c.length === 0 || c.length > 200) return false\n if (/[\\r\\n]/.test(c)) return false\n // Each token may contain only these chars: letters/digits and a small set of\n // path/flag punctuation. This rejects shell metacharacters (; & | < > ( ) $\n // backtick \" ' * ? ! ^ \\ %), env expansion, and quoting in one shot.\n const tokens = c.split(/\\s+/).filter(Boolean)\n if (tokens.length === 0) return false\n return tokens.every((t) => /^[A-Za-z0-9._/:@=+-]+$/.test(t))\n}\n\n/** First token of a command (the executable), for a PATH-presence probe. */\nfunction firstToken(command: string): string {\n return command.trim().split(/\\s+/)[0] ?? \"\"\n}\n\n/** True when a command would MUTATE the tree and so must never be a gate: a\n * `--fix`/`--write` flag or a `:fix`/`:write` script-name suffix (e.g.\n * `npm run lint:fix`, `eslint --fix`). Whitespace is normalized first so a\n * tab/double-space can't slip a flag past the scan. Exported + shared with the\n * discovered-command sanitizer. */\nexport function isMutatingCommand(command: string): boolean {\n const c = command.trim().replace(/\\s+/g, \" \")\n if (/(^| )(--fix|--write|-w|--watch|--serve|--interactive|-i)( |=|$)/i.test(c)) return true\n if (/(^| |:|-)(fix|write)\\b/i.test(c)) return true\n return false\n}\n\n/** True when the command is safe AND its executable resolves on PATH (a missing\n * tool → drop the check rather than guarantee a false-red). */\nfunction commandRunnable(command: string): boolean {\n if (!isSafeCommand(command)) return false\n return resolveExecutable(firstToken(command), { env: process.env }) !== null\n}\n\nasync function readJsonFile(file: string): Promise<unknown | undefined> {\n try {\n return JSON.parse(await fs.readFile(file, \"utf8\")) as unknown\n } catch {\n return undefined\n }\n}\n\nasync function readTextFile(file: string): Promise<string | undefined> {\n try {\n return await fs.readFile(file, \"utf8\")\n } catch {\n return undefined\n }\n}\n\n/** package.json `scripts` as a plain string→string map (best-effort). */\nasync function readScripts(root: string): Promise<Record<string, string>> {\n const pkg = await readJsonFile(nodePath.join(root, \"package.json\"))\n const scripts = pkg && typeof pkg === \"object\" ? (pkg as { scripts?: unknown }).scripts : undefined\n const out: Record<string, string> = {}\n if (scripts && typeof scripts === \"object\") {\n for (const [k, v] of Object.entries(scripts as Record<string, unknown>)) {\n if (typeof v === \"string\") out[k] = v\n }\n }\n return out\n}\n\n/** Pick the JS package runner from the lockfile present at the root. */\nfunction nodeRunner(root: string): \"bun\" | \"pnpm\" | \"yarn\" | \"npm\" {\n if (existsSync(nodePath.join(root, \"bun.lockb\")) || existsSync(nodePath.join(root, \"bun.lock\"))) return \"bun\"\n if (existsSync(nodePath.join(root, \"pnpm-lock.yaml\"))) return \"pnpm\"\n if (existsSync(nodePath.join(root, \"yarn.lock\"))) return \"yarn\"\n return \"npm\"\n}\n\n/** Map a package.json script NAME to a canonical id (read-only checks only — a\n * `:fix`/`:write` variant mutates the tree and is never a gate). */\nfunction scriptNameToId(name: string): CheckId | null {\n if (/:(fix|write)$/i.test(name)) return null\n if (/^(typecheck|type-check|check-types|tsc|types)$/i.test(name)) return \"typecheck\"\n if (/^(lint|lint:check|eslint)$/i.test(name)) return \"lint\"\n if (/^test$/i.test(name)) return \"test\"\n return null\n}\n\nasync function collectNodeChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"package.json\"))) return []\n const runner = nodeRunner(root)\n if (resolveExecutable(runner, { env: process.env }) === null) return []\n const scripts = await readScripts(root)\n const out: Candidate[] = []\n for (const [name] of Object.entries(scripts)) {\n const id = scriptNameToId(name)\n if (!id) continue\n if (id === \"test\" && !includeTests) continue\n const command = `${runner} run ${name}`\n if (isSafeCommand(command)) out.push({ id, command, source: \"package.json\" })\n }\n return out\n}\n\n/**\n * Best-effort CI gap-filler: scan GitHub Actions / GitLab CI YAML for single-line\n * `run:` static checks (typecheck/lint/build) the project actually runs. We do\n * NOT lift `test` from CI — CI test jobs frequently need services/secrets and\n * would false-red on every stop. Multi-line / scripted `run:` blocks are skipped\n * (they can't be a single argv). Any parse failure yields nothing.\n */\nasync function collectCiChecks(root: string): Promise<Candidate[]> {\n const files: string[] = []\n const wfDir = nodePath.join(root, \".github\", \"workflows\")\n try {\n for (const name of await fs.readdir(wfDir)) {\n if (/\\.ya?ml$/i.test(name)) files.push(nodePath.join(wfDir, name))\n }\n } catch {\n /* no workflows dir */\n }\n const gitlab = nodePath.join(root, \".gitlab-ci.yml\")\n if (existsSync(gitlab)) files.push(gitlab)\n if (files.length === 0) return []\n\n let parseYaml: (s: string) => unknown\n try {\n // Lazy import so the parser module has no hard dep when CI files are absent.\n ;({ parse: parseYaml } = await import(\"yaml\"))\n } catch {\n return []\n }\n\n const out: Candidate[] = []\n const seen = new Set<CheckId>()\n const classify = (cmd: string): CheckId | null => {\n if (/\\b(typecheck|type-check|tsc|mypy|pyright)\\b/i.test(cmd)) return \"typecheck\"\n if (/\\b(lint|eslint|ruff|clippy|golangci|vet)\\b/i.test(cmd)) return \"lint\"\n return null\n }\n const consider = (run: unknown, source: string): void => {\n if (typeof run !== \"string\") return\n const cmd = run.trim()\n if (cmd.includes(\"\\n\")) return // multi-line script block — not a single argv.\n if (!isSafeCommand(cmd) || isMutatingCommand(cmd) || !commandRunnable(cmd)) return\n const id = classify(cmd)\n if (!id || seen.has(id)) return\n seen.add(id)\n out.push({ id, command: cmd, source })\n }\n // Walk the parsed YAML for any `run:` string anywhere (GH `steps[].run`, GitLab\n // `<job>.script[]`). A generic deep walk tolerates both schemas + future shapes.\n const walk = (node: unknown, source: string): void => {\n if (!node || typeof node !== \"object\") return\n if (Array.isArray(node)) {\n for (const v of node) walk(v, source)\n return\n }\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n if (k === \"run\" && typeof v === \"string\") consider(v, source)\n else if (k === \"script\") {\n // GitLab `script:` is a string or string[].\n if (typeof v === \"string\") consider(v, source)\n else if (Array.isArray(v)) for (const s of v) consider(s, source)\n } else walk(v, source)\n }\n }\n for (const f of files) {\n const text = await readTextFile(f)\n if (text === undefined) continue\n try {\n walk(parseYaml(text), nodePath.relative(root, f) || nodePath.basename(f))\n } catch {\n /* unparseable workflow → skip */\n }\n }\n return out\n}\n\n/** Make / just / Taskfile targets matching canonical ids → `<tool> <target>`. */\nasync function collectTaskChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n const out: Candidate[] = []\n const wantId = (target: string): CheckId | null => scriptNameToId(target)\n // Makefile / justfile: a line `target:` (Make) or `target:` (just recipe head).\n for (const [file, tool] of [\n [\"Makefile\", \"make\"],\n [\"makefile\", \"make\"],\n [\"justfile\", \"just\"],\n [\".justfile\", \"just\"],\n ] as const) {\n const text = await readTextFile(nodePath.join(root, file))\n if (text === undefined) continue\n if (resolveExecutable(tool, { env: process.env }) === null) continue\n for (const line of text.split(/\\r?\\n/)) {\n const m = /^([A-Za-z0-9._-]+)\\s*:/.exec(line)\n if (!m) continue\n const id = wantId(m[1])\n if (!id || (id === \"test\" && !includeTests)) continue\n const command = `${tool} ${m[1]}`\n if (isSafeCommand(command)) out.push({ id, command, source: file })\n }\n }\n // Taskfile.yml: top-level `tasks:` keys.\n const taskfile = [\"Taskfile.yml\", \"Taskfile.yaml\"].map((n) => nodePath.join(root, n)).find((p) => existsSync(p))\n if (taskfile && resolveExecutable(\"task\", { env: process.env }) !== null) {\n const text = await readTextFile(taskfile)\n if (text !== undefined) {\n try {\n const { parse } = await import(\"yaml\")\n const doc = parse(text) as { tasks?: Record<string, unknown> } | undefined\n for (const name of Object.keys(doc?.tasks ?? {})) {\n const id = wantId(name)\n if (!id || (id === \"test\" && !includeTests)) continue\n const command = `task ${name}`\n if (isSafeCommand(command)) out.push({ id, command, source: nodePath.basename(taskfile) })\n }\n } catch {\n /* skip */\n }\n }\n }\n return out\n}\n\nasync function collectRustChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"Cargo.toml\"))) return []\n if (resolveExecutable(\"cargo\", { env: process.env }) === null) return []\n const out: Candidate[] = [{ id: \"typecheck\", command: \"cargo check\", source: \"Cargo.toml\" }]\n // clippy is a separate component; only emit it if `cargo-clippy` resolves.\n if (resolveExecutable(\"cargo-clippy\", { env: process.env }) !== null) {\n out.push({ id: \"lint\", command: \"cargo clippy\", source: \"Cargo.toml\" })\n }\n if (includeTests) out.push({ id: \"test\", command: \"cargo test\", source: \"Cargo.toml\" })\n return out\n}\n\nasync function collectGoChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n if (!existsSync(nodePath.join(root, \"go.mod\"))) return []\n if (resolveExecutable(\"go\", { env: process.env }) === null) return []\n // `go vet` compiles + reports suspect constructs — the typecheck-equivalent.\n const out: Candidate[] = [{ id: \"typecheck\", command: \"go vet ./...\", source: \"go.mod\" }]\n if (includeTests) out.push({ id: \"test\", command: \"go test ./...\", source: \"go.mod\" })\n return out\n}\n\nasync function collectPythonChecks(root: string, includeTests: boolean): Promise<Candidate[]> {\n const configFiles = [\"pyproject.toml\", \"setup.cfg\", \"pytest.ini\", \"tox.ini\", \"ruff.toml\", \"mypy.ini\"]\n const present = configFiles.filter((f) => existsSync(nodePath.join(root, f)))\n if (present.length === 0) return []\n const evidence = present[0]\n const out: Candidate[] = []\n const configText = (await readTextFile(nodePath.join(root, present[0]))) ?? \"\"\n const mentions = (tool: string): boolean => present.some((f) => f.startsWith(tool)) || configText.includes(tool)\n // Only emit a Python tool when BOTH config evidence AND the tool resolve.\n if (mentions(\"mypy\") && resolveExecutable(\"mypy\", { env: process.env }) !== null) {\n out.push({ id: \"typecheck\", command: \"mypy .\", source: evidence })\n }\n if (mentions(\"ruff\") && resolveExecutable(\"ruff\", { env: process.env }) !== null) {\n out.push({ id: \"lint\", command: \"ruff check .\", source: evidence })\n }\n if (includeTests && (mentions(\"pytest\") || existsSync(nodePath.join(root, \"pytest.ini\")))) {\n if (resolveExecutable(\"pytest\", { env: process.env }) !== null) {\n out.push({ id: \"test\", command: \"pytest -q\", source: evidence })\n }\n }\n return out\n}\n\n/** Pick the first runnable candidate per id, in source-priority order. */\nfunction pickByPriority(candidates: Candidate[]): { checks: CheckSpec[]; evidence: string[] } {\n const byId = new Map<CheckId, Candidate>()\n for (const c of candidates) {\n if (!byId.has(c.id) && commandRunnable(c.command)) byId.set(c.id, c)\n }\n const order: CheckId[] = [\"typecheck\", \"lint\", \"test\"]\n const checks: CheckSpec[] = []\n const evidence = new Set<string>()\n for (const id of order) {\n const c = byId.get(id)\n if (c) {\n checks.push({ id: c.id, command: c.command })\n evidence.add(c.source)\n }\n }\n return { checks, evidence: [...evidence] }\n}\n\n/**\n * Resolve the gate descriptor for an already-resolved repo `root`.\n * 1. bun/TS sealed fast-path — byte-identical to the legacy `detectHarnessGateId`\n * (bun on PATH + a `typecheck` script → sealed `default-ci`/`typecheck-test`).\n * 2. else the deterministic parser: collect candidates from package.json\n * scripts (primary, self-contained), Make/just/task, language manifests, and\n * a CI gap-filler for static checks; pick one per id by priority. A `parsed`\n * descriptor is returned only when at least one STATIC check survives (a\n * test-only set would either be off-by-default or risk false-reds).\n * 3. else null (the launcher prints why and the gate stays off).\n */\nexport async function parseGateDescriptor(\n root: string,\n opts: { includeTests: boolean },\n): Promise<GateDescriptor | null> {\n // (1) bun/TS sealed parity.\n if (resolveExecutable(\"bun\", { env: process.env }) !== null) {\n const scripts = await readScripts(root)\n if (typeof scripts.typecheck === \"string\") {\n const gateId = typeof scripts.lint === \"string\" ? \"default-ci\" : \"typecheck-test\"\n return { kind: \"sealed\", gateId, workdir: root }\n }\n }\n\n // (2) deterministic parser. package.json scripts first (self-contained), then\n // task runners + manifests, then a CI gap-filler for static checks.\n const groups = await Promise.all([\n collectNodeChecks(root, opts.includeTests),\n collectTaskChecks(root, opts.includeTests),\n collectRustChecks(root, opts.includeTests),\n collectGoChecks(root, opts.includeTests),\n collectPythonChecks(root, opts.includeTests),\n collectCiChecks(root),\n ])\n const candidates = groups.flat()\n if (candidates.length === 0) return null\n\n const ecosystem =\n candidates.find((c) => c.source === \"package.json\") ? \"node\"\n : candidates.find((c) => c.source === \"Cargo.toml\") ? \"rust\"\n : candidates.find((c) => c.source === \"go.mod\") ? \"go\"\n : candidates.find((c) => /^(pyproject|setup|pytest|tox|ruff|mypy)/.test(c.source)) ? \"python\"\n : candidates.find((c) => /^(Makefile|makefile|justfile|\\.justfile|Taskfile)/.test(c.source)) ? \"make\"\n : \"ci\"\n\n const { checks, evidence } = pickByPriority(candidates)\n if (checks.length === 0) return null\n // Require at least one STATIC check (typecheck/lint/build) unless the caller\n // opted into running the full test suite — a test-only set is otherwise either\n // off-by-default or a slow/false-red risk on every stop.\n const hasStatic = checks.some((c) => STATIC_IDS.has(c.id as CheckId))\n if (!hasStatic && !opts.includeTests) return null\n return { kind: \"parsed\", checks, ecosystem, workdir: root, evidence }\n}\n\n/** The checks a descriptor runs: a sealed descriptor resolves its sealed command\n * set from the registry; parsed/discovered carry their own. Fresh array. */\nexport function checksForDescriptor(d: GateDescriptor): CheckSpec[] {\n if (d.kind === \"sealed\") {\n const sealed = resolveSealedGate(d.gateId)\n return sealed ? sealed.checks.map((c) => ({ id: c.id, command: c.command })) : []\n }\n return d.checks.map((c) => ({ id: c.id, command: c.command }))\n}\n\n/** A stable key over a descriptor's effective check set, for baseline isolation.\n * Sealed descriptors key on their gate id (preserving legacy baseline keys);\n * parsed/discovered key on the canonicalized (id,command) set, so a changed\n * command set yields a fresh baseline instead of masking/inventing regressions. */\nexport function descriptorHash(d: GateDescriptor): string {\n if (d.kind === \"sealed\") return `sealed:${d.gateId}`\n const canon = [...d.checks]\n .map((c) => `${c.id}\u0000${c.command.trim().replace(/\\s+/g, \" \")}`)\n .sort()\n .join(\"\u0001\")\n return `${d.kind}:${createHash(\"sha256\").update(canon).digest(\"hex\").slice(0, 32)}`\n}\n","/**\n * Evidence-pinned model FALLBACK for the structural Stop-gate — the last resort\n * when the deterministic parser (`harness-parse`) finds no runnable checks.\n *\n * A read-only worker reads the repo's own config/docs and proposes the canonical\n * check commands. Two guards keep this safe despite being model-authored:\n * 1. SANITIZE — `sanitizeDiscoveredCheck` rejects shell metacharacters (so a\n * command can never chain/redirect/expand), destructive/stateful verbs,\n * interactive/watch shapes, and an executable that isn't on PATH. What\n * survives is a single plain argv line, safe for `liveExec`'s naive split.\n * 2. EVIDENCE-PIN — a surviving command must appear (whitespace-normalized)\n * VERBATIM in one of the collected source files. The model cannot invent a\n * command or be prompt-injected into emitting one that isn't already a real\n * command in the repo (and a real command in a user-trusted repo is the\n * same authority the existing gate already runs).\n *\n * Discovery runs ONCE at launch and the result is cached per (repoFingerprint,\n * sourcesHash) in a human-readable record. The runtime Stop hook only READS the\n * cached record (no model call at stop). This is consumed ONLY by the local Stop\n * hook; the sealed-gate kernel never sees a discovered command.\n */\n\nimport { createHash } from \"node:crypto\"\nimport { existsSync, promises as fs } from \"node:fs\"\nimport nodePath from \"node:path\"\n\nimport { resolveExecutable } from \"~/lib/exec\"\nimport { PATHS } from \"~/lib/paths\"\n\nimport { type CheckSpec } from \"./gate-runner\"\nimport { isMutatingCommand, isSafeCommand, type CheckId } from \"./harness-parse\"\nimport { repoFingerprint, repoRoot } from \"./stop-gate-policy\"\n\n/** The allowlist of files the discovery worker is steered to read — config +\n * docs that legitimately describe how to check a project. Secret files are\n * additionally blocked at the worker IO layer (`.env*`/`*.pem`/`id_*`/…). */\nconst SIGNAL_FILES: ReadonlyArray<string> = [\n \"package.json\",\n \"Makefile\",\n \"makefile\",\n \"justfile\",\n \".justfile\",\n \"Taskfile.yml\",\n \"Taskfile.yaml\",\n \"Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"setup.cfg\",\n \"tox.ini\",\n \"pytest.ini\",\n \"CONTRIBUTING.md\",\n \"CONTRIBUTING\",\n \"README.md\",\n \"README\",\n \"DEVELOPING.md\",\n \"mix.exs\",\n \"build.gradle\",\n \"pom.xml\",\n \"composer.json\",\n]\nconst SIGNAL_DIRS: ReadonlyArray<string> = [\".github/workflows\"]\n\n/** A discovered check command after sanitization + evidence-pinning. */\nexport interface DiscoveredRecord {\n root: string\n fingerprint: string\n sourcesHash: string\n discoveredAt: string\n model: string\n ecosystem: string\n checks: CheckSpec[]\n confidence: string\n evidence: string[]\n}\n\nconst MAX_SIGNAL_BYTES = 64 * 1024\n/** Global caps so a repo with many workflow files can't inflate discovery /\n * hashing cost: at most this many files and this much total text. */\nconst MAX_SIGNAL_FILES = 40\nconst MAX_TOTAL_SIGNAL_BYTES = 512 * 1024\n\n/** Words that mark a command as destructive, stateful, or non-terminating — a\n * \"check\" must never do any of these. (Shell operators are already rejected by\n * `isSafeCommand`, so chaining can't smuggle them past this word scan.) */\nconst DENY_WORD =\n /\\b(rm|rmdir|mv|dd|mkfs|sudo|chmod|chown|publish|push|deploy|migrate|kubectl|terraform|docker|curl|wget|ssh|scp|rsync|nc|eval|npm i|npm install|yarn add|pip install|apt|brew|watch|serve|repl|dev|start)\\b/i\n\n/**\n * True when `command` is safe to auto-run as a check: a plain argv line (no\n * shell metacharacters), no destructive/stateful verb, no mutating/interactive\n * shape, and its executable resolves on PATH. The deny-word scan runs on a\n * whitespace-NORMALIZED copy so a tab / double-space can't split `npm install`\n * past the literal-space patterns.\n */\nexport function sanitizeDiscoveredCheck(command: string): boolean {\n if (!isSafeCommand(command)) return false\n const norm = normalizeWs(command)\n if (DENY_WORD.test(norm)) return false\n if (isMutatingCommand(norm)) return false // --fix/--write/--watch + :fix/:write scripts\n const first = norm.split(\" \")[0] ?? \"\"\n return resolveExecutable(first, { env: process.env }) !== null\n}\n\nfunction normalizeWs(s: string): string {\n return s.replace(/\\s+/g, \" \").trim()\n}\n\n/** Collect the text of the allowlisted signal files (capped), for evidence-pin +\n * the sources hash. Returns the concatenated text and the relative file list. */\nasync function collectSignals(root: string): Promise<{ text: string; files: string[] }> {\n const parts: string[] = []\n const files: string[] = []\n let total = 0\n const readCapped = async (abs: string, rel: string): Promise<void> => {\n if (files.length >= MAX_SIGNAL_FILES || total >= MAX_TOTAL_SIGNAL_BYTES) return\n try {\n const raw = await fs.readFile(abs, \"utf8\")\n const slice = raw.length > MAX_SIGNAL_BYTES ? raw.slice(0, MAX_SIGNAL_BYTES) : raw\n parts.push(slice)\n files.push(rel)\n total += slice.length\n } catch {\n /* unreadable → skip */\n }\n }\n for (const f of SIGNAL_FILES) {\n const abs = nodePath.join(root, f)\n if (existsSync(abs)) await readCapped(abs, f)\n }\n for (const d of SIGNAL_DIRS) {\n const dir = nodePath.join(root, d)\n try {\n for (const name of await fs.readdir(dir)) {\n if (/\\.(ya?ml)$/i.test(name)) await readCapped(nodePath.join(dir, name), nodePath.join(d, name))\n }\n } catch {\n /* no dir */\n }\n }\n return { text: parts.join(\"\\n\"), files }\n}\n\n/** A freshness hash over the signal files' contents + relative paths. A change\n * to how the project checks itself flips this → re-discovery on next launch. */\nexport async function sourcesHash(root: string): Promise<string> {\n const { text, files } = await collectSignals(root)\n return createHash(\"sha256\")\n .update(files.sort().join(\"\\n\"))\n .update(\"\\0\")\n .update(text)\n .digest(\"hex\")\n}\n\nfunction discoveredDir(): string {\n return nodePath.join(PATHS.APP_DIR, \"stop-gate\", \"discovered\")\n}\nfunction recordPathFor(root: string): string {\n return nodePath.join(discoveredDir(), createHash(\"sha256\").update(nodePath.resolve(root)).digest(\"hex\").slice(0, 32))\n}\n\n/** Read the cached discovered record for `root`, verifying it still matches the\n * live repo identity AND the live sources hash. Any mismatch / unreadable /\n * empty-checks record → null (re-discover or stay off; never run a stale set). */\nexport async function readDiscoveredGate(root: string): Promise<DiscoveredRecord | null> {\n let rec: DiscoveredRecord\n try {\n rec = JSON.parse(await fs.readFile(recordPathFor(root), \"utf8\")) as DiscoveredRecord\n } catch {\n return null\n }\n if (!rec || !Array.isArray(rec.checks) || rec.checks.length === 0) return null\n const fp = await repoFingerprint(root).catch(() => \"\")\n if (fp.length === 0 || fp !== rec.fingerprint) return null // identity drift → deny.\n const sh = await sourcesHash(root).catch(() => \"\")\n if (sh.length === 0 || sh !== rec.sourcesHash) return null // config changed → stale.\n // Re-validate id AND re-sanitize the command at read time (defense-in-depth\n // against a tampered/corrupt record).\n const checks = rec.checks.filter(\n (c) => c && typeof c.id === \"string\" && VALID_IDS.has(c.id) && typeof c.command === \"string\" && sanitizeDiscoveredCheck(c.command),\n )\n if (checks.length === 0) return null\n return { ...rec, checks }\n}\n\nexport async function writeDiscoveredGate(rec: DiscoveredRecord): Promise<void> {\n await fs.mkdir(discoveredDir(), { recursive: true })\n const tmp = `${recordPathFor(rec.root)}.${process.pid}.tmp`\n await fs.writeFile(tmp, `${JSON.stringify(rec, null, 2)}\\n`, { mode: 0o600 })\n await fs.rename(tmp, recordPathFor(rec.root))\n}\n\nconst DISCOVERY_PROMPT = (files: string[]): string =>\n `You are configuring an automated pre-finish CHECK gate for this repository. Read ONLY these `\n + `already-present config/doc files to learn how the project checks itself: ${files.join(\", \")}. `\n + `Identify the canonical FAST static check command (typecheck and/or lint) and, if obvious, the test `\n + `command. Rules: (1) return a command ONLY if it appears VERBATIM in one of those files — never invent `\n + `one; (2) commands must be non-interactive, self-terminating, read-only verification (NO install / `\n + `publish / push / deploy / migrate / format-in-place / watch / serve / dev-server / delete); (3) at most `\n + `3 commands; (4) if unsure, return an empty list. Respond with ONLY a fenced \\`\\`\\`json block of the shape `\n + `{\"ecosystem\":\"<label>\",\"checks\":[{\"id\":\"typecheck|lint|test\",\"command\":\"<single-line command>\"}],`\n + `\"confidence\":\"high|low\"} and NOTHING else.`\n\ninterface DiscoverResult {\n ecosystem: string\n checks: CheckSpec[]\n confidence: string\n evidence: string[]\n}\n\n/** Extract the first fenced/bare JSON object from the worker's text. */\nfunction extractJson(text: string): unknown {\n const fenced = /```(?:json)?\\s*([\\s\\S]*?)```/i.exec(text)\n const body = fenced ? fenced[1] : text\n const start = body.indexOf(\"{\")\n const end = body.lastIndexOf(\"}\")\n if (start < 0 || end <= start) return undefined\n try {\n return JSON.parse(body.slice(start, end + 1)) as unknown\n } catch {\n return undefined\n }\n}\n\nconst VALID_IDS: ReadonlySet<string> = new Set<CheckId>([\"typecheck\", \"lint\", \"test\"])\n\n/**\n * Sanitize + EVIDENCE-PIN the model's raw checks against the collected source\n * text. Pure (no IO) so it is unit-testable without the worker. A check survives\n * only if: its id is canonical, it isn't a `test` while tests are off, its id\n * hasn't already been taken, it passes `sanitizeDiscoveredCheck`, AND its command\n * appears (whitespace-normalized) VERBATIM in `evidenceText`. The evidence-pin is\n * the load-bearing guard: the model cannot invent a command or be prompt-injected\n * into one that isn't already a real command in the (user-trusted) repo.\n */\nexport function filterDiscoveredChecks(\n rawChecks: unknown,\n evidenceText: string,\n includeTests: boolean,\n): CheckSpec[] {\n const evidenceNorm = normalizeWs(evidenceText)\n const seen = new Set<string>()\n const checks: CheckSpec[] = []\n for (const c of Array.isArray(rawChecks) ? rawChecks : []) {\n if (!c || typeof c !== \"object\") continue\n const id = (c as { id?: unknown }).id\n const command = (c as { command?: unknown }).command\n if (typeof id !== \"string\" || !VALID_IDS.has(id)) continue\n if (typeof command !== \"string\") continue\n if (id === \"test\" && !includeTests) continue\n if (seen.has(id)) continue\n if (!sanitizeDiscoveredCheck(command)) continue\n if (!evidenceNorm.includes(normalizeWs(command))) continue\n seen.add(id)\n checks.push({ id, command: command.trim() })\n }\n return checks\n}\n\n/**\n * Run the read-only worker to discover check commands for `cwd`. Returns the\n * sanitized + evidence-pinned result, or null (worker unavailable / errored /\n * nothing survived). NEVER throws.\n */\nexport async function discoverGateCommands(\n cwd: string,\n opts: { signal?: AbortSignal; includeTests: boolean },\n): Promise<DiscoverResult | null> {\n const root = await repoRoot(cwd).catch(() => cwd)\n const { text: evidenceText, files } = await collectSignals(root)\n if (files.length === 0) return null\n\n let result: { text: string; isError?: boolean }\n try {\n // Lazy-import the heavy worker-agent engine ONLY when discovery actually\n // runs — keeping it out of the module-load graph of `claude.ts` (which\n // imports this module), so the mock.module-isolated CLI tests don't deadlock\n // pulling the Pi runtime at load time.\n const { runWorkerAgent } = await import(\"~/lib/worker-agent/engine\")\n // Discovery is advisory setup, not a verified workflow producer/checker,\n // so honoring the operator's explore default here is intentional.\n result = await runWorkerAgent({\n mode: \"explore\",\n workspace: root,\n prompt: DISCOVERY_PROMPT(files),\n signal: opts.signal,\n })\n } catch {\n return null\n }\n if (result.isError) return null\n const parsed = extractJson(result.text)\n if (!parsed || typeof parsed !== \"object\") return null\n const obj = parsed as { ecosystem?: unknown; checks?: unknown; confidence?: unknown }\n const checks = filterDiscoveredChecks(obj.checks, evidenceText, opts.includeTests)\n if (checks.length === 0) return null\n const ecosystem = typeof obj.ecosystem === \"string\" && obj.ecosystem.length > 0 ? obj.ecosystem : \"discovered\"\n const confidence = typeof obj.confidence === \"string\" ? obj.confidence : \"low\"\n return { ecosystem, checks, confidence, evidence: files }\n}\n"],"mappings":"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"}
|
package/dist/hooks.mjs
CHANGED
|
@@ -10980,7 +10980,8 @@ var init_fast_profile_contract = __esmMin((() => {
|
|
|
10980
10980
|
implementer: "gemini-3.8-flash",
|
|
10981
10981
|
reviewer: "claude-sonnet-5",
|
|
10982
10982
|
advisor: "gpt-5.6-sol",
|
|
10983
|
-
oracle: "claude-opus-5"
|
|
10983
|
+
oracle: "claude-opus-5",
|
|
10984
|
+
astra: "gpt-6-astra"
|
|
10984
10985
|
});
|
|
10985
10986
|
FAST_PROFILE_NATIVE_AGENT_NAMES = [
|
|
10986
10987
|
"Explore",
|
|
@@ -11006,6 +11007,7 @@ var init_fast_profile_contract = __esmMin((() => {
|
|
|
11006
11007
|
FAST_PROFILE_ADVISOR_MODEL = FAST_PROFILE_MODELS.advisor;
|
|
11007
11008
|
`${FAST_PROFILE_ADVISOR_MODEL}`;
|
|
11008
11009
|
FAST_PROFILE_MODELS.oracle;
|
|
11010
|
+
FAST_PROFILE_MODELS.astra;
|
|
11009
11011
|
FAST_PROFILE_DELEGATION_GRAPH = Object.freeze({
|
|
11010
11012
|
Explore: Object.freeze([]),
|
|
11011
11013
|
Plan: Object.freeze(["Explore", "reviewer"]),
|
package/dist/hooks.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4fcac8bff9996b835fd54e27d98f5262a4579be8f8b55a1bae1bdd7aaaa0431e
|
package/dist/{internal-fast-dispatch-guard-kzSKB0yV.js → internal-fast-dispatch-guard-19xQEouu.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as FAST_PROFILE_NATIVE_AGENT_NAMES, s as FAST_PROFILE_DELEGATION_GRAPH } from "./fast-profile-contract-_DhIWank.js";
|
|
2
2
|
import { t as buildSelfCommand } from "./self-invocation-B84JCHps.js";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
@@ -244,4 +244,4 @@ function buildFastDispatchGuardHookCommand(invocation, opts) {
|
|
|
244
244
|
//#endregion
|
|
245
245
|
export { internalFastDispatchGuard as n, FAST_DISPATCH_TOOL_MATCHER as r, buildFastDispatchGuardHookCommand as t };
|
|
246
246
|
|
|
247
|
-
//# sourceMappingURL=internal-fast-dispatch-guard-
|
|
247
|
+
//# sourceMappingURL=internal-fast-dispatch-guard-19xQEouu.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-fast-dispatch-guard-kzSKB0yV.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":";;;;;;;;;;;;;AAeA,MAAa,6BAA6B;AAE1C,MAAa,0BAA0B;AAOF,IAAI,IAAY,uBAAuB;;AAG5E,MAAa,sBAET,OAAO,OAAO;CAChB,SAAS,IAAI,IAA4B,8BAA8B,OAAO;CAC9E,MAAM,IAAI,IAA4B,8BAA8B,IAAI;CACxE,mBAAmB,IAAI,IACrB,8BAA8B,kBAChC;CACA,aAAa,IAAI,IACf,8BAA8B,WAChC;CACA,UAAU,IAAI,IAA4B,8BAA8B,QAAQ;CAChF,iCAAiB,IAAI,IAA4B,CAAC,CAAC;AACrD,CAAC;;AAGD,MAAa,6BAA6B;AA8B1C,SAAS,OAAO,KAAa,KAAsB;CACjD,OAAO,OAAO,UAAU,eAAe,KAAK,KAAK,GAAG;AACtD;AAEA,SAAS,iBAAiB,KAA8B,OAA8C;CACpG,OAAO,MAAM,QAAQ,SAAS,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,KAAK;AAC1E;;;;;AAMA,SAAS,qBACP,KACA,OAC6E;CAC7E,MAAM,SAAS,iBAAiB,KAAK,KAAK;CAC1C,IAAI,OAAO,WAAW,GAAG,OAAO;EAAE,SAAS;EAAO,WAAW;EAAO,UAAU;CAAM;CACpF,MAAM,WAAW,OAAO,QAAQ,UAAU,UAAU,KAAA,KAAa,UAAU,IAAI;CAC/E,IAAI,SAAS,MAAM,UAAU,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,WAAW,CAAC,GACjF,OAAO;EAAE,SAAS;EAAM,WAAW;EAAM,UAAU;CAAM;CAE3D,MAAM,WAAW,CAAC,GAAG,IAAI,IAAK,SAA2B,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAAC;CACtF,IAAI,SAAS,SAAS,GAAG,OAAO;EAAE,SAAS;EAAM,WAAW;EAAO,UAAU;CAAK;CAClF,OAAO;EAAE,SAAS;EAAM,OAAO,SAAS;EAAI,WAAW;EAAO,UAAU;CAAM;AAChF;AAEA,SAAS,KACP,QACA,QACA,QACyB;CACzB,OAAO;EAAE,SAAS;EAAO;EAAQ;EAAQ;EAAQ,SAAS;CAAO;AACnE;;;;;AAMA,MAAM,sBAA6C;CACjD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;AASA,SAAgB,wBACd,OACA,MACyB;CACzB,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,UAAU,WAAW,KAAK,MAAM,KAAK,IAAI;CAC3D,QAAQ;EACN,OAAO,KAAK,8CAA8C;CAC5D;CACA,IAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAC/D,OAAO,KAAK,sDAAsD;CAGpE,MAAM,UAAU;CAChB,IAAI,OAAO,QAAQ,cAAc,UAC/B,OAAO,KAAK,qDAAqD;CAEnE,MAAM,WAAW,QAAQ;CACzB,IAAI,CAAC,iBAAiB,KAAK,QAAQ,GACjC,OAAO;EAAE,SAAS;EAAM,SAAS;CAAqB;CAGxD,IAAI,CAAC,QAAQ,cAAc,OAAO,QAAQ,eAAe,YAAY,MAAM,QAAQ,QAAQ,UAAU,GACnG,OAAO,KAAK,yBAAyB,SAAS,kCAAkC;CAElF,MAAM,YAAY,QAAQ;CAG1B,KAAK,MAAM,SAAS,qBAClB,IAAI,OAAO,WAAW,KAAK,GAAG;EAC5B,MAAM,MAAM,UAAU;EACtB,IAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,QAAQ,OAC/C,OAAO,KAAK,yBAAyB,SAAS,8BAA8B,MAAM,mCAAmC;CAEzH;CAGF,MAAM,eAAe,qBACnB,WACA,CAAC,iBAAiB,cAAc,CAClC;CACA,IAAI,aAAa,WACf,OAAO,KAAK,yBAAyB,SAAS,8BAA8B;CAE9E,IAAI,aAAa,UACf,OAAO,KAAK,yBAAyB,SAAS,yBAAyB;CAEzE,MAAM,cAAc,aAAa;CAEjC,MAAM,0BAA0B,MAAM,iBAClC,IAAI,IAAI,KAAK,cAAc,oBAC3B,IAAI,IAAY,CACd,GAAG,yBACH,GAAI,MAAM,cAAc,CAAC,0BAA0B,IAAI,CAAC,CAC1D,CAAC;CAEL,IAAI,CAAC,eAAe,CAAC,wBAAwB,IAAI,WAAW,GAC1D,OAAO,KACL,yBAAyB,SAAS,yBAAyB,CAAC,GAAG,uBAAuB,CAAC,CAAC,KAAK,IAAI,GACnG;CAEF,MAAM,SAAS;CAKf,MAAM,eAAe,EAAE,GAAG,UAAU;CACpC,OAAO,aAAa;CAEpB,MAAM,aAAa,qBAAqB,SAAoC,CAAC,cAAc,WAAW,CAAC;CACvG,MAAM,WAAW,qBAAqB,SAAoC,CAAC,YAAY,SAAS,CAAC;CACjG,MAAM,eAAe,qBAAqB,SAAoC,CAC5E,sBACA,iBACF,CAAC;CACD,IAAI,WAAW,aAAa,SAAS,aAAa,aAAa,WAC7D,OAAO,KAAK,yBAAyB,SAAS,gCAAgC,MAAM;CAEtF,IAAI,WAAW,YAAY,SAAS,YAAY,aAAa,UAC3D,OAAO,KAAK,yBAAyB,SAAS,oCAAoC,MAAM;CAG1F,MAAM,kBAAkB,WAAW;CACnC,MAAM,gBAAgB,SAAS;CAC/B,MAAM,cAAc,aAAa;CAIjC,IAAI,CAAC,mBAAmB,eACtB,OAAO,KAAK,yBAAyB,SAAS,mCAAmC,MAAM;CAEzF,IAAI,CAAC,mBAAmB,aACtB,OAAO,KAAK,yBAAyB,SAAS,sCAAsC,MAAM;CAK5F,IAAI,CAAC,iBAAiB;EACpB,MAAM,mBAAmB,WAAW,WAAW,SAAS,WAAW,aAAa;EAChF,MAAM,6BAA6B;GAAC;GAAY;GAAU;EAAY,CAAC,CAAC,MACrE,WAAW,OAAO,UAAU,KAAA,CAC/B;EACA,IAAI,oBAAoB,4BACtB,OAAO,KAAK,yBAAyB,SAAS,iCAAiC,MAAM;EAEvF,OAAO;GACL,SAAS;GACT,QAAQ;GACR;GACA;GACA,SAAS;EACX;CACF;CAOA,IAAI,kBAAC,IALoB,IAAY,CACnC,GAAG,yBAAA,eAEL,CAEgB,EAAA,CAAE,IAAI,eAAe,GACnC,OAAO,KAAK,6CAA6C,KAAK,UAAU,eAAe,KAAK,MAAM;CAEpG,MAAM,SAAS;CAEf,IAAI,EADmB,oBAAoB,2BAAW,IAAI,IAA4B,EAAA,CAClE,IAAI,MAAM,GAC5B,OAAO,KAAK,yBAAyB,OAAO,iBAAiB,UAAU,QAAQ,MAAM;CAEvF,OAAO;EAAE,SAAS;EAAM;EAAQ;EAAQ;EAAc,SAAS;CAAQ;AACzE;;AAGA,SAAgB,wBACd,cACQ;CACR,OAAO,KAAK,UAAU,EACpB,oBAAoB;EAClB,eAAe;EACf,oBAAoB;EACpB;CACF,EACF,CAAC;AACH;;AAGA,SAAgB,uBAAuB,QAAwB;CAC7D,OAAO,KAAK,UAAU,EACpB,oBAAoB;EAClB,eAAe;EACf,oBAAoB;EACpB,0BAA0B;CAC5B,EACF,CAAC;AACH;;;;;;;;;;ACvRA,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,4BAA4B,cAAc;CACrD,MAAM;EACJ,MAAM;EACN,aACE;CACJ;CACA,MAAM;EACJ,aAAa;GACX,MAAM;GACN,aAAa;GACb,SAAS;EACX;EACA,gBAAgB;GACd,MAAM;GACN,aAAa;EACf;CACF;CACA,IAAI,EAAE,QAAQ;EACZ,MAAM,iBAAiB,KAAK,iBACxB,KAAK,eAAe,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO,IAClE,KAAA;EACJ,MAAM,cAAc,KAAK,gBAAgB,SAAS,iBAAiB,eAAe,SAAS,eAAe,IAAI;EAC9G,MAAM,WAAW,wBAAwB,cAAc,GAAG;GACxD;GACA;EACF,CAAC;EACD,IAAI,CAAC,SAAS,WAAW,SAAS,QAChC,QAAQ,OAAO,MAAM,uBAAuB,SAAS,MAAM,CAAC;OACvD,IAAI,SAAS,YAAY,WAAW,SAAS,cAClD,QAAQ,OAAO,MAAM,wBAAwB,SAAS,YAAY,CAAC;EAIrE,QAAQ,WAAW;CACrB;AACF,CAAC;;AAGD,SAAgB,kCACd,YACA,MACQ;CACR,MAAM,QAAkB,CAAC;CACzB,IAAI,MAAM,aAAa,MAAM,KAAK,eAAe;CACjD,IAAI,MAAM,kBAAkB,KAAK,eAAe,SAAS,GACvD,MAAM,KAAK,qBAAqB,KAAK,eAAe,KAAK,GAAG,EAAE,EAAE;CAElE,MAAM,OAAO,CAAC,gCAAgC,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG;CAChE,OAAO,iBAAiB,YAAY,IAAI;AAC1C"}
|
|
1
|
+
{"version":3,"file":"internal-fast-dispatch-guard-19xQEouu.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-CrsZI0pO.js → internal-fast-dispatch-guard-D8YNlnuQ.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-19xQEouu.js";
|
|
2
2
|
export { internalFastDispatchGuard };
|
package/dist/{internal-max-dispatch-guard-lS4usOV5.js → internal-max-dispatch-guard-DZ7KLWxP.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as MAX_PROFILE_NATIVE_AGENT_NAMES, o as MAX_PROFILE_NATIVE_EFFORTS, s as MAX_PROFILE_NATIVE_MODELS } from "./max-profile-contract-
|
|
1
|
+
import { a as MAX_PROFILE_NATIVE_AGENT_NAMES, o as MAX_PROFILE_NATIVE_EFFORTS, s as MAX_PROFILE_NATIVE_MODELS } from "./max-profile-contract-BcrMlrmo.js";
|
|
2
2
|
import { t as buildSelfCommand } from "./self-invocation-B84JCHps.js";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
@@ -219,4 +219,4 @@ function buildMaxDispatchGuardHookCommand(invocation, opts = {}) {
|
|
|
219
219
|
//#endregion
|
|
220
220
|
export { assertMaxDispatchGuardInstalled as i, internalMaxDispatchGuard as n, MAX_DISPATCH_TOOL_MATCHER as r, buildMaxDispatchGuardHookCommand as t };
|
|
221
221
|
|
|
222
|
-
//# sourceMappingURL=internal-max-dispatch-guard-
|
|
222
|
+
//# sourceMappingURL=internal-max-dispatch-guard-DZ7KLWxP.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-max-dispatch-guard-lS4usOV5.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_EFFORTS,\n MAX_PROFILE_NATIVE_MODELS,\n type MaxProfileNativeAgentName,\n} from \"./max-profile-contract\"\nimport type { Effort } from \"./reasoning-effort\"\n\nexport const MAX_DISPATCH_TOOL_MATCHER = \"^(Task|Agent)$\"\n\nexport const MAX_BROWSE_DISPATCH_AGENT = \"worker-browse\" as const\n\nconst MAX_NATIVE_AGENT_SET = new Set<string>([\n ...MAX_PROFILE_NATIVE_AGENT_NAMES,\n MAX_BROWSE_DISPATCH_AGENT,\n])\n\n/** Assert max launches never start without their native Task/Agent ACL. */\nexport class MaxDispatchGuardInstallError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"MaxDispatchGuardInstallError\"\n }\n}\n\nexport function assertMaxDispatchGuardInstalled(\n maxProfile: boolean,\n installationSucceeded: boolean,\n): void {\n if (maxProfile && !installationSucceeded) {\n throw new MaxDispatchGuardInstallError(\n \"max profile requires the native Task/Agent ACL hook, but it could not be installed; refusing to start an unguarded max session.\",\n )\n }\n}\n\nconst MAX_ALLOWED_MODEL_BASES = new Set([\"gpt-5.6-luna\", \"gemini-3.8-flash\", \"grok-4.6\"])\n\n/**\n * Claude Code's public Agent schema currently requires one of these built-in\n * model aliases even when the selected custom agent already owns its model in\n * frontmatter. They are transport placeholders in max, not model overrides.\n * Strip them so the role's catalog-validated max model remains authoritative.\n * Case and a trailing `[1m]` are ignored deliberately: neither can turn a\n * client-owned placeholder into an Opus override in the closed max roster.\n */\nexport const MAX_AGENT_SCHEMA_MODEL_ALIASES = Object.freeze([\n \"sonnet\",\n \"opus\",\n \"haiku\",\n \"fable\",\n] as const)\n\nconst MAX_AGENT_SCHEMA_MODEL_ALIAS_SET = new Set<string>(\n MAX_AGENT_SCHEMA_MODEL_ALIASES,\n)\n\nfunction isMaxAgentSchemaModelAlias(model: unknown): boolean {\n if (typeof model !== \"string\") return false\n return MAX_AGENT_SCHEMA_MODEL_ALIAS_SET.has(baseModel(model.trim()).toLowerCase())\n}\n\nexport interface MaxDispatchGuardOptions {\n /** Catalog-resolved reviewer model for this launch (Sonnet 5 1M/xhigh). */\n reviewerModel?: string\n reviewerEffort?: Extract<Effort, \"high\" | \"xhigh\" | \"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 === \"claude-sonnet-5\" || model === \"claude-opus-5\"\n ? [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"]\n : model === \"grok-4.6\" || model === \"gemini-3.8-flash\"\n ? [\"low\", \"medium\", \"high\"]\n : []\n if (allowed.includes(effort as Effort)) return effort as Effort\n return undefined\n}\n\n/**\n * Native max Task/Agent ACL. Role frontmatter owns the fixed model. Required\n * public-schema aliases are stripped as transport placeholders; a client that\n * can express custom ids may override only to Luna, Gemini, or Grok with a\n * live-ladder effort. Sol/Codex/unknown/private ids are rejected. The lead may\n * target any emitted max role; role-to-role edges are unrestricted here.\n */\nexport function decideMaxDispatchGuard(\n stdin: string | unknown,\n opts: MaxDispatchGuardOptions = {},\n): MaxDispatchDecision {\n let parsed: unknown\n try {\n parsed = typeof stdin === \"string\" ? JSON.parse(stdin) : stdin\n } catch {\n return { allowed: false, reason: \"max dispatch denied: malformed hook payload\", verdict: \"deny\" }\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return { allowed: false, reason: \"max dispatch denied: hook payload must be an object\", verdict: \"deny\" }\n }\n const payload = parsed as Record<string, unknown>\n if (typeof payload.tool_name !== \"string\") {\n return { allowed: false, reason: \"max dispatch denied: hook payload has no tool_name\", verdict: \"deny\" }\n }\n if (!/^(Task|Agent)$/.test(payload.tool_name)) {\n return { allowed: true, verdict: \"allow-non-dispatch\" }\n }\n if (!payload.tool_input || typeof payload.tool_input !== \"object\" || Array.isArray(payload.tool_input)) {\n return { allowed: false, reason: `max dispatch denied: ${payload.tool_name} payload has malformed tool_input`, verdict: \"deny\" }\n }\n const toolInput = payload.tool_input as Record<string, unknown>\n const target = stringField(toolInput, \"subagent_type\", \"subagentType\")\n if (!target || !MAX_NATIVE_AGENT_SET.has(target)) {\n return { allowed: false, reason: `max dispatch denied: target must be one of ${[...MAX_PROFILE_NATIVE_AGENT_NAMES, MAX_BROWSE_DISPATCH_AGENT].join(\", \")}`, verdict: \"deny\" }\n }\n const nativeTarget = target as MaxProfileNativeAgentName | typeof MAX_BROWSE_DISPATCH_AGENT\n const modelValue = toolInput.model\n const schemaAlias = isMaxAgentSchemaModelAlias(modelValue)\n const normalizedModel = modelValue === undefined || schemaAlias\n ? undefined\n : normalizeMaxDispatchModel(modelValue)\n if (modelValue !== undefined && !schemaAlias && !normalizedModel) {\n return { allowed: false, reason: \"max dispatch denied: model must be a Claude Code Agent schema alias, Luna[1m], Gemini 3.8 Flash[1m], or bare Grok 4.6\", target, verdict: \"deny\" }\n }\n const requestedEffort = toolInput.thinking ?? toolInput.effort\n const effectiveModel = normalizedModel\n ? baseModel(normalizedModel)\n : nativeTarget === MAX_BROWSE_DISPATCH_AGENT\n // Keep this aligned with BROWSE_DEFAULT_MODEL. Importing worker-agent/engine\n // here would pull the worker runtime into the small hook bundle.\n ? \"gpt-5.6-luna\"\n : nativeTarget === \"reviewer\" && opts.reviewerModel\n ? baseModel(opts.reviewerModel)\n : MAX_PROFILE_NATIVE_MODELS[nativeTarget]\n const effort = normalizeMaxDispatchEffort(effectiveModel, requestedEffort)\n if ((toolInput.thinking !== undefined || toolInput.effort !== undefined) && !effort) {\n return { allowed: false, reason: \"max dispatch denied: reasoning override is not supported by the selected model\", target, verdict: \"deny\" }\n }\n const fixedReviewerEffort = nativeTarget === \"reviewer\"\n && !normalizedModel\n && opts.reviewerModel\n ? opts.reviewerEffort ?? MAX_PROFILE_NATIVE_EFFORTS.reviewer\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 launch-resolved reviewer\n // effort so the dispatch cannot drift from the generated frontmatter.\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 reviewerEffort = args.reviewerEffort === \"max\"\n ? \"max\"\n : args.reviewerEffort === \"xhigh\"\n ? \"xhigh\"\n : \"high\"\n const decision = decideMaxDispatchGuard(readStdinSync(), {\n reviewerModel: args.reviewerModel,\n reviewerEffort,\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\" | \"xhigh\" | \"max\" } = {},\n): string {\n const flags: string[] = []\n if (opts.reviewerModel) flags.push(`--reviewerModel \"${opts.reviewerModel}\"`)\n if (opts.reviewerEffort) flags.push(`--reviewerEffort ${opts.reviewerEffort}`)\n return buildSelfCommand(\n invocation,\n [\"internal-max-dispatch-guard\", ...flags].join(\" \"),\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"internal-max-dispatch-guard-DZ7KLWxP.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_EFFORTS,\n MAX_PROFILE_NATIVE_MODELS,\n type MaxProfileNativeAgentName,\n} from \"./max-profile-contract\"\nimport type { Effort } from \"./reasoning-effort\"\n\nexport const MAX_DISPATCH_TOOL_MATCHER = \"^(Task|Agent)$\"\n\nexport const MAX_BROWSE_DISPATCH_AGENT = \"worker-browse\" as const\n\nconst MAX_NATIVE_AGENT_SET = new Set<string>([\n ...MAX_PROFILE_NATIVE_AGENT_NAMES,\n MAX_BROWSE_DISPATCH_AGENT,\n])\n\n/** Assert max launches never start without their native Task/Agent ACL. */\nexport class MaxDispatchGuardInstallError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"MaxDispatchGuardInstallError\"\n }\n}\n\nexport function assertMaxDispatchGuardInstalled(\n maxProfile: boolean,\n installationSucceeded: boolean,\n): void {\n if (maxProfile && !installationSucceeded) {\n throw new MaxDispatchGuardInstallError(\n \"max profile requires the native Task/Agent ACL hook, but it could not be installed; refusing to start an unguarded max session.\",\n )\n }\n}\n\nconst MAX_ALLOWED_MODEL_BASES = new Set([\"gpt-5.6-luna\", \"gemini-3.8-flash\", \"grok-4.6\"])\n\n/**\n * Claude Code's public Agent schema currently requires one of these built-in\n * model aliases even when the selected custom agent already owns its model in\n * frontmatter. They are transport placeholders in max, not model overrides.\n * Strip them so the role's catalog-validated max model remains authoritative.\n * Case and a trailing `[1m]` are ignored deliberately: neither can turn a\n * client-owned placeholder into an Opus override in the closed max roster.\n */\nexport const MAX_AGENT_SCHEMA_MODEL_ALIASES = Object.freeze([\n \"sonnet\",\n \"opus\",\n \"haiku\",\n \"fable\",\n] as const)\n\nconst MAX_AGENT_SCHEMA_MODEL_ALIAS_SET = new Set<string>(\n MAX_AGENT_SCHEMA_MODEL_ALIASES,\n)\n\nfunction isMaxAgentSchemaModelAlias(model: unknown): boolean {\n if (typeof model !== \"string\") return false\n return MAX_AGENT_SCHEMA_MODEL_ALIAS_SET.has(baseModel(model.trim()).toLowerCase())\n}\n\nexport interface MaxDispatchGuardOptions {\n /** Catalog-resolved reviewer model for this launch (Sonnet 5 1M/xhigh). */\n reviewerModel?: string\n reviewerEffort?: Extract<Effort, \"high\" | \"xhigh\" | \"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 === \"claude-sonnet-5\" || model === \"claude-opus-5\"\n ? [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"]\n : model === \"grok-4.6\" || model === \"gemini-3.8-flash\"\n ? [\"low\", \"medium\", \"high\"]\n : []\n if (allowed.includes(effort as Effort)) return effort as Effort\n return undefined\n}\n\n/**\n * Native max Task/Agent ACL. Role frontmatter owns the fixed model. Required\n * public-schema aliases are stripped as transport placeholders; a client that\n * can express custom ids may override only to Luna, Gemini, or Grok with a\n * live-ladder effort. Sol/Codex/unknown/private ids are rejected. The lead may\n * target any emitted max role; role-to-role edges are unrestricted here.\n */\nexport function decideMaxDispatchGuard(\n stdin: string | unknown,\n opts: MaxDispatchGuardOptions = {},\n): MaxDispatchDecision {\n let parsed: unknown\n try {\n parsed = typeof stdin === \"string\" ? JSON.parse(stdin) : stdin\n } catch {\n return { allowed: false, reason: \"max dispatch denied: malformed hook payload\", verdict: \"deny\" }\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return { allowed: false, reason: \"max dispatch denied: hook payload must be an object\", verdict: \"deny\" }\n }\n const payload = parsed as Record<string, unknown>\n if (typeof payload.tool_name !== \"string\") {\n return { allowed: false, reason: \"max dispatch denied: hook payload has no tool_name\", verdict: \"deny\" }\n }\n if (!/^(Task|Agent)$/.test(payload.tool_name)) {\n return { allowed: true, verdict: \"allow-non-dispatch\" }\n }\n if (!payload.tool_input || typeof payload.tool_input !== \"object\" || Array.isArray(payload.tool_input)) {\n return { allowed: false, reason: `max dispatch denied: ${payload.tool_name} payload has malformed tool_input`, verdict: \"deny\" }\n }\n const toolInput = payload.tool_input as Record<string, unknown>\n const target = stringField(toolInput, \"subagent_type\", \"subagentType\")\n if (!target || !MAX_NATIVE_AGENT_SET.has(target)) {\n return { allowed: false, reason: `max dispatch denied: target must be one of ${[...MAX_PROFILE_NATIVE_AGENT_NAMES, MAX_BROWSE_DISPATCH_AGENT].join(\", \")}`, verdict: \"deny\" }\n }\n const nativeTarget = target as MaxProfileNativeAgentName | typeof MAX_BROWSE_DISPATCH_AGENT\n const modelValue = toolInput.model\n const schemaAlias = isMaxAgentSchemaModelAlias(modelValue)\n const normalizedModel = modelValue === undefined || schemaAlias\n ? undefined\n : normalizeMaxDispatchModel(modelValue)\n if (modelValue !== undefined && !schemaAlias && !normalizedModel) {\n return { allowed: false, reason: \"max dispatch denied: model must be a Claude Code Agent schema alias, Luna[1m], Gemini 3.8 Flash[1m], or bare Grok 4.6\", target, verdict: \"deny\" }\n }\n const requestedEffort = toolInput.thinking ?? toolInput.effort\n const effectiveModel = normalizedModel\n ? baseModel(normalizedModel)\n : nativeTarget === MAX_BROWSE_DISPATCH_AGENT\n // Keep this aligned with BROWSE_DEFAULT_MODEL. Importing worker-agent/engine\n // here would pull the worker runtime into the small hook bundle.\n ? \"gpt-5.6-luna\"\n : nativeTarget === \"reviewer\" && opts.reviewerModel\n ? baseModel(opts.reviewerModel)\n : MAX_PROFILE_NATIVE_MODELS[nativeTarget]\n const effort = normalizeMaxDispatchEffort(effectiveModel, requestedEffort)\n if ((toolInput.thinking !== undefined || toolInput.effort !== undefined) && !effort) {\n return { allowed: false, reason: \"max dispatch denied: reasoning override is not supported by the selected model\", target, verdict: \"deny\" }\n }\n const fixedReviewerEffort = nativeTarget === \"reviewer\"\n && !normalizedModel\n && opts.reviewerModel\n ? opts.reviewerEffort ?? MAX_PROFILE_NATIVE_EFFORTS.reviewer\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 launch-resolved reviewer\n // effort so the dispatch cannot drift from the generated frontmatter.\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 reviewerEffort = args.reviewerEffort === \"max\"\n ? \"max\"\n : args.reviewerEffort === \"xhigh\"\n ? \"xhigh\"\n : \"high\"\n const decision = decideMaxDispatchGuard(readStdinSync(), {\n reviewerModel: args.reviewerModel,\n reviewerEffort,\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\" | \"xhigh\" | \"max\" } = {},\n): string {\n const flags: string[] = []\n if (opts.reviewerModel) flags.push(`--reviewerModel \"${opts.reviewerModel}\"`)\n if (opts.reviewerEffort) flags.push(`--reviewerEffort ${opts.reviewerEffort}`)\n return buildSelfCommand(\n invocation,\n [\"internal-max-dispatch-guard\", ...flags].join(\" \"),\n )\n}\n"],"mappings":"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"}
|
package/dist/{internal-max-dispatch-guard-DF-f8VwE.js → internal-max-dispatch-guard-_Pp4LltM.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-DZ7KLWxP.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-DjLA-lxB.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-qzqXfRxE.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-DDkRvyYL.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-YyYVQbbN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-stop-hook-XRtB33hW.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-YyYVQbbN.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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"}
|