@webpieces/ai-hook-rules 0.4.703 → 0.4.705

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +4 -2
  2. package/src/adapters/agent-adapters.d.ts +13 -0
  3. package/src/adapters/agent-adapters.js +23 -0
  4. package/src/adapters/agent-adapters.js.map +1 -0
  5. package/src/adapters/agent-payload.d.ts +48 -0
  6. package/src/adapters/agent-payload.js +30 -0
  7. package/src/adapters/agent-payload.js.map +1 -0
  8. package/src/adapters/agent-response.d.ts +6 -0
  9. package/src/adapters/{claude-code-response.js → agent-response.js} +56 -22
  10. package/src/adapters/agent-response.js.map +1 -0
  11. package/src/adapters/claude-code-adapter.d.ts +26 -0
  12. package/src/adapters/claude-code-adapter.js +69 -0
  13. package/src/adapters/claude-code-adapter.js.map +1 -0
  14. package/src/adapters/codex-adapter.d.ts +19 -0
  15. package/src/adapters/codex-adapter.js +48 -0
  16. package/src/adapters/codex-adapter.js.map +1 -0
  17. package/src/adapters/codex-subagent-guard.d.ts +30 -0
  18. package/src/adapters/codex-subagent-guard.js +58 -0
  19. package/src/adapters/codex-subagent-guard.js.map +1 -0
  20. package/src/adapters/detect-ai.d.ts +36 -0
  21. package/src/adapters/detect-ai.js +47 -0
  22. package/src/adapters/detect-ai.js.map +1 -0
  23. package/src/adapters/guards-hook.d.ts +1 -0
  24. package/src/adapters/guards-hook.js +21 -6
  25. package/src/adapters/guards-hook.js.map +1 -1
  26. package/src/adapters/hook-app-fixtures.d.ts +68 -0
  27. package/src/adapters/hook-app-fixtures.js +175 -0
  28. package/src/adapters/hook-app-fixtures.js.map +1 -0
  29. package/src/adapters/hook-app.d.ts +61 -0
  30. package/src/adapters/hook-app.js +110 -0
  31. package/src/adapters/hook-app.js.map +1 -0
  32. package/src/adapters/hook-core.d.ts +15 -6
  33. package/src/adapters/hook-core.js +157 -139
  34. package/src/adapters/hook-core.js.map +1 -1
  35. package/src/adapters/hook-outcome.d.ts +46 -0
  36. package/src/adapters/hook-outcome.js +60 -0
  37. package/src/adapters/hook-outcome.js.map +1 -0
  38. package/src/adapters/hook-ports.d.ts +57 -0
  39. package/src/adapters/hook-ports.js +90 -0
  40. package/src/adapters/hook-ports.js.map +1 -0
  41. package/src/adapters/rules-hook.d.ts +1 -0
  42. package/src/adapters/rules-hook.js +19 -5
  43. package/src/adapters/rules-hook.js.map +1 -1
  44. package/src/core/agent-event.d.ts +65 -0
  45. package/src/core/agent-event.js +59 -0
  46. package/src/core/agent-event.js.map +1 -0
  47. package/src/core/apply-patch-parse.d.ts +36 -0
  48. package/src/core/apply-patch-parse.js +154 -0
  49. package/src/core/apply-patch-parse.js.map +1 -0
  50. package/src/core/delete-scoped-rules.d.ts +8 -0
  51. package/src/core/delete-scoped-rules.js +31 -0
  52. package/src/core/delete-scoped-rules.js.map +1 -0
  53. package/src/core/runner.js +2 -1
  54. package/src/core/runner.js.map +1 -1
  55. package/src/core/shell-read-parity.d.ts +22 -0
  56. package/src/core/shell-read-parity.js +145 -0
  57. package/src/core/shell-read-parity.js.map +1 -0
  58. package/src/core/types.d.ts +1 -1
  59. package/src/core/types.js.map +1 -1
  60. package/src/index.d.ts +2 -0
  61. package/src/index.js +11 -1
  62. package/src/index.js.map +1 -1
  63. package/src/adapters/claude-code-response.d.ts +0 -3
  64. package/src/adapters/claude-code-response.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"hook-core.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-core.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { run, runBash, runRead } from '../core/runner';\nimport { branchStateHangTimeoutFor } from '../core/main-sync-timeout';\nimport { logRejection, extractRuleNames } from '../core/rejection-log';\nimport { logGuardDecision, GuardDecision, branchForLog, invocationLog, MATRIX_L0_BLOCK, matrixL2Row } from '../core/decision-log';\nimport { triggerMainSyncRefresh } from '../core/main-sync-refresh';\nimport { CONFIG_FILENAME } from '../core/load-config';\nimport { RepoRootFinder, renderRuleFailForAi } from '@webpieces/rules-config';\nimport { NormalizedToolInput, NormalizedEdit, ToolKind, InformAiError, RuleFailError, HookMode, BlockedResult } from '../core/types';\nimport { toError } from '../core/to-error';\nimport { emitDeny, emitAllow } from './claude-code-response';\nimport { governingShimRoot, isAllowed, installedShimRulesVersion } from '../bin/shim';\nimport { shimStaleDenyReason } from '../bin/shim-deny-reason';\nimport { managedSurfaceDrift } from '../bin/hook-registration';\nimport { writeGuardMatrixDoc, guardMatrixPointer } from '../core/l0-matrix';\nimport { logStream, StreamIdentity } from '../core/log-stream';\nimport { L0_FAULT_SHIM_STALE, L0_FAULT_NONE } from '../core/l0-fault-codes';\n\n// Which category of rules this hook invocation runs. The hook is split into two independently\n// installable PreToolUse hooks; each runs ONE category (the runner filters by it), and both can\n// receive file AND bash payloads:\n// - 'rules' → code-style rules (file/edit scope). Bash payloads pass through (no code rules apply).\n// - 'guards' → hookGuards section: bash git/PR guards on Bash AND file guards (feature-branch-guard)\n// on Write/Edit, PLUS a log-and-allow audit of Read. Matcher is Write|Edit|MultiEdit|Bash|Read.\n// - 'all' → both categories, used by the openclaw plugin adapter (a single before_tool_call hook).\nexport type { HookMode };\n\nconst HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);\n\n// Read-only tools carry NO guard or code rule, but the guards hook owns the per-invocation audit log\n// (the `calls/` stream). When the guards matcher includes these (see setup.ts GUARDS_HOOK), a\n// log-and-allow fast path records every file the AI opens — so a human can later inspect whether it\n// read a project's design.json BEFORE editing the project. Never blocked. Scoped to Read for now;\n// widen (Grep/Glob/NotebookRead) later if desired.\nconst READ_ONLY_TOOLS = new Set(['Read']);\n\ninterface ClaudeCodePayload {\n tool_name: string;\n tool_input: ClaudeCodeToolInput;\n // Claude Code sends the session's current working directory (follows a persisted `cd`). Used to\n // scope guards to the git repo the AI is actually in — see runner git-repo-boundary governance.\n cwd?: string;\n // Present ONLY when the hook fires inside a SUBAGENT; absent = the coordinator. Used to NAME the log\n // writer file and nothing else — never to decide which tree a call acts on, which is measured from\n // the path (see core/version-sync.ts). A worktree-isolated agent was measured resuming on the primary\n // clone after its tree was reaped, so identity cannot stand in for location.\n session_id?: string;\n agent_id?: string;\n agent_type?: string;\n}\n\ninterface ClaudeCodeToolInput {\n file_path?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n edits?: ClaudeCodeEditEntry[];\n command?: string;\n}\n\ninterface ClaudeCodeEditEntry {\n old_string?: string;\n new_string?: string;\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve: (value: string) => void) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => { data += chunk; });\n process.stdin.on('end', () => resolve(data));\n process.stdin.on('error', () => resolve(''));\n if (process.stdin.isTTY) resolve('');\n });\n}\n\nfunction safeParse(raw: string): ClaudeCodePayload | null {\n if (!raw || raw.trim() === '') return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeCodePayload;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Malformed hook input from Claude Code stdin: ${error.message}`, { cause: error });\n }\n}\n\nfunction normalizeToolKind(toolName: string): ToolKind | null {\n if (HANDLED_FILE_TOOLS.has(toolName)) return toolName as ToolKind;\n return null;\n}\n\nfunction normalizeToolInput(toolKind: ToolKind, toolInput: ClaudeCodeToolInput): NormalizedToolInput | null {\n const filePath = toolInput.file_path;\n if (!filePath) return null;\n\n if (toolKind === 'Write') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit('', toolInput.content || ''),\n ]);\n }\n if (toolKind === 'Edit') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit(toolInput.old_string || '', toolInput.new_string || ''),\n ]);\n }\n if (toolKind === 'MultiEdit') {\n const raw = Array.isArray(toolInput.edits) ? toolInput.edits : [];\n const edits = raw.map((e: ClaudeCodeEditEntry) => new NormalizedEdit(e.old_string || '', e.new_string || ''));\n return new NormalizedToolInput(filePath, edits);\n }\n return null;\n}\n\n// The rule name for a block's audit line: the FIRST rule the report cites, or `fallback` when the\n// report opens with no `[rule]` header (a hand-written guard message). Comma-joined when a report\n// cites several, so `rule=` never silently drops one.\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction blockingRule(report: string, fallback: string): string {\n const names = extractRuleNames(report);\n return names.length > 0 ? names.join(',') : fallback;\n}\n\nfunction handleBash(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const command = payload.tool_input.command;\n if (!command || command.trim() === '') { emitAllow(); }\n const result = runBash(command, cwd, mode);\n if (!result) { emitAllow(); }\n // NO DECISION LINE HERE. This used to write a generic `bash-guard` line because a Bash deny once\n // had no audit trail at all — but every layer now records its own: L1 into `L1-location/` with its\n // row, L2's guards into `L2-decisions/` with their rule and cache, and emitDeny below stamps the\n // call-level outcome onto `calls/`. So this was the THIRD line for one block, and the worst of the\n // three: it re-resolved the root from `cwd` via RepoRootFinder, which is not necessarily the tree\n // the guard actually judged, so a `cd`-relocated command scattered one block across two different\n // `.webpieces` directories.\n //\n // Bash deny → pass 'Bash' so denyJson adds the ANSI-red systemMessage (the only field a Bash deny\n // shows the human; permissionDecisionReason is invisible on Bash). See claude-code-response.ts.\n emitDeny(result.report, 'Bash', blockingRule(result.report, 'bash-guard'), result.fault);\n}\n\n/**\n * The read-scoped guard pass. Returns normally to ALLOW; only calls emitDeny when the guard fires.\n *\n * Wrapped in its own catch that swallows into an allow. Every other path in this hook fails CLOSED,\n * and that is right for edits and shell commands — but a crash here would block the agent from\n * READING, which includes reading webpieces.config.json to turn the offending guard off. So this one\n * path deliberately inverts the policy: a broken read-guard degrades to a no-op, never to a wedge.\n */\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction handleRead(filePath: string, cwd: string, mode: HookMode): void {\n if (filePath === '') return;\n let result: BlockedResult | null = null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n result = runRead(filePath, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return; // fail OPEN — see the doc comment\n }\n if (!result) return;\n logRejection('Read', new NormalizedToolInput(filePath, []), result, cwd);\n emitDeny(result.report, 'Read', blockingRule(result.report, 'read-guard'), result.fault);\n}\n\nfunction handleFileTool(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const toolKind = normalizeToolKind(payload.tool_name);\n if (!toolKind) { emitAllow(); }\n\n const input = normalizeToolInput(toolKind, payload.tool_input);\n if (!input) { emitAllow(); }\n\n // Always allow edits to webpieces.config.json — it's the fix target when the config is broken.\n // This exits BEFORE run(), so feature-branch-guard never sees a config edit; record that so the\n // audit trail explains why a config edit on a bad branch was not blocked (see decision-log.ts).\n if (path.basename(input.filePath) === CONFIG_FILENAME) {\n if (mode !== 'rules') {\n // `.webpieces/` (the decision log + sync cache these two calls write) lives at the repo\n // root, not the AI's cwd — resolve it so a config edit from a subdir doesn't create a\n // stray `<subdir>/.webpieces` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(\n root,\n new GuardDecision('feature-branch-guard', toolKind, input.filePath, branchForLog(root), 'ALLOW_EXEMPT', 'config-bypass (feature-branch-guard skipped)', '-', L0_FAULT_NONE, matrixL2Row('config-bypass (feature-branch-guard skipped)')),\n );\n // The guard's own refresh trigger lives inside its check(), which we skip here — so warm\n // the cache directly, otherwise a session that only edits webpieces.config.json never\n // refreshes the sync status. Fire-and-forget; never blocks the edit.\n triggerMainSyncRefresh(root, branchStateHangTimeoutFor(cwd));\n }\n emitAllow();\n }\n\n const result = run(toolKind, input, cwd, mode);\n if (!result) { emitAllow(); }\n\n logRejection(toolKind, input, result, cwd);\n // File-tool deny → pass the Write/Edit/MultiEdit kind so denyJson omits systemMessage (the reason\n // already renders red natively for these tools). See claude-code-response.ts.\n emitDeny(result.report, toolKind, blockingRule(result.report, 'file-guard'), result.fault);\n}\n\n// What a stale committed shim lets through — now a thin adapter over the ONE L0 allowlist (isAllowed in\n// ../bin/shim), not a list of its own. A stale shim must NEVER trap the actions needed to recover: the\n// original \"block everything but the cures\" version also shadowed the always-allowed\n// webpieces.config.json edit (handleFileTool) and blocked reads, so a repo that ALSO needed its config\n// fixed would deadlock — blocked from editing the one file whose edit is normally always allowed, and\n// blocked from reading it to know how.\n//\n// It used to carry its OWN narrower list (isShimCureCommand: the three shim cures only), and that\n// narrowness was a defect, not a safety property: `pnpm install` and `git pull` — the two commands that\n// resolve the version disagreement underneath a stale shim — were denied. Consulting the shared\n// allowlist fixes that by construction.\n//\n// What is NOT a defect, and must not be \"fixed\": those cures rewrite the committed shim from the\n// INSTALLED binary's renderShim(), overwriting whatever was there. That is the invariant, not\n// collateral damage. The shim (D/X/K, in POSIX sh, pre-binary) and this binary (S/C/Y, in JS) are two\n// halves of ONE L0 and they exchange assumptions — the shim parses file_path and carries ALLOW-READ /\n// ALLOW-CONFIG entries this binary relies on. Pair a binary with a shim rendered by a DIFFERENT\n// release and L0 acquires holes that nothing reports. So the rule is absolute: the committed shim\n// equals renderShim() of the binary in node_modules, and a cure that forces that is the cure working.\n// See healShim's header, which states the same invariant from the other side.\n//\n// Corollary for anyone regenerating the shim in a webpieces PR: commit `templates/ai-hook.sh` (source,\n// locked to renderShim() by unit test) and leave `.claude/webpieces/ai-hook.sh` (generated artifact)\n// alone. In THIS repo the local source runs ahead of the pinned node_modules, so committing a shim\n// rendered from local source produces a commit whose shim and whose @webpieces pin come from different\n// releases — precisely the mismatch above. The artifact heals on the next upgrade; that is its job.\n//\n// - 'allow-cure' → a Bash cure on the allowlist: emitAllow directly, bypassing the git guards.\n// - 'pass' → a recovery action the normal flow already permits, so fall THROUGH and let it: ANY\n// Read (you must read to know how to fix — see handleRead, which itself fails open),\n// or an edit to webpieces.config.json (the always-allowed recovery target).\n// - 'deny' → all OTHER work: blocked until the committed shim matches renderShim() again.\nexport type ShimStaleDecision = 'allow-cure' | 'pass' | 'deny';\n// webpieces-disable no-function-outside-class -- pure decision helper beside the adapter's other module-scope functions; exported for direct unit testing.\nexport function shimStaleRecoveryDecision(toolName: string, command: string, filePath: string): ShimStaleDecision {\n const allowed = isAllowed(toolName, command, filePath);\n if (allowed === 'pass') return 'pass';\n if (allowed === 'allow') return 'allow-cure';\n return 'deny';\n}\n\n// MANAGED-HOOK-SURFACE self-guard, moved here from the rendered shim (2026-07-24) and widened from one\n// file to three (2026-08-07). The committed\n// .claude/webpieces/ai-hook.sh is webpieces-MANAGED and generated from renderShim(); if it no longer\n// matches, it was reverted / hand-edited / predates this binary, so its OWN fail-closed logic can't be\n// trusted. We are the CURRENT binary from node_modules — the trustworthy party — so WE decide here\n// instead of the (possibly stale) shim. It used to `cmp` itself inside the shim: a double-edged trap,\n// since the check lived in the very file it guarded and a fix could only ship by regenerating that\n// file. Now we fail closed on all real WORK while always leaving the recovery path open (see\n// shimStaleRecoveryDecision): the whole L0 allowlist, any Read, and editing webpieces.config.json. We deny +\n// tell the AI; we do NOT silently rewrite the file under it. 'rules' hook skips it (guards owns the\n// shim). Returns normally (pass / nothing to do) or exits via emitAllow/emitDeny.\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction enforceCommittedShim(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n // ONE root for the whole decision, resolved from the RUNNING MODULE (governingShimRoot), never from\n // `cwd`: the shim file we compare and the renderShim() we compare it TO must come from the same\n // install, or the check straddles two trees and can never converge (see governingShimRoot's header).\n // `cwd` still selects where the L0 matrix doc is dropped — that is a \"where does the AI read\" question,\n // not part of the judgement.\n const shimRoot = governingShimRoot();\n if (mode === 'rules') return;\n // WHICH of the three managed things moved — ai-hook.sh, the settings.json registration, or its\n // managed env entry (the Bash-cwd pin that keeps a guard's verdict independent of where an earlier\n // `cd` left the shell; see managed-env.ts). Nothing validated the registration before it joined this\n // fault, so a settings file left on a superseded form silently changed WHO GOVERNS, with no signal\n // anywhere — which is the whole reason the registration is a drift surface and not just an install\n // step.\n const drifted = managedSurfaceDrift(shimRoot);\n if (drifted.length === 0) return;\n const decision = shimStaleRecoveryDecision(payload.tool_name, payload.tool_input.command ?? '', payload.tool_input.file_path ?? '');\n if (decision === 'pass') return;\n if (decision === 'allow-cure') emitAllow();\n // Drop the L0 matrix doc where the AI can read it and point the deny at it — a Read is entry 1 of\n // the same allowlist, so the pointer is always followable. Best-effort: no doc → no pointer.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const docPath = writeGuardMatrixDoc(root);\n // WRITE THE AUDIT LINE HERE. This block happens BEFORE invocationLog.begin() — it has to, since a\n // stale shim invalidates everything downstream — so emitDeny's flush finds nothing pending and an\n // `S` storm left NO trace at all: the one fault most likely to block twenty consecutive tool calls\n // was the one fault the trail could not show. A decision line is the fix that costs no reordering.\n const target = payload.tool_input.command ?? payload.tool_input.file_path ?? '';\n logGuardDecision(\n root,\n new GuardDecision('committed-shim-stale', payload.tool_name, target, branchForLog(root), 'BLOCK_AI_CURE', 'L0 fault S (committed shim != renderShim)', '-', L0_FAULT_SHIM_STALE, MATRIX_L0_BLOCK),\n );\n // L0 fault S in GUARD_MATRIX.md's codebook — named as the blocking rule so the invocation line\n // says WHAT stopped the call, not merely that something did, and stamped as `fault=S` so the same\n // grep finds it here as in the sh half's `L0-shim/` stream.\n // A subagent is discriminated by `agent_id`, which Claude Code populates on stdin only off the main\n // loop (main falls back to the session id, so the field is absent). Its cure differs: the hooks\n // blocking it resolve through CLAUDE_PROJECT_DIR, which names the MAIN tree.\n const inSubagent = (payload.agent_id ?? '') !== '';\n emitDeny(shimStaleDenyReason(installedShimRulesVersion(), shimRoot ?? '', drifted, inSubagent) + guardMatrixPointer(docPath), payload.tool_name, 'committed-shim-stale', L0_FAULT_SHIM_STALE);\n}\n\n/**\n * Shared entry point for all three Claude Code PreToolUse adapters. `mode` selects which tool kinds\n * to validate; payloads outside the mode's scope pass through (emitAllow). Blocks by emitting a\n * PreToolUse `permissionDecision:\"deny\"` JSON on stdout (exit 0) — see claude-code-response.ts. Fails\n * CLOSED on any unexpected crash (emits a deny) so a broken hook never silently lets an edit through,\n * and the reason now surfaces in the Claude Code UI instead of being hidden on a stderr+exit-2 block.\n */\nexport async function runMain(mode: HookMode): Promise<void> {\n // Captured from the payload as soon as it parses so the fail-closed catch below can tell denyJson\n // which tool it is denying — a crash on a Bash call still gets the visible red systemMessage, a\n // crash on a file tool does not. Empty (before parse / malformed input) → treated as non-Bash.\n let toolName = '';\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = await readStdin();\n const payload = safeParse(raw);\n if (!payload) { emitAllow(); }\n toolName = payload.tool_name;\n\n // BEFORE enforceCommittedShim(), which can itself write a BLOCK line. See LogStream for why\n // all three of session/agent/hook are needed to keep concurrent writers off one file.\n logStream.identify(new StreamIdentity(payload.session_id ?? '', payload.agent_id ?? '', mode));\n\n // Prefer the payload cwd (the AI's actual working dir, follows a persisted `cd`) over\n // process.cwd(); they match today, but the payload is the authoritative signal and stays\n // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).\n const cwd = payload.cwd ?? process.cwd();\n\n // Committed-shim self-guard: blocks real work while the committed shim is stale, but keeps the\n // recovery path open (cures, reads, config edit). See enforceCommittedShim / shimStaleRecoveryDecision.\n enforceCommittedShim(payload, cwd, mode);\n\n // Read-only tools (Read): audit-log, warm the main-sync cache, then run the ONE read-scoped\n // guard (read-stale-guard) and allow. Runs BEFORE the general rule engine — no code-style rule\n // ever sees a Read, and the only way this path can deny is a stale `main`.\n // The audit trail still records every file the AI opened (see setup.ts).\n if (READ_ONLY_TOOLS.has(payload.tool_name)) {\n const readPath = payload.tool_input.file_path ?? '';\n if (mode !== 'rules') {\n invocationLog.begin(cwd, payload.tool_name, readPath);\n // Reads vastly outnumber edits, so refreshing here is what actually keeps the shared\n // main-sync cache warm for feature-branch-guard. Detached; never slows the read.\n triggerMainSyncRefresh(cwd, branchStateHangTimeoutFor(cwd));\n }\n handleRead(readPath, cwd, mode);\n emitAllow();\n }\n\n // Per-invocation guard log (the `calls/` stream): tool + command/file + live branch +\n // main-sync-status snapshot, on EVERY guards call, for later cleanup automation. Best-effort;\n // never blocks the call. (The committed shim is no longer silently healed here — a mismatch is\n // reported by the self-guard above, not rewritten out from under the AI.)\n if (mode !== 'rules') {\n const target = payload.tool_name === 'Bash' ? (payload.tool_input.command ?? '') : (payload.tool_input.file_path ?? '');\n invocationLog.begin(cwd, payload.tool_name, target);\n }\n\n if (payload.tool_name === 'Bash') {\n // No code-style rule is bash-scoped, so the rules hook ignores Bash.\n if (mode === 'rules') { emitAllow(); }\n handleBash(payload, cwd, mode);\n return;\n }\n\n // File payloads run in 'rules' (code-style), 'guards' (file-scoped guards like\n // feature-branch-guard), and 'all'. The runner filters to the right category.\n handleFileTool(payload, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n denyForCrash(error, toolName);\n }\n}\n\n/**\n * The fail-closed boundary for anything that escaped the hook body. An escaped RuleFailError (a rule\n * that threw past the runner's per-rule catch) or an InformAiError (bad config/stdin) both carry an\n * AI-readable message; anything else is an unexpected bug. All three DENY and surface their reason,\n * because a hook that crashed established nothing and must never be read as an allow.\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope hook entry points in this adapter; a lone class for one terminal boundary would break the file's shape\nfunction denyForCrash(error: Error, toolName: string): never {\n if (error instanceof RuleFailError) {\n emitDeny(renderRuleFailForAi(error), toolName, 'rule-crash');\n }\n if (error instanceof InformAiError) {\n emitDeny(error.message, toolName, 'bad-config-or-stdin');\n }\n emitDeny(`[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}`, toolName, 'hook-crash');\n}\n"]}
1
+ {"version":3,"file":"hook-core.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-core.ts"],"names":[],"mappings":";;AAsNA,8DAKC;AA6ED,kCAyDC;;AAjWD,mDAA6B;AAE7B,2CAAuD;AACvD,iEAAsE;AACtE,yDAAuE;AACvE,uDAAkI;AAClI,iEAAmE;AACnE,qDAAsD;AACtD,0DAA8E;AAC9E,yCAA2G;AAE3G,+CAA2C;AAC3C,qDAAoE;AACpE,iDAA6D;AAC7D,mDAAmE;AACnE,qDAAiD;AACjD,iEAA2F;AAC3F,sCAAsF;AACtF,gEAA+D;AAC/D,8DAA8D;AAC9D,iDAA4E;AAC5E,mDAA+D;AAC/D,2DAA4E;AAW5E,MAAM,QAAQ,GAAG,IAAI,8BAAa,EAAE,CAAC;AACrC,MAAM,cAAc,GAAG,IAAI,mDAA4B,EAAE,CAAC;AAE1D,kGAAkG;AAClG,kGAAkG;AAClG,sDAAsD;AACtD,0JAA0J;AAC1J,SAAS,YAAY,CAAC,MAAc,EAAE,QAAgB;IAClD,MAAM,KAAK,GAAG,IAAA,gCAAgB,EAAC,MAAM,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AACzD,CAAC;AAED,0JAA0J;AAC1J,SAAS,UAAU,CAAC,KAAqB,EAAE,GAAW,EAAE,IAAc;IAClE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;IAC9D,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAAC,IAAA,0BAAS,GAAE,CAAC;IAAC,CAAC;IAE3C,8FAA8F;IAC9F,kGAAkG;IAClG,kGAAkG;IAClG,kGAAkG;IAClG,gBAAgB;IAChB,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QACjC,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,gBAAO,EAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;QAAC,IAAA,0BAAS,GAAE,CAAC;IAAC,CAAC;IAC7B,iGAAiG;IACjG,mGAAmG;IACnG,iGAAiG;IACjG,mGAAmG;IACnG,kGAAkG;IAClG,kGAAkG;IAClG,4BAA4B;IAC5B,EAAE;IACF,gGAAgG;IAChG,yFAAyF;IACzF,qBAAqB;IACrB,IAAA,yBAAQ,EAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;;;GAOG;AACH,0JAA0J;AAC1J,SAAS,UAAU,CAAC,KAAqB,EAAE,QAAgB,EAAE,GAAW,EAAE,IAAc;IACpF,IAAI,QAAQ,KAAK,EAAE;QAAE,OAAO;IAC5B,IAAI,MAAM,GAAyB,IAAI,CAAC;IACxC,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,GAAG,IAAA,gBAAO,EAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,KAAK,KAAK,CAAC;QACX,OAAO,CAAC,kCAAkC;IAC9C,CAAC;IACD,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,IAAA,4BAAY,EAAC,MAAM,EAAE,IAAI,2BAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACzE,IAAA,yBAAQ,EAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;;;GAOG;AACH,0JAA0J;AAC1J,SAAS,kBAAkB,CAAC,KAAqB,EAAE,GAAW,EAAE,IAAc;IAC1E,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO;IAClC,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACnB,4BAAa,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACtD,qFAAqF;QACrF,iFAAiF;QACjF,IAAA,0CAAsB,EAAC,GAAG,EAAE,IAAA,6CAAyB,EAAC,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACvC,IAAA,0BAAS,GAAE,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,sJAAsJ;AACtJ,SAAS,cAAc,CAAC,KAAqB,EAAE,GAAW,EAAE,IAAc;IACtE,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAAC,IAAA,0BAAS,GAAE,CAAC;IAAC,CAAC;IAE9C,gGAAgG;IAChG,mFAAmF;IACnF,MAAM,aAAa,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7F,IAAI,aAAa,EAAE,CAAC;QAChB,IAAA,yBAAQ,EAAC,KAAK,EAAE,aAAa,CAAC,MAAM,EAAE,0CAAmB,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;IACpF,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC7B,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1C,CAAC;IACD,IAAA,0BAAS,GAAE,CAAC;AAChB,CAAC;AAED,0JAA0J;AAC1J,SAAS,aAAa,CAAC,KAAqB,EAAE,IAAmB,EAAE,GAAW,EAAE,IAAc;IAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IAEzB,+FAA+F;IAC/F,kGAAkG;IAClG,gGAAgG;IAChG,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,6BAAe,EAAE,CAAC;QACpD,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACnB,wFAAwF;YACxF,sFAAsF;YACtF,oCAAoC;YACpC,MAAM,IAAI,GAAG,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YACvD,IAAA,+BAAgB,EACZ,IAAI,EACJ,IAAI,4BAAa,CAAC,sBAAsB,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAA,2BAAY,EAAC,IAAI,CAAC,EAAE,cAAc,EAAE,8CAA8C,EAAE,GAAG,EAAE,8BAAa,EAAE,IAAA,0BAAW,EAAC,8CAA8C,CAAC,CAAC,CAChP,CAAC;YACF,yFAAyF;YACzF,sFAAsF;YACtF,qEAAqE;YACrE,IAAA,0CAAsB,EAAC,IAAI,EAAE,IAAA,6CAAyB,EAAC,GAAG,CAAC,CAAC,CAAC;QACjE,CAAC;QACD,OAAO;IACX,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,YAAG,EAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,IAAA,4BAAY,EAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IAChD,2FAA2F;IAC3F,wEAAwE;IACxE,IAAA,yBAAQ,EAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5F,CAAC;AAmCD,2JAA2J;AAC3J,SAAgB,yBAAyB,CAAC,QAAgB,EAAE,OAAe,EAAE,QAAgB;IACzF,MAAM,OAAO,GAAG,IAAA,gBAAS,EAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IACvD,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,MAAM,CAAC;IACtC,IAAI,OAAO,KAAK,OAAO;QAAE,OAAO,YAAY,CAAC;IAC7C,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,uGAAuG;AACvG,4CAA4C;AAC5C,qGAAqG;AACrG,uGAAuG;AACvG,mGAAmG;AACnG,sGAAsG;AACtG,mGAAmG;AACnG,6FAA6F;AAC7F,6GAA6G;AAC7G,oGAAoG;AACpG,kFAAkF;AAClF,EAAE;AACF,wGAAwG;AACxG,sGAAsG;AACtG,sGAAsG;AACtG,oGAAoG;AACpG,0JAA0J;AAC1J,SAAS,oBAAoB,CAAC,OAAqB,EAAE,KAAqB,EAAE,GAAW,EAAE,IAAc;IACnG,oGAAoG;IACpG,gGAAgG;IAChG,qGAAqG;IACrG,wGAAwG;IACxG,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,IAAA,wBAAiB,GAAE,CAAC;IACrC,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO;IAC7B,+FAA+F;IAC/F,mGAAmG;IACnG,qGAAqG;IACrG,mGAAmG;IACnG,mGAAmG;IACnG,QAAQ;IACR,MAAM,OAAO,GAAG,IAAA,uCAAmB,EAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjC,MAAM,QAAQ,GAAG,yBAAyB,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,EAAE,EAAE,OAAO,CAAC,UAAU,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IACpI,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO;IAChC,IAAI,QAAQ,KAAK,YAAY;QAAE,IAAA,0BAAS,GAAE,CAAC;IAC3C,kGAAkG;IAClG,6FAA6F;IAC7F,MAAM,IAAI,GAAG,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,IAAA,+BAAmB,EAAC,IAAI,CAAC,CAAC;IAC1C,kGAAkG;IAClG,kGAAkG;IAClG,mGAAmG;IACnG,mGAAmG;IACnG,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,IAAI,EAAE,CAAC;IAChF,IAAA,+BAAgB,EACZ,IAAI,EACJ,IAAI,4BAAa,CAAC,sBAAsB,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAA,2BAAY,EAAC,IAAI,CAAC,EAAE,eAAe,EAAE,2CAA2C,EAAE,GAAG,EAAE,oCAAmB,EAAE,8BAAe,CAAC,CACpM,CAAC;IACF,+FAA+F;IAC/F,kGAAkG;IAClG,4DAA4D;IAC5D,iGAAiG;IACjG,+FAA+F;IAC/F,6EAA6E;IAC7E,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,KAAK,EAAE,CAAC;IACxC,IAAA,yBAAQ,EAAC,KAAK,EAAE,IAAA,sCAAmB,EAAC,IAAA,gCAAyB,GAAE,EAAE,QAAQ,IAAI,EAAE,EAAE,OAAO,EAAE,UAAU,CAAC,GAAG,IAAA,8BAAkB,EAAC,OAAO,CAAC,EAAE,sBAAsB,EAAE,oCAAmB,CAAC,CAAC;AACtL,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,oPAAoP;AACpP,SAAgB,WAAW,CAAC,GAAW,EAAE,IAAc;IACnD,iGAAiG;IACjG,kGAAkG;IAClG,8FAA8F;IAC9F,IAAI,KAAK,GAA0B,IAAI,CAAC;IACxC,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,kCAAkB,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,EAAE,CAAC;YAAC,IAAA,0BAAS,GAAE,CAAC;QAAC,CAAC;QAC9B,4FAA4F;QAC5F,6FAA6F;QAC7F,4FAA4F;QAC5F,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAEnC,4FAA4F;QAC5F,sFAAsF;QACtF,sBAAS,CAAC,QAAQ,CAAC,IAAI,2BAAc,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAE7E,sFAAsF;QACtF,yFAAyF;QACzF,uFAAuF;QACvF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAEzC,+FAA+F;QAC/F,wGAAwG;QACxG,oBAAoB,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QAEhD,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAEvC,6EAA6E;QAC7E,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QAErC,sFAAsF;QACtF,8FAA8F;QAC9F,+FAA+F;QAC/F,0EAA0E;QAC1E,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACnB,4BAAa,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,WAAW,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAClE,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACxB,qEAAqE;YACrE,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBAAC,IAAA,0BAAS,GAAE,CAAC;YAAC,CAAC;YACtC,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACjC,CAAC;QAED,+EAA+E;QAC/E,8EAA8E;QAC9E,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,+FAA+F;QAC/F,4FAA4F;QAC5F,oFAAoF;QACpF,IAAI,KAAK,YAAY,6BAAc;YAAE,OAAO,KAAK,CAAC,OAAO,CAAC;QAC1D,OAAO,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACtC,CAAC;AACL,CAAC;AAED,wGAAwG;AACxG,wGAAwG;AACxG,6CAA6C;AAC7C,+GAA+G;AAC/G,SAAS,SAAS,CAAC,KAAqB;IACpC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;IAChF,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;AACvE,CAAC;AAED;;;;;;;;;GASG;AACH,oLAAoL;AACpL,SAAS,YAAY,CAAC,KAAY,EAAE,KAA4B;IAC5D,IAAI,KAAK,YAAY,qBAAa,EAAE,CAAC;QACjC,OAAO,IAAA,4BAAW,EAAC,KAAK,EAAE,IAAA,kCAAmB,EAAC,KAAK,CAAC,EAAE,YAAY,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,KAAK,YAAY,qBAAa,EAAE,CAAC;QACjC,OAAO,IAAA,4BAAW,EAAC,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,IAAA,4BAAW,EAAC,KAAK,EAAE,0DAA0D,KAAK,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACvH,CAAC","sourcesContent":["import * as path from 'path';\n\nimport { run, runBash, runRead } from '../core/runner';\nimport { branchStateHangTimeoutFor } from '../core/main-sync-timeout';\nimport { logRejection, extractRuleNames } from '../core/rejection-log';\nimport { logGuardDecision, GuardDecision, branchForLog, invocationLog, MATRIX_L0_BLOCK, matrixL2Row } from '../core/decision-log';\nimport { triggerMainSyncRefresh } from '../core/main-sync-refresh';\nimport { CONFIG_FILENAME } from '../core/load-config';\nimport { RepoRootFinder, renderRuleFailForAi } from '@webpieces/rules-config';\nimport { NormalizedToolInput, InformAiError, RuleFailError, HookMode, BlockedResult } from '../core/types';\nimport { AgentHookEvent, FileOperation } from '../core/agent-event';\nimport { toError } from '../core/to-error';\nimport { emitDeny, emitAllow, denyOutcome } from './agent-response';\nimport { HookOutcome, HookTerminated } from './hook-outcome';\nimport { AgentPayload, AgentPayloadParser } from './agent-payload';\nimport { AgentAdapters } from './agent-adapters';\nimport { CodexSubagentSharedTreeGuard, CODEX_SUBAGENT_RULE } from './codex-subagent-guard';\nimport { governingShimRoot, isAllowed, installedShimRulesVersion } from '../bin/shim';\nimport { managedSurfaceDrift } from '../bin/hook-registration';\nimport { shimStaleDenyReason } from '../bin/shim-deny-reason';\nimport { writeGuardMatrixDoc, guardMatrixPointer } from '../core/l0-matrix';\nimport { logStream, StreamIdentity } from '../core/log-stream';\nimport { L0_FAULT_SHIM_STALE, L0_FAULT_NONE } from '../core/l0-fault-codes';\n\n// Which category of rules this hook invocation runs. The hook is split into two independently\n// installable PreToolUse hooks; each runs ONE category (the runner filters by it), and both can\n// receive file AND bash payloads:\n// - 'rules' → code-style rules (file/edit scope). Bash payloads pass through (no code rules apply).\n// - 'guards' → hookGuards section: bash git/PR guards on Bash AND file guards (feature-branch-guard)\n// on Write/Edit, PLUS a log-and-allow audit of Read. Matcher is Write|Edit|MultiEdit|Bash|Read.\n// - 'all' → both categories, used by the openclaw plugin adapter (a single before_tool_call hook).\nexport type { HookMode };\n\nconst ADAPTERS = new AgentAdapters();\nconst SUBAGENT_GUARD = new CodexSubagentSharedTreeGuard();\n\n// The rule name for a block's audit line: the FIRST rule the report cites, or `fallback` when the\n// report opens with no `[rule]` header (a hand-written guard message). Comma-joined when a report\n// cites several, so `rule=` never silently drops one.\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction blockingRule(report: string, fallback: string): string {\n const names = extractRuleNames(report);\n return names.length > 0 ? names.join(',') : fallback;\n}\n\n// webpieces-disable no-function-outside-class -- sibling of handleRead()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction handleBash(event: AgentHookEvent, cwd: string, mode: HookMode): never {\n const command = event.bash === null ? '' : event.bash.command;\n if (command.trim() === '') { emitAllow(); }\n\n // READ PARITY, and it can only ever be reached from a Codex event: the adapter leaves `reads`\n // empty for Claude Code, which has a real `Read` tool and its own fast path. A Codex read arrives\n // as `Bash` running a pager, so without this the read guard and the `calls/` audit trail see none\n // of them. The command is STILL run through the bash guards below — this adds a verdict, it never\n // replaces one.\n for (const readPath of event.reads) {\n handleRead(event, readPath, cwd, mode);\n }\n\n const result = runBash(command, cwd, mode);\n if (!result) { emitAllow(); }\n // NO DECISION LINE HERE. This used to write a generic `bash-guard` line because a Bash deny once\n // had no audit trail at all — but every layer now records its own: L1 into `L1-location/` with its\n // row, L2's guards into `L2-decisions/` with their rule and cache, and emitDeny below stamps the\n // call-level outcome onto `calls/`. So this was the THIRD line for one block, and the worst of the\n // three: it re-resolved the root from `cwd` via RepoRootFinder, which is not necessarily the tree\n // the guard actually judged, so a `cd`-relocated command scattered one block across two different\n // `.webpieces` directories.\n //\n // Bash deny → the event's kind is 'Bash', so denyJson adds the ANSI-red systemMessage (the only\n // field a Bash deny shows the human; permissionDecisionReason is invisible on Bash). See\n // agent-response.ts.\n emitDeny(event, result.report, blockingRule(result.report, 'bash-guard'), result.fault);\n}\n\n/**\n * The read-scoped guard pass. Returns normally to ALLOW; only calls emitDeny when the guard fires.\n *\n * Wrapped in its own catch that swallows into an allow. Every other path in this hook fails CLOSED,\n * and that is right for edits and shell commands — but a crash here would block the agent from\n * READING, which includes reading webpieces.config.json to turn the offending guard off. So this one\n * path deliberately inverts the policy: a broken read-guard degrades to a no-op, never to a wedge.\n */\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction handleRead(event: AgentHookEvent, filePath: string, cwd: string, mode: HookMode): void {\n if (filePath === '') return;\n let result: BlockedResult | null = null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n result = runRead(filePath, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return; // fail OPEN — see the doc comment\n }\n if (!result) return;\n logRejection('Read', new NormalizedToolInput(filePath, []), result, cwd);\n emitDeny(event, result.report, blockingRule(result.report, 'read-guard'), result.fault);\n}\n\n/**\n * Read-only tools (Read): audit-log, warm the main-sync cache, then run the ONE read-scoped guard\n * (read-stale-guard) and allow. Runs BEFORE the general rule engine — no code-style rule ever sees a\n * Read, and the only way this path can deny is a stale `main`. The audit trail still records every\n * file the AI opened (see setup.ts).\n *\n * Returns normally ONLY when the event is not a Read; otherwise it ends the invocation.\n */\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction handleReadFastPath(event: AgentHookEvent, cwd: string, mode: HookMode): void {\n if (event.kind !== 'Read') return;\n const readPath = event.reads.length > 0 ? event.reads[0] : '';\n if (mode !== 'rules') {\n invocationLog.begin(cwd, event.rawToolName, readPath);\n // Reads vastly outnumber edits, so refreshing here is what actually keeps the shared\n // main-sync cache warm for feature-branch-guard. Detached; never slows the read.\n triggerMainSyncRefresh(cwd, branchStateHangTimeoutFor(cwd));\n }\n handleRead(event, readPath, cwd, mode);\n emitAllow();\n}\n\n/**\n * The file/edit pipeline, run once per file the call touches.\n *\n * `event.files` is a LIST because ONE Codex `apply_patch` carries many files with mixed operations.\n * A Claude Code event always has exactly one entry, so the loop runs once and the behaviour is the\n * single-file behaviour it has always had.\n */\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleRead() in this module; the adapter is module-scope functions by design\nfunction handleFileTool(event: AgentHookEvent, cwd: string, mode: HookMode): never {\n if (event.files.length === 0) { emitAllow(); }\n\n // A Codex SUBAGENT writing into the tree it shares with its coordinator. Returns null for every\n // Claude Code event — that harness can hand a subagent its own worktree, and does.\n const subagentBlock = SUBAGENT_GUARD.check(event, new RepoRootFinder().resolveRepoRoot(cwd));\n if (subagentBlock) {\n emitDeny(event, subagentBlock.report, CODEX_SUBAGENT_RULE, subagentBlock.fault);\n }\n\n for (const file of event.files) {\n handleOneFile(event, file, cwd, mode);\n }\n emitAllow();\n}\n\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction handleOneFile(event: AgentHookEvent, file: FileOperation, cwd: string, mode: HookMode): void {\n const input = file.input;\n\n // Always allow edits to webpieces.config.json — it's the fix target when the config is broken.\n // This returns BEFORE run(), so feature-branch-guard never sees a config edit; record that so the\n // audit trail explains why a config edit on a bad branch was not blocked (see decision-log.ts).\n if (path.basename(input.filePath) === CONFIG_FILENAME) {\n if (mode !== 'rules') {\n // `.webpieces/` (the decision log + sync cache these two calls write) lives at the repo\n // root, not the AI's cwd — resolve it so a config edit from a subdir doesn't create a\n // stray `<subdir>/.webpieces` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(\n root,\n new GuardDecision('feature-branch-guard', file.toolKind, input.filePath, branchForLog(root), 'ALLOW_EXEMPT', 'config-bypass (feature-branch-guard skipped)', '-', L0_FAULT_NONE, matrixL2Row('config-bypass (feature-branch-guard skipped)')),\n );\n // The guard's own refresh trigger lives inside its check(), which we skip here — so warm\n // the cache directly, otherwise a session that only edits webpieces.config.json never\n // refreshes the sync status. Fire-and-forget; never blocks the edit.\n triggerMainSyncRefresh(root, branchStateHangTimeoutFor(cwd));\n }\n return;\n }\n\n const result = run(file.toolKind, input, cwd, mode);\n if (!result) return;\n\n logRejection(file.toolKind, input, result, cwd);\n // File-tool deny → the event's kind is 'File', so denyJson omits systemMessage (the reason\n // already renders red natively for these tools). See agent-response.ts.\n emitDeny(event, result.report, blockingRule(result.report, 'file-guard'), result.fault);\n}\n\n// What a stale committed shim lets through — now a thin adapter over the ONE L0 allowlist (isAllowed in\n// ../bin/shim), not a list of its own. A stale shim must NEVER trap the actions needed to recover: the\n// original \"block everything but the cures\" version also shadowed the always-allowed\n// webpieces.config.json edit (handleFileTool) and blocked reads, so a repo that ALSO needed its config\n// fixed would deadlock — blocked from editing the one file whose edit is normally always allowed, and\n// blocked from reading it to know how.\n//\n// It used to carry its OWN narrower list (isShimCureCommand: the three shim cures only), and that\n// narrowness was a defect, not a safety property: `pnpm install` and `git pull` — the two commands that\n// resolve the version disagreement underneath a stale shim — were denied. Consulting the shared\n// allowlist fixes that by construction.\n//\n// What is NOT a defect, and must not be \"fixed\": those cures rewrite the committed shim from the\n// INSTALLED binary's renderShim(), overwriting whatever was there. That is the invariant, not\n// collateral damage. The shim (D/X/K, in POSIX sh, pre-binary) and this binary (S/C/Y, in JS) are two\n// halves of ONE L0 and they exchange assumptions — the shim parses file_path and carries ALLOW-READ /\n// ALLOW-CONFIG entries this binary relies on. Pair a binary with a shim rendered by a DIFFERENT\n// release and L0 acquires holes that nothing reports. So the rule is absolute: the committed shim\n// equals renderShim() of the binary in node_modules, and a cure that forces that is the cure working.\n// See healShim's header, which states the same invariant from the other side.\n//\n// Corollary for anyone regenerating the shim in a webpieces PR: commit `templates/ai-hook.sh` (source,\n// locked to renderShim() by unit test) and leave `.claude/webpieces/ai-hook.sh` (generated artifact)\n// alone. In THIS repo the local source runs ahead of the pinned node_modules, so committing a shim\n// rendered from local source produces a commit whose shim and whose @webpieces pin come from different\n// releases — precisely the mismatch above. The artifact heals on the next upgrade; that is its job.\n//\n// - 'allow-cure' → a Bash cure on the allowlist: emitAllow directly, bypassing the git guards.\n// - 'pass' → a recovery action the normal flow already permits, so fall THROUGH and let it: ANY\n// Read (you must read to know how to fix — see handleRead, which itself fails open),\n// or an edit to webpieces.config.json (the always-allowed recovery target).\n// - 'deny' → all OTHER work: blocked until the committed shim matches renderShim() again.\nexport type ShimStaleDecision = 'allow-cure' | 'pass' | 'deny';\n// webpieces-disable no-function-outside-class -- pure decision helper beside the adapter's other module-scope functions; exported for direct unit testing.\nexport function shimStaleRecoveryDecision(toolName: string, command: string, filePath: string): ShimStaleDecision {\n const allowed = isAllowed(toolName, command, filePath);\n if (allowed === 'pass') return 'pass';\n if (allowed === 'allow') return 'allow-cure';\n return 'deny';\n}\n\n// MANAGED-HOOK-SURFACE self-guard, moved here from the rendered shim (2026-07-24) and widened from one\n// file to three (2026-08-07). The committed\n// .claude/webpieces/ai-hook.sh is webpieces-MANAGED and generated from renderShim(); if it no longer\n// matches, it was reverted / hand-edited / predates this binary, so its OWN fail-closed logic can't be\n// trusted. We are the CURRENT binary from node_modules — the trustworthy party — so WE decide here\n// instead of the (possibly stale) shim. It used to `cmp` itself inside the shim: a double-edged trap,\n// since the check lived in the very file it guarded and a fix could only ship by regenerating that\n// file. Now we fail closed on all real WORK while always leaving the recovery path open (see\n// shimStaleRecoveryDecision): the whole L0 allowlist, any Read, and editing webpieces.config.json. We deny +\n// tell the AI; we do NOT silently rewrite the file under it. 'rules' hook skips it (guards owns the\n// shim). Returns normally (pass / nothing to do) or exits via emitAllow/emitDeny.\n//\n// It asks the allowlist about the RAW WIRE FIELDS, not about the normalized event, and that ordering is\n// deliberate: L0 has to hold on a tree too broken to trust anything above it, including the adapters.\n// The raw fields are the same key names in both harnesses (measured), so one reading serves both, and\n// the answer cannot change because a normalizer changed. `event` is here only to decorate the deny.\n// webpieces-disable no-function-outside-class -- sibling of handleBash()/handleFileTool() in this module; the adapter is module-scope functions by design\nfunction enforceCommittedShim(payload: AgentPayload, event: AgentHookEvent, cwd: string, mode: HookMode): void {\n // ONE root for the whole decision, resolved from the RUNNING MODULE (governingShimRoot), never from\n // `cwd`: the shim file we compare and the renderShim() we compare it TO must come from the same\n // install, or the check straddles two trees and can never converge (see governingShimRoot's header).\n // `cwd` still selects where the L0 matrix doc is dropped — that is a \"where does the AI read\" question,\n // not part of the judgement.\n const shimRoot = governingShimRoot();\n if (mode === 'rules') return;\n // WHICH of the three managed things moved — ai-hook.sh, the settings.json registration, or its\n // managed env entry (the Bash-cwd pin that keeps a guard's verdict independent of where an earlier\n // `cd` left the shell; see managed-env.ts). Nothing validated the registration before it joined this\n // fault, so a settings file left on a superseded form silently changed WHO GOVERNS, with no signal\n // anywhere — which is the whole reason the registration is a drift surface and not just an install\n // step.\n const drifted = managedSurfaceDrift(shimRoot);\n if (drifted.length === 0) return;\n const decision = shimStaleRecoveryDecision(payload.tool_name, payload.tool_input.command ?? '', payload.tool_input.file_path ?? '');\n if (decision === 'pass') return;\n if (decision === 'allow-cure') emitAllow();\n // Drop the L0 matrix doc where the AI can read it and point the deny at it — a Read is entry 1 of\n // the same allowlist, so the pointer is always followable. Best-effort: no doc → no pointer.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const docPath = writeGuardMatrixDoc(root);\n // WRITE THE AUDIT LINE HERE. This block happens BEFORE invocationLog.begin() — it has to, since a\n // stale shim invalidates everything downstream — so emitDeny's flush finds nothing pending and an\n // `S` storm left NO trace at all: the one fault most likely to block twenty consecutive tool calls\n // was the one fault the trail could not show. A decision line is the fix that costs no reordering.\n const target = payload.tool_input.command ?? payload.tool_input.file_path ?? '';\n logGuardDecision(\n root,\n new GuardDecision('committed-shim-stale', payload.tool_name, target, branchForLog(root), 'BLOCK_AI_CURE', 'L0 fault S (committed shim != renderShim)', '-', L0_FAULT_SHIM_STALE, MATRIX_L0_BLOCK),\n );\n // L0 fault S in GUARD_MATRIX.md's codebook — named as the blocking rule so the invocation line\n // says WHAT stopped the call, not merely that something did, and stamped as `fault=S` so the same\n // grep finds it here as in the sh half's `L0-shim/` stream.\n // A subagent is discriminated by `agent_id`, which BOTH harnesses populate on stdin only off the\n // main loop (main falls back to the session id / leaves it empty). Its cure differs: the hooks\n // blocking it resolve through CLAUDE_PROJECT_DIR, which names the MAIN tree.\n const inSubagent = event.agentId !== '';\n emitDeny(event, shimStaleDenyReason(installedShimRulesVersion(), shimRoot ?? '', drifted, inSubagent) + guardMatrixPointer(docPath), 'committed-shim-stale', L0_FAULT_SHIM_STALE);\n}\n\n/**\n * THE PIPELINE, from raw stdin bytes to the decision — the whole of `parse -> adapter -> runner ->\n * emit`, as ONE function returning ONE value.\n *\n * `mode` selects which tool kinds to validate; payloads outside the mode's scope pass through\n * (emitAllow). A block is a PreToolUse `permissionDecision:\"deny\"` JSON on stdout with exit 0 — see\n * agent-response.ts. Fails CLOSED on any unexpected crash (returns a deny) so a broken hook never\n * silently lets an edit through, and the reason surfaces in the agent's UI instead of being hidden on\n * a stderr+exit-2 block.\n *\n * It reads NO stdin, writes NO stdout and calls NO exit: those three couplings are ports owned by\n * HookApp (see hook-ports.ts), which is what makes the composed pipeline drivable from a test. This\n * function is what `runMain` was; it is not a second spelling of it — `runMain` is deleted, and\n * `HookApp.run()` is the only entry point.\n */\n// webpieces-disable no-function-outside-class -- the module-scope hook body itself, sibling of handleBash()/handleFileTool(); the adapter is module-scope functions by design and must stay callable from a tree too broken to build a DI container\nexport function runPipeline(raw: string, mode: HookMode): HookOutcome {\n // Captured as soon as the ENVELOPE parses so the fail-closed catch below can tell denyJson which\n // kind of call it is denying — a crash on a Bash call still gets the visible red systemMessage, a\n // crash on a file tool does not. Null (before parse / malformed input) → treated as non-Bash.\n let event: AgentHookEvent | null = null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const payload = new AgentPayloadParser().parse(raw);\n if (!payload) { emitAllow(); }\n // The envelope shape first: it reads only `tool_name` and the identity fields, so it cannot\n // fail, and it is what the crash path needs. The full normalization below reads `tool_input`\n // and CAN fail (a malformed Codex patch envelope denies rather than being half-understood).\n event = ADAPTERS.envelope(payload);\n\n // BEFORE enforceCommittedShim(), which can itself write a BLOCK line. See LogStream for why\n // all three of session/agent/hook are needed to keep concurrent writers off one file.\n logStream.identify(new StreamIdentity(event.sessionId, event.agentId, mode));\n\n // Prefer the payload cwd (the AI's actual working dir, follows a persisted `cd`) over\n // process.cwd(); they match today, but the payload is the authoritative signal and stays\n // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).\n const cwd = payload.cwd ?? process.cwd();\n\n // Committed-shim self-guard: blocks real work while the committed shim is stale, but keeps the\n // recovery path open (cures, reads, config edit). See enforceCommittedShim / shimStaleRecoveryDecision.\n enforceCommittedShim(payload, event, cwd, mode);\n\n event = ADAPTERS.toEvent(payload, cwd);\n\n // Read-only tools: their own fast path, which never returns when it applies.\n handleReadFastPath(event, cwd, mode);\n\n // Per-invocation guard log (the `calls/` stream): tool + command/file + live branch +\n // main-sync-status snapshot, on EVERY guards call, for later cleanup automation. Best-effort;\n // never blocks the call. (The committed shim is no longer silently healed here — a mismatch is\n // reported by the self-guard above, not rewritten out from under the AI.)\n if (mode !== 'rules') {\n invocationLog.begin(cwd, event.rawToolName, logTarget(event));\n }\n\n if (event.kind === 'Bash') {\n // No code-style rule is bash-scoped, so the rules hook ignores Bash.\n if (mode === 'rules') { emitAllow(); }\n handleBash(event, cwd, mode);\n }\n\n // File payloads run in 'rules' (code-style), 'guards' (file-scoped guards like\n // feature-branch-guard), and 'all'. The runner filters to the right category.\n handleFileTool(event, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n // The pipeline's own terminal control flow, not a failure: emitAllow/emitDeny threw the answer\n // out to here from wherever they were called. Treating it as a crash would turn every allow\n // into a deny, so this branch comes FIRST and returns the carried outcome verbatim.\n if (error instanceof HookTerminated) return error.outcome;\n return denyForCrash(error, event);\n }\n}\n\n// What the `calls/` audit line names as the call's target: the command for a shell call, else the first\n// file it touches. A Codex `apply_patch` touching several files names the first — the rejection log and\n// the decision log carry the rest, per file.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope hook entry points in this adapter\nfunction logTarget(event: AgentHookEvent): string {\n if (event.kind === 'Bash') return event.bash === null ? '' : event.bash.command;\n return event.files.length > 0 ? event.files[0].input.filePath : '';\n}\n\n/**\n * The fail-closed boundary for anything that escaped the hook body. An escaped RuleFailError (a rule\n * that threw past the runner's per-rule catch) or an InformAiError (bad config/stdin, or a Codex patch\n * envelope this parser refuses to guess at) both carry an AI-readable message; anything else is an\n * unexpected bug. All three DENY and surface their reason, because a hook that crashed established\n * nothing and must never be read as an allow.\n *\n * It BUILDS the deny (denyOutcome) rather than throwing it (emitDeny), because it is already inside\n * the catch that the throw would land in — see HookTerminated. Same bytes either way.\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope hook entry points in this adapter; a lone class for one terminal boundary would break the file's shape\nfunction denyForCrash(error: Error, event: AgentHookEvent | null): HookOutcome {\n if (error instanceof RuleFailError) {\n return denyOutcome(event, renderRuleFailForAi(error), 'rule-crash');\n }\n if (error instanceof InformAiError) {\n return denyOutcome(event, error.message, 'bad-config-or-stdin');\n }\n return denyOutcome(event, `[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}`, 'hook-crash');\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import { HookMode } from '../core/types';
2
+ /**
3
+ * What ONE hook invocation decided, as a value instead of as two side effects.
4
+ *
5
+ * Data class per CLAUDE.md rule 1 — fields only. `stdout` is the EXACT bytes to write (a deny's JSON
6
+ * plus its trailing newline, or '' for an allow, because a silent exit 0 IS the allow) and `exitCode`
7
+ * is the last byte of the PreToolUse contract. Making the decision a value is what lets a test assert
8
+ * the composed pipeline's output without a process to inspect.
9
+ */
10
+ export declare class HookOutcome {
11
+ readonly stdout: string;
12
+ readonly exitCode: number;
13
+ constructor(stdout: string, exitCode: number);
14
+ }
15
+ /**
16
+ * The arguments one hook binary is invoked with. Today that is only WHICH category of rules to run —
17
+ * `guards` for the git/PR/branch guards, `rules` for the code-style rules — but it is a class rather
18
+ * than a bare string so a second argument is an added field and not a changed signature at every call
19
+ * site. Data class per CLAUDE.md rule 1.
20
+ */
21
+ export declare class HookArgs {
22
+ readonly mode: HookMode;
23
+ constructor(mode: HookMode);
24
+ }
25
+ /**
26
+ * THE HOOK'S TERMINAL CONTROL FLOW, as a throw.
27
+ *
28
+ * `emitAllow()` / `emitDeny()` are reached from a dozen places nested several frames deep inside the
29
+ * pipeline, and every one of them means "this invocation is over, here is its answer". They used to
30
+ * say that by writing to stdout and terminating the process on the spot, which is precisely what
31
+ * made the composed pipeline untestable — the answer never became a value anybody could look at.
32
+ *
33
+ * Throwing carries the same "nothing after this line runs" guarantee (both helpers are still typed
34
+ * `never`) while turning the answer into a HookOutcome that `HookApp` writes and exits with. The
35
+ * ORDER of observable effects is unchanged: the audit line is still flushed at the emit site, the
36
+ * bytes are still written before the exit, and the exit still happens at the same point, through the
37
+ * injected HookProcessExit port, in production.
38
+ *
39
+ * The ONE thing this shape requires: any `catch` between an emit site and HookApp must RETHROW it
40
+ * rather than treat it as a crash. There is exactly one such catch (the fail-closed boundary in
41
+ * hook-core's `runPipeline`), and it returns the carried outcome.
42
+ */
43
+ export declare class HookTerminated extends Error {
44
+ readonly outcome: HookOutcome;
45
+ constructor(outcome: HookOutcome);
46
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HookTerminated = exports.HookArgs = exports.HookOutcome = void 0;
4
+ /**
5
+ * What ONE hook invocation decided, as a value instead of as two side effects.
6
+ *
7
+ * Data class per CLAUDE.md rule 1 — fields only. `stdout` is the EXACT bytes to write (a deny's JSON
8
+ * plus its trailing newline, or '' for an allow, because a silent exit 0 IS the allow) and `exitCode`
9
+ * is the last byte of the PreToolUse contract. Making the decision a value is what lets a test assert
10
+ * the composed pipeline's output without a process to inspect.
11
+ */
12
+ class HookOutcome {
13
+ stdout;
14
+ exitCode;
15
+ constructor(stdout, exitCode) {
16
+ this.stdout = stdout;
17
+ this.exitCode = exitCode;
18
+ }
19
+ }
20
+ exports.HookOutcome = HookOutcome;
21
+ /**
22
+ * The arguments one hook binary is invoked with. Today that is only WHICH category of rules to run —
23
+ * `guards` for the git/PR/branch guards, `rules` for the code-style rules — but it is a class rather
24
+ * than a bare string so a second argument is an added field and not a changed signature at every call
25
+ * site. Data class per CLAUDE.md rule 1.
26
+ */
27
+ class HookArgs {
28
+ mode;
29
+ constructor(mode) {
30
+ this.mode = mode;
31
+ }
32
+ }
33
+ exports.HookArgs = HookArgs;
34
+ /**
35
+ * THE HOOK'S TERMINAL CONTROL FLOW, as a throw.
36
+ *
37
+ * `emitAllow()` / `emitDeny()` are reached from a dozen places nested several frames deep inside the
38
+ * pipeline, and every one of them means "this invocation is over, here is its answer". They used to
39
+ * say that by writing to stdout and terminating the process on the spot, which is precisely what
40
+ * made the composed pipeline untestable — the answer never became a value anybody could look at.
41
+ *
42
+ * Throwing carries the same "nothing after this line runs" guarantee (both helpers are still typed
43
+ * `never`) while turning the answer into a HookOutcome that `HookApp` writes and exits with. The
44
+ * ORDER of observable effects is unchanged: the audit line is still flushed at the emit site, the
45
+ * bytes are still written before the exit, and the exit still happens at the same point, through the
46
+ * injected HookProcessExit port, in production.
47
+ *
48
+ * The ONE thing this shape requires: any `catch` between an emit site and HookApp must RETHROW it
49
+ * rather than treat it as a crash. There is exactly one such catch (the fail-closed boundary in
50
+ * hook-core's `runPipeline`), and it returns the carried outcome.
51
+ */
52
+ class HookTerminated extends Error {
53
+ outcome;
54
+ constructor(outcome) {
55
+ super('hook invocation terminated');
56
+ this.outcome = outcome;
57
+ }
58
+ }
59
+ exports.HookTerminated = HookTerminated;
60
+ //# sourceMappingURL=hook-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-outcome.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-outcome.ts"],"names":[],"mappings":";;;AAEA;;;;;;;GAOG;AACH,MAAa,WAAW;IACX,MAAM,CAAS;IACf,QAAQ,CAAS;IAE1B,YAAY,MAAc,EAAE,QAAgB;QACxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;CACJ;AARD,kCAQC;AAED;;;;;GAKG;AACH,MAAa,QAAQ;IACR,IAAI,CAAW;IAExB,YAAY,IAAc;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAND,4BAMC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAa,cAAe,SAAQ,KAAK;IAC5B,OAAO,CAAc;IAE9B,YAAY,OAAoB;QAC5B,KAAK,CAAC,4BAA4B,CAAC,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAPD,wCAOC","sourcesContent":["import { HookMode } from '../core/types';\n\n/**\n * What ONE hook invocation decided, as a value instead of as two side effects.\n *\n * Data class per CLAUDE.md rule 1 — fields only. `stdout` is the EXACT bytes to write (a deny's JSON\n * plus its trailing newline, or '' for an allow, because a silent exit 0 IS the allow) and `exitCode`\n * is the last byte of the PreToolUse contract. Making the decision a value is what lets a test assert\n * the composed pipeline's output without a process to inspect.\n */\nexport class HookOutcome {\n readonly stdout: string;\n readonly exitCode: number;\n\n constructor(stdout: string, exitCode: number) {\n this.stdout = stdout;\n this.exitCode = exitCode;\n }\n}\n\n/**\n * The arguments one hook binary is invoked with. Today that is only WHICH category of rules to run —\n * `guards` for the git/PR/branch guards, `rules` for the code-style rules — but it is a class rather\n * than a bare string so a second argument is an added field and not a changed signature at every call\n * site. Data class per CLAUDE.md rule 1.\n */\nexport class HookArgs {\n readonly mode: HookMode;\n\n constructor(mode: HookMode) {\n this.mode = mode;\n }\n}\n\n/**\n * THE HOOK'S TERMINAL CONTROL FLOW, as a throw.\n *\n * `emitAllow()` / `emitDeny()` are reached from a dozen places nested several frames deep inside the\n * pipeline, and every one of them means \"this invocation is over, here is its answer\". They used to\n * say that by writing to stdout and terminating the process on the spot, which is precisely what\n * made the composed pipeline untestable — the answer never became a value anybody could look at.\n *\n * Throwing carries the same \"nothing after this line runs\" guarantee (both helpers are still typed\n * `never`) while turning the answer into a HookOutcome that `HookApp` writes and exits with. The\n * ORDER of observable effects is unchanged: the audit line is still flushed at the emit site, the\n * bytes are still written before the exit, and the exit still happens at the same point, through the\n * injected HookProcessExit port, in production.\n *\n * The ONE thing this shape requires: any `catch` between an emit site and HookApp must RETHROW it\n * rather than treat it as a crash. There is exactly one such catch (the fail-closed boundary in\n * hook-core's `runPipeline`), and it returns the carried outcome.\n */\nexport class HookTerminated extends Error {\n readonly outcome: HookOutcome;\n\n constructor(outcome: HookOutcome) {\n super('hook invocation terminated');\n this.outcome = outcome;\n }\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * THE THREE PROCESS COUPLINGS OF THE HOOK — as injectable seams.
3
+ *
4
+ * Why they exist at all: the hook's real contract is a PIPELINE — `stdin -> parse -> adapter -> runner
5
+ * -> emit -> exit` — and until this file existed there was no way to drive that pipeline from a test.
6
+ * `runMain` read `process.stdin` itself and the emit boundary called `process.stdout.write` /
7
+ * `process.exit` directly, so the only testable units were the pure functions UNDERNEATH the pipeline.
8
+ * Tests therefore sat too low: they moved every time the composition was refactored (the AgentHookEvent
9
+ * refactor rewrote `agent-response.spec.ts`'s call sites wholesale) and they proved nothing about the
10
+ * composed bytes.
11
+ *
12
+ * The boundary is cut JUST ABOVE THE INJECTION POINT: production is
13
+ * `new Container({autobind:true}).get(HookApp).run(new HookArgs('guards'))`, and a test builds the same
14
+ * container with these three classes rebound to doubles. That is the whole difference between the two.
15
+ * NOT a spawned process (too high — slow, and nothing can be substituted), NOT the pure helpers
16
+ * underneath (too low — they move under every refactor).
17
+ *
18
+ * THREE, and no more. The rule engine, the adapters, the filesystem and git are NEVER substituted —
19
+ * substituting any of them would leave the golden tests asserting the shape of the test harness rather
20
+ * than the shape of the pipeline, which is the failure mode this whole seam exists to escape.
21
+ *
22
+ * They also stay OFF the L0 recovery path. `src/bin/install-entry.ts` and `src/bin/shim.ts` import
23
+ * nothing but `fs`/`path` on purpose, so a corrupt `node_modules` still yields a clean fault code
24
+ * instead of a crash; an inversify import reachable from either would turn a diagnosable fault into an
25
+ * opaque one. The container lives in the two hook entry points and HookApp, and nowhere else.
26
+ *
27
+ * CLASSES, not interfaces, and that is the DI convention winning over the "behavior is an interface"
28
+ * rule: this repo injects BY TYPE and forbids `Symbol()` DI tokens (`no-symbol-di-tokens`), and an
29
+ * interface is not a runtime value, so it cannot BE the token. A test double therefore `extends` the
30
+ * port and overrides the one method. See CLAUDE.md section 5.
31
+ */
32
+ export declare class HookStdinSource {
33
+ /**
34
+ * The PreToolUse payload, byte for byte as the harness wrote it.
35
+ *
36
+ * Moved here verbatim from `hook-core.readStdin()` — same encoding, same three listeners, same
37
+ * "resolve('') on error or on a TTY" behaviour, so an empty read still means "nothing to judge"
38
+ * and never a hang.
39
+ */
40
+ read(): Promise<string>;
41
+ }
42
+ export declare class HookStdoutSink {
43
+ /**
44
+ * The PreToolUse decision bytes. A deny is JSON + '\n' on STDOUT; an allow writes NOTHING — the
45
+ * protocol reads a silent exit 0 as "allow". See agent-response.ts for the wire shape itself.
46
+ */
47
+ write(bytes: string): void;
48
+ }
49
+ export declare class HookProcessExit {
50
+ /**
51
+ * The hook's exit code IS the Claude Code PreToolUse protocol: exit 0 + JSON on stdout is a deny,
52
+ * silent exit 0 is an allow, and exit 2 would make Claude ignore stdout entirely. So the exit is
53
+ * not incidental plumbing to be tidied away — it is the last byte of the contract, which is
54
+ * exactly why it is a port and not a bare call.
55
+ */
56
+ exit(code: number): void;
57
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HookProcessExit = exports.HookStdoutSink = exports.HookStdinSource = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const inversify_1 = require("inversify");
6
+ /**
7
+ * THE THREE PROCESS COUPLINGS OF THE HOOK — as injectable seams.
8
+ *
9
+ * Why they exist at all: the hook's real contract is a PIPELINE — `stdin -> parse -> adapter -> runner
10
+ * -> emit -> exit` — and until this file existed there was no way to drive that pipeline from a test.
11
+ * `runMain` read `process.stdin` itself and the emit boundary called `process.stdout.write` /
12
+ * `process.exit` directly, so the only testable units were the pure functions UNDERNEATH the pipeline.
13
+ * Tests therefore sat too low: they moved every time the composition was refactored (the AgentHookEvent
14
+ * refactor rewrote `agent-response.spec.ts`'s call sites wholesale) and they proved nothing about the
15
+ * composed bytes.
16
+ *
17
+ * The boundary is cut JUST ABOVE THE INJECTION POINT: production is
18
+ * `new Container({autobind:true}).get(HookApp).run(new HookArgs('guards'))`, and a test builds the same
19
+ * container with these three classes rebound to doubles. That is the whole difference between the two.
20
+ * NOT a spawned process (too high — slow, and nothing can be substituted), NOT the pure helpers
21
+ * underneath (too low — they move under every refactor).
22
+ *
23
+ * THREE, and no more. The rule engine, the adapters, the filesystem and git are NEVER substituted —
24
+ * substituting any of them would leave the golden tests asserting the shape of the test harness rather
25
+ * than the shape of the pipeline, which is the failure mode this whole seam exists to escape.
26
+ *
27
+ * They also stay OFF the L0 recovery path. `src/bin/install-entry.ts` and `src/bin/shim.ts` import
28
+ * nothing but `fs`/`path` on purpose, so a corrupt `node_modules` still yields a clean fault code
29
+ * instead of a crash; an inversify import reachable from either would turn a diagnosable fault into an
30
+ * opaque one. The container lives in the two hook entry points and HookApp, and nowhere else.
31
+ *
32
+ * CLASSES, not interfaces, and that is the DI convention winning over the "behavior is an interface"
33
+ * rule: this repo injects BY TYPE and forbids `Symbol()` DI tokens (`no-symbol-di-tokens`), and an
34
+ * interface is not a runtime value, so it cannot BE the token. A test double therefore `extends` the
35
+ * port and overrides the one method. See CLAUDE.md section 5.
36
+ */
37
+ let HookStdinSource = class HookStdinSource {
38
+ /**
39
+ * The PreToolUse payload, byte for byte as the harness wrote it.
40
+ *
41
+ * Moved here verbatim from `hook-core.readStdin()` — same encoding, same three listeners, same
42
+ * "resolve('') on error or on a TTY" behaviour, so an empty read still means "nothing to judge"
43
+ * and never a hang.
44
+ */
45
+ read() {
46
+ return new Promise((resolve) => {
47
+ let data = '';
48
+ process.stdin.setEncoding('utf8');
49
+ process.stdin.on('data', (chunk) => { data += chunk; });
50
+ process.stdin.on('end', () => { resolve(data); });
51
+ process.stdin.on('error', () => { resolve(''); });
52
+ if (process.stdin.isTTY)
53
+ resolve('');
54
+ });
55
+ }
56
+ };
57
+ exports.HookStdinSource = HookStdinSource;
58
+ exports.HookStdinSource = HookStdinSource = tslib_1.__decorate([
59
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
60
+ ], HookStdinSource);
61
+ let HookStdoutSink = class HookStdoutSink {
62
+ /**
63
+ * The PreToolUse decision bytes. A deny is JSON + '\n' on STDOUT; an allow writes NOTHING — the
64
+ * protocol reads a silent exit 0 as "allow". See agent-response.ts for the wire shape itself.
65
+ */
66
+ write(bytes) {
67
+ process.stdout.write(bytes);
68
+ }
69
+ };
70
+ exports.HookStdoutSink = HookStdoutSink;
71
+ exports.HookStdoutSink = HookStdoutSink = tslib_1.__decorate([
72
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
73
+ ], HookStdoutSink);
74
+ let HookProcessExit = class HookProcessExit {
75
+ /**
76
+ * The hook's exit code IS the Claude Code PreToolUse protocol: exit 0 + JSON on stdout is a deny,
77
+ * silent exit 0 is an allow, and exit 2 would make Claude ignore stdout entirely. So the exit is
78
+ * not incidental plumbing to be tidied away — it is the last byte of the contract, which is
79
+ * exactly why it is a port and not a bare call.
80
+ */
81
+ exit(code) {
82
+ // webpieces-disable no-process-exit-outside-main -- hook exit-code IS the Claude Code PreToolUse protocol (exit 0 + JSON = the contract); this class is the designated terminal boundary, injected so a test can record the code instead of killing the worker.
83
+ process.exit(code);
84
+ }
85
+ };
86
+ exports.HookProcessExit = HookProcessExit;
87
+ exports.HookProcessExit = HookProcessExit = tslib_1.__decorate([
88
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
89
+ ], HookProcessExit);
90
+ //# sourceMappingURL=hook-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-ports.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-ports.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;;;;OAMG;IACH,IAAI;QACA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAgC,EAAE,EAAE;YACpD,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAClC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAChE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClD,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK;gBAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAlBY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAkB3B;AAGM,IAAM,cAAc,GAApB,MAAM,cAAc;IACvB;;;OAGG;IACH,KAAK,CAAC,KAAa;QACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;CACJ,CAAA;AARY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,cAAc,CAQ1B;AAGM,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;;;OAKG;IACH,IAAI,CAAC,IAAY;QACb,gQAAgQ;QAChQ,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;CACJ,CAAA;AAXY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAW3B","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * THE THREE PROCESS COUPLINGS OF THE HOOK — as injectable seams.\n *\n * Why they exist at all: the hook's real contract is a PIPELINE — `stdin -> parse -> adapter -> runner\n * -> emit -> exit` — and until this file existed there was no way to drive that pipeline from a test.\n * `runMain` read `process.stdin` itself and the emit boundary called `process.stdout.write` /\n * `process.exit` directly, so the only testable units were the pure functions UNDERNEATH the pipeline.\n * Tests therefore sat too low: they moved every time the composition was refactored (the AgentHookEvent\n * refactor rewrote `agent-response.spec.ts`'s call sites wholesale) and they proved nothing about the\n * composed bytes.\n *\n * The boundary is cut JUST ABOVE THE INJECTION POINT: production is\n * `new Container({autobind:true}).get(HookApp).run(new HookArgs('guards'))`, and a test builds the same\n * container with these three classes rebound to doubles. That is the whole difference between the two.\n * NOT a spawned process (too high — slow, and nothing can be substituted), NOT the pure helpers\n * underneath (too low — they move under every refactor).\n *\n * THREE, and no more. The rule engine, the adapters, the filesystem and git are NEVER substituted —\n * substituting any of them would leave the golden tests asserting the shape of the test harness rather\n * than the shape of the pipeline, which is the failure mode this whole seam exists to escape.\n *\n * They also stay OFF the L0 recovery path. `src/bin/install-entry.ts` and `src/bin/shim.ts` import\n * nothing but `fs`/`path` on purpose, so a corrupt `node_modules` still yields a clean fault code\n * instead of a crash; an inversify import reachable from either would turn a diagnosable fault into an\n * opaque one. The container lives in the two hook entry points and HookApp, and nowhere else.\n *\n * CLASSES, not interfaces, and that is the DI convention winning over the \"behavior is an interface\"\n * rule: this repo injects BY TYPE and forbids `Symbol()` DI tokens (`no-symbol-di-tokens`), and an\n * interface is not a runtime value, so it cannot BE the token. A test double therefore `extends` the\n * port and overrides the one method. See CLAUDE.md section 5.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class HookStdinSource {\n /**\n * The PreToolUse payload, byte for byte as the harness wrote it.\n *\n * Moved here verbatim from `hook-core.readStdin()` — same encoding, same three listeners, same\n * \"resolve('') on error or on a TTY\" behaviour, so an empty read still means \"nothing to judge\"\n * and never a hang.\n */\n read(): Promise<string> {\n return new Promise((resolve: (value: string) => void) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => { data += chunk; });\n process.stdin.on('end', () => { resolve(data); });\n process.stdin.on('error', () => { resolve(''); });\n if (process.stdin.isTTY) resolve('');\n });\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class HookStdoutSink {\n /**\n * The PreToolUse decision bytes. A deny is JSON + '\\n' on STDOUT; an allow writes NOTHING — the\n * protocol reads a silent exit 0 as \"allow\". See agent-response.ts for the wire shape itself.\n */\n write(bytes: string): void {\n process.stdout.write(bytes);\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class HookProcessExit {\n /**\n * The hook's exit code IS the Claude Code PreToolUse protocol: exit 0 + JSON on stdout is a deny,\n * silent exit 0 is an allow, and exit 2 would make Claude ignore stdout entirely. So the exit is\n * not incidental plumbing to be tidied away — it is the last byte of the contract, which is\n * exactly why it is a port and not a bare call.\n */\n exit(code: number): void {\n // webpieces-disable no-process-exit-outside-main -- hook exit-code IS the Claude Code PreToolUse protocol (exit 0 + JSON = the contract); this class is the designated terminal boundary, injected so a test can record the code instead of killing the worker.\n process.exit(code);\n }\n}\n"]}
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
+ import 'reflect-metadata';
2
3
  export declare function main(): Promise<void>;
@@ -2,13 +2,27 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.main = main;
5
- // Claude Code PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).
5
+ // Claude Code / Codex PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).
6
6
  // Bash payloads pass through untouched — branch/PR/merge protection is the separate guards hook.
7
- const hook_core_1 = require("./hook-core");
8
- function main() {
9
- return (0, hook_core_1.runMain)('rules');
7
+ //
8
+ // COMPOSITION ROOT, and deliberately nothing else — see guards-hook.ts. The only difference between
9
+ // the two binaries is the HookArgs constructed here.
10
+ require("reflect-metadata");
11
+ const inversify_1 = require("inversify");
12
+ const hook_app_1 = require("./hook-app");
13
+ const hook_outcome_1 = require("./hook-outcome");
14
+ // webpieces-disable no-function-outside-class -- this IS the bin's process entry point, named in package.json `exports` as ./claude-code-guards / ./claude-code-rules; a class here would be a namespace around one call and could not be the module's callable entry.
15
+ async function main() {
16
+ const container = new inversify_1.Container({ autobind: true });
17
+ const app = container.get(hook_app_1.HookApp);
18
+ await app.run(new hook_outcome_1.HookArgs('rules'));
10
19
  }
20
+ // `.catch` and not a bare `void main()`: a container that cannot be built throws BEFORE any HookApp
21
+ // exists, and an unhandled rejection exits non-zero — which PreToolUse reads as a non-blocking error
22
+ // and lets the tool call through. See HookBootFailure. `main` is async so a synchronous throw inside it
23
+ // arrives here as a rejection too.
11
24
  if (require.main === module) {
12
- void main();
25
+ // webpieces-disable no-any-unknown -- a rejection value is `unknown` by construction; HookBootFailure narrows it through toError(), which is the one place that job belongs
26
+ void main().catch((err) => { new hook_app_1.HookBootFailure().report(err); });
13
27
  }
14
28
  //# sourceMappingURL=rules-hook.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules-hook.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/rules-hook.ts"],"names":[],"mappings":";;;AAKA,oBAEC;AAND,+FAA+F;AAC/F,iGAAiG;AACjG,2CAAsC;AAEtC,SAAgB,IAAI;IAChB,OAAO,IAAA,mBAAO,EAAC,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,KAAK,IAAI,EAAE,CAAC;AAChB,CAAC","sourcesContent":["#!/usr/bin/env node\n// Claude Code PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).\n// Bash payloads pass through untouched — branch/PR/merge protection is the separate guards hook.\nimport { runMain } from './hook-core';\n\nexport function main(): Promise<void> {\n return runMain('rules');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
1
+ {"version":3,"file":"rules-hook.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/rules-hook.ts"],"names":[],"mappings":";;;AAaA,oBAIC;AAhBD,uGAAuG;AACvG,iGAAiG;AACjG,EAAE;AACF,oGAAoG;AACpG,qDAAqD;AACrD,4BAA0B;AAC1B,yCAAsC;AAEtC,yCAAsD;AACtD,iDAA0C;AAE1C,uQAAuQ;AAChQ,KAAK,UAAU,IAAI;IACtB,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,kBAAO,CAAC,CAAC;IACnC,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,uBAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,oGAAoG;AACpG,qGAAqG;AACrG,wGAAwG;AACxG,mCAAmC;AACnC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,4KAA4K;IAC5K,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAQ,EAAE,GAAG,IAAI,0BAAe,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtF,CAAC","sourcesContent":["#!/usr/bin/env node\n// Claude Code / Codex PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).\n// Bash payloads pass through untouched — branch/PR/merge protection is the separate guards hook.\n//\n// COMPOSITION ROOT, and deliberately nothing else — see guards-hook.ts. The only difference between\n// the two binaries is the HookArgs constructed here.\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\n\nimport { HookApp, HookBootFailure } from './hook-app';\nimport { HookArgs } from './hook-outcome';\n\n// webpieces-disable no-function-outside-class -- this IS the bin's process entry point, named in package.json `exports` as ./claude-code-guards / ./claude-code-rules; a class here would be a namespace around one call and could not be the module's callable entry.\nexport async function main(): Promise<void> {\n const container = new Container({ autobind: true });\n const app = container.get(HookApp);\n await app.run(new HookArgs('rules'));\n}\n\n// `.catch` and not a bare `void main()`: a container that cannot be built throws BEFORE any HookApp\n// exists, and an unhandled rejection exits non-zero — which PreToolUse reads as a non-blocking error\n// and lets the tool call through. See HookBootFailure. `main` is async so a synchronous throw inside it\n// arrives here as a rejection too.\nif (require.main === module) {\n // webpieces-disable no-any-unknown -- a rejection value is `unknown` by construction; HookBootFailure narrows it through toError(), which is the one place that job belongs\n void main().catch((err: unknown): void => { new HookBootFailure().report(err); });\n}\n"]}
@@ -0,0 +1,65 @@
1
+ import { ToolKind, NormalizedToolInput, NormalizedBashInput } from './types';
2
+ /**
3
+ * WHICH coding agent produced this hook call. The ONE discriminator that decides it lives in
4
+ * ../adapters/detect-ai.ts (`turn_id` present ⇒ codex) and nowhere else.
5
+ *
6
+ * Every codex-only surface — the apply_patch parser, shell read-parity, the shared-tree subagent
7
+ * guard — is gated on this field, so a Claude Code call can never reach any of them. That is not a
8
+ * nicety: Claude Code is the harness every developer uses today and its behaviour must not move.
9
+ */
10
+ export type AiType = 'claude-code' | 'codex';
11
+ /**
12
+ * How the hook ROUTES this call. It is deliberately NOT `ToolKind`.
13
+ *
14
+ * The plan sketched the field as `ToolKind | 'Bash' | 'Ignored'`, and that shape cannot express the
15
+ * measured Codex reality: ONE `apply_patch` envelope carries MANY files with MIXED operations (an
16
+ * Add, an Update and a Delete in a single call). A single event-level `ToolKind` would have to pick
17
+ * one of them and lie about the rest. So the per-file kind moved down onto `FileOperation`, where
18
+ * there is one per file, and the event keeps only the routing question:
19
+ *
20
+ * - 'File' → run the file/edit pipeline over `files` (one entry for Claude, N for Codex).
21
+ * - 'Bash' → run the bash guards over `bash`. `reads` may ALSO be populated on a Codex call
22
+ * whose command is read-shaped (see ../core/shell-read-parity.ts).
23
+ * - 'Read' → the read fast path over `reads`.
24
+ * - 'Ignored' → nothing to judge; allow immediately.
25
+ */
26
+ export type AgentEventKind = 'File' | 'Bash' | 'Read' | 'Ignored';
27
+ /**
28
+ * ONE file touched by ONE operation, with the kind that operation is.
29
+ *
30
+ * `toolKind` is per-FILE precisely because `apply_patch` mixes operations inside one call; see
31
+ * AgentEventKind's header for why this is not carried on the event.
32
+ */
33
+ export declare class FileOperation {
34
+ readonly toolKind: ToolKind;
35
+ readonly input: NormalizedToolInput;
36
+ constructor(toolKind: ToolKind, input: NormalizedToolInput);
37
+ }
38
+ /**
39
+ * The ONE normalized shape every harness's PreToolUse payload is morphed into, so the hook body,
40
+ * the guards and the audit log are written once rather than once per agent.
41
+ *
42
+ * A data class per CLAUDE.md rule 1: fields only, explicit constructor, no business logic. The
43
+ * morphing lives in the adapters (../adapters/claude-code-adapter.ts, ../adapters/codex-adapter.ts).
44
+ */
45
+ export declare class AgentHookEvent {
46
+ readonly aiType: AiType;
47
+ readonly kind: AgentEventKind;
48
+ /** The harness's own tool name — 'Edit', 'apply_patch', 'Bash'. Carried for logs, never switched on. */
49
+ readonly rawToolName: string;
50
+ readonly cwd: string;
51
+ readonly sessionId: string;
52
+ /**
53
+ * MEASURED, and identical in both harnesses: empty ⇒ the coordinator, populated ⇒ a subagent.
54
+ * Used to NAME a log writer, and by the Codex shared-tree guard to know it is talking to a
55
+ * subagent. Never to decide WHICH TREE a call acts on — that is measured from the path.
56
+ */
57
+ readonly agentId: string;
58
+ readonly agentType: string;
59
+ /** Claude: exactly 0 or 1. Codex `apply_patch`: N, with mixed kinds. */
60
+ readonly files: readonly FileOperation[];
61
+ readonly bash: NormalizedBashInput | null;
62
+ /** Read targets. Claude: the `Read` tool's file. Codex: read-parity synthesis, else empty. */
63
+ readonly reads: readonly string[];
64
+ constructor(aiType: AiType, kind: AgentEventKind, rawToolName: string, cwd: string, sessionId: string, agentId: string, agentType: string, files: readonly FileOperation[], bash: NormalizedBashInput | null, reads: readonly string[]);
65
+ }