@webpieces/ai-hook-rules 0.4.703 → 0.4.704

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +2 -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 +4 -0
  9. package/src/adapters/{claude-code-response.js → agent-response.js} +26 -11
  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/hook-core.d.ts +5 -5
  24. package/src/adapters/hook-core.js +117 -113
  25. package/src/adapters/hook-core.js.map +1 -1
  26. package/src/core/agent-event.d.ts +65 -0
  27. package/src/core/agent-event.js +59 -0
  28. package/src/core/agent-event.js.map +1 -0
  29. package/src/core/apply-patch-parse.d.ts +36 -0
  30. package/src/core/apply-patch-parse.js +154 -0
  31. package/src/core/apply-patch-parse.js.map +1 -0
  32. package/src/core/delete-scoped-rules.d.ts +8 -0
  33. package/src/core/delete-scoped-rules.js +31 -0
  34. package/src/core/delete-scoped-rules.js.map +1 -0
  35. package/src/core/runner.js +2 -1
  36. package/src/core/runner.js.map +1 -1
  37. package/src/core/shell-read-parity.d.ts +22 -0
  38. package/src/core/shell-read-parity.js +145 -0
  39. package/src/core/shell-read-parity.js.map +1 -0
  40. package/src/core/types.d.ts +1 -1
  41. package/src/core/types.js.map +1 -1
  42. package/src/index.d.ts +2 -0
  43. package/src/index.js +11 -1
  44. package/src/index.js.map +1 -1
  45. package/src/adapters/claude-code-response.d.ts +0 -3
  46. 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":"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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, InformAiError, RuleFailError, HookMode, BlockedResult } from '../core/types';\nimport { AgentHookEvent, FileOperation } from '../core/agent-event';\nimport { toError } from '../core/to-error';\nimport { emitDeny, emitAllow } from './agent-response';\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 { 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 ADAPTERS = new AgentAdapters();\nconst SUBAGENT_GUARD = new CodexSubagentSharedTreeGuard();\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\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): void {\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 * 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): void {\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 * Shared entry point for every PreToolUse adapter. `mode` selects which tool kinds to validate;\n * payloads outside the mode's scope pass through (emitAllow). Blocks by emitting a PreToolUse\n * `permissionDecision:\"deny\"` JSON on stdout (exit 0) — see agent-response.ts. Fails CLOSED on any\n * unexpected crash (emits a deny) so a broken hook never silently lets an edit through, and the reason\n * surfaces in the agent's UI instead of being hidden on a stderr+exit-2 block.\n */\nexport async function runMain(mode: HookMode): Promise<void> {\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 raw = await readStdin();\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 (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 (event.kind === 'Read') {\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 // 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 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(event, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n 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// 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): never {\n if (error instanceof RuleFailError) {\n emitDeny(event, renderRuleFailForAi(error), 'rule-crash');\n }\n if (error instanceof InformAiError) {\n emitDeny(event, error.message, 'bad-config-or-stdin');\n }\n emitDeny(event, `[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}`, 'hook-crash');\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
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentHookEvent = exports.FileOperation = void 0;
4
+ /**
5
+ * ONE file touched by ONE operation, with the kind that operation is.
6
+ *
7
+ * `toolKind` is per-FILE precisely because `apply_patch` mixes operations inside one call; see
8
+ * AgentEventKind's header for why this is not carried on the event.
9
+ */
10
+ class FileOperation {
11
+ toolKind;
12
+ input;
13
+ constructor(toolKind, input) {
14
+ this.toolKind = toolKind;
15
+ this.input = input;
16
+ }
17
+ }
18
+ exports.FileOperation = FileOperation;
19
+ /**
20
+ * The ONE normalized shape every harness's PreToolUse payload is morphed into, so the hook body,
21
+ * the guards and the audit log are written once rather than once per agent.
22
+ *
23
+ * A data class per CLAUDE.md rule 1: fields only, explicit constructor, no business logic. The
24
+ * morphing lives in the adapters (../adapters/claude-code-adapter.ts, ../adapters/codex-adapter.ts).
25
+ */
26
+ class AgentHookEvent {
27
+ aiType;
28
+ kind;
29
+ /** The harness's own tool name — 'Edit', 'apply_patch', 'Bash'. Carried for logs, never switched on. */
30
+ rawToolName;
31
+ cwd;
32
+ sessionId;
33
+ /**
34
+ * MEASURED, and identical in both harnesses: empty ⇒ the coordinator, populated ⇒ a subagent.
35
+ * Used to NAME a log writer, and by the Codex shared-tree guard to know it is talking to a
36
+ * subagent. Never to decide WHICH TREE a call acts on — that is measured from the path.
37
+ */
38
+ agentId;
39
+ agentType;
40
+ /** Claude: exactly 0 or 1. Codex `apply_patch`: N, with mixed kinds. */
41
+ files;
42
+ bash;
43
+ /** Read targets. Claude: the `Read` tool's file. Codex: read-parity synthesis, else empty. */
44
+ reads;
45
+ constructor(aiType, kind, rawToolName, cwd, sessionId, agentId, agentType, files, bash, reads) {
46
+ this.aiType = aiType;
47
+ this.kind = kind;
48
+ this.rawToolName = rawToolName;
49
+ this.cwd = cwd;
50
+ this.sessionId = sessionId;
51
+ this.agentId = agentId;
52
+ this.agentType = agentType;
53
+ this.files = files;
54
+ this.bash = bash;
55
+ this.reads = reads;
56
+ }
57
+ }
58
+ exports.AgentHookEvent = AgentHookEvent;
59
+ //# sourceMappingURL=agent-event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-event.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/agent-event.ts"],"names":[],"mappings":";;;AA6BA;;;;;GAKG;AACH,MAAa,aAAa;IACb,QAAQ,CAAW;IACnB,KAAK,CAAsB;IAEpC,YAAY,QAAkB,EAAE,KAA0B;QACtD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AARD,sCAQC;AAED;;;;;;GAMG;AACH,MAAa,cAAc;IACd,MAAM,CAAS;IACf,IAAI,CAAiB;IAC9B,wGAAwG;IAC/F,WAAW,CAAS;IACpB,GAAG,CAAS;IACZ,SAAS,CAAS;IAC3B;;;;OAIG;IACM,OAAO,CAAS;IAChB,SAAS,CAAS;IAC3B,wEAAwE;IAC/D,KAAK,CAA2B;IAChC,IAAI,CAA6B;IAC1C,8FAA8F;IACrF,KAAK,CAAoB;IAElC,YACI,MAAc,EACd,IAAoB,EACpB,WAAmB,EACnB,GAAW,EACX,SAAiB,EACjB,OAAe,EACf,SAAiB,EACjB,KAA+B,EAC/B,IAAgC,EAChC,KAAwB;QAExB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AA3CD,wCA2CC","sourcesContent":["import { ToolKind, NormalizedToolInput, NormalizedBashInput } from './types';\n\n/**\n * WHICH coding agent produced this hook call. The ONE discriminator that decides it lives in\n * ../adapters/detect-ai.ts (`turn_id` present ⇒ codex) and nowhere else.\n *\n * Every codex-only surface — the apply_patch parser, shell read-parity, the shared-tree subagent\n * guard — is gated on this field, so a Claude Code call can never reach any of them. That is not a\n * nicety: Claude Code is the harness every developer uses today and its behaviour must not move.\n */\nexport type AiType = 'claude-code' | 'codex';\n\n/**\n * How the hook ROUTES this call. It is deliberately NOT `ToolKind`.\n *\n * The plan sketched the field as `ToolKind | 'Bash' | 'Ignored'`, and that shape cannot express the\n * measured Codex reality: ONE `apply_patch` envelope carries MANY files with MIXED operations (an\n * Add, an Update and a Delete in a single call). A single event-level `ToolKind` would have to pick\n * one of them and lie about the rest. So the per-file kind moved down onto `FileOperation`, where\n * there is one per file, and the event keeps only the routing question:\n *\n * - 'File' → run the file/edit pipeline over `files` (one entry for Claude, N for Codex).\n * - 'Bash' → run the bash guards over `bash`. `reads` may ALSO be populated on a Codex call\n * whose command is read-shaped (see ../core/shell-read-parity.ts).\n * - 'Read' → the read fast path over `reads`.\n * - 'Ignored' → nothing to judge; allow immediately.\n */\nexport type AgentEventKind = 'File' | 'Bash' | 'Read' | 'Ignored';\n\n/**\n * ONE file touched by ONE operation, with the kind that operation is.\n *\n * `toolKind` is per-FILE precisely because `apply_patch` mixes operations inside one call; see\n * AgentEventKind's header for why this is not carried on the event.\n */\nexport class FileOperation {\n readonly toolKind: ToolKind;\n readonly input: NormalizedToolInput;\n\n constructor(toolKind: ToolKind, input: NormalizedToolInput) {\n this.toolKind = toolKind;\n this.input = input;\n }\n}\n\n/**\n * The ONE normalized shape every harness's PreToolUse payload is morphed into, so the hook body,\n * the guards and the audit log are written once rather than once per agent.\n *\n * A data class per CLAUDE.md rule 1: fields only, explicit constructor, no business logic. The\n * morphing lives in the adapters (../adapters/claude-code-adapter.ts, ../adapters/codex-adapter.ts).\n */\nexport class AgentHookEvent {\n readonly aiType: AiType;\n readonly kind: AgentEventKind;\n /** The harness's own tool name — 'Edit', 'apply_patch', 'Bash'. Carried for logs, never switched on. */\n readonly rawToolName: string;\n readonly cwd: string;\n readonly sessionId: string;\n /**\n * MEASURED, and identical in both harnesses: empty ⇒ the coordinator, populated ⇒ a subagent.\n * Used to NAME a log writer, and by the Codex shared-tree guard to know it is talking to a\n * subagent. Never to decide WHICH TREE a call acts on — that is measured from the path.\n */\n readonly agentId: string;\n readonly agentType: string;\n /** Claude: exactly 0 or 1. Codex `apply_patch`: N, with mixed kinds. */\n readonly files: readonly FileOperation[];\n readonly bash: NormalizedBashInput | null;\n /** Read targets. Claude: the `Read` tool's file. Codex: read-parity synthesis, else empty. */\n readonly reads: readonly string[];\n\n constructor(\n aiType: AiType,\n kind: AgentEventKind,\n rawToolName: string,\n cwd: string,\n sessionId: string,\n agentId: string,\n agentType: string,\n files: readonly FileOperation[],\n bash: NormalizedBashInput | null,\n reads: readonly string[],\n ) {\n this.aiType = aiType;\n this.kind = kind;\n this.rawToolName = rawToolName;\n this.cwd = cwd;\n this.sessionId = sessionId;\n this.agentId = agentId;\n this.agentType = agentType;\n this.files = files;\n this.bash = bash;\n this.reads = reads;\n }\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { FileOperation } from './agent-event';
2
+ /**
3
+ * Parses Codex's `apply_patch` envelope into the same `FileOperation[]` the rest of the hook already
4
+ * understands, so a Codex edit is judged by the identical rules a Claude edit is.
5
+ *
6
+ * The grammar is MEASURED from a live codex-cli 0.151.0 session, not inferred from a diff format it
7
+ * resembles. Two properties of it drive every design choice here:
8
+ *
9
+ * - ONE envelope carries MANY files with MIXED operations, which is why the result is a LIST of
10
+ * `FileOperation` (each with its own kind) rather than one kind for the call.
11
+ * - Hunk headers are a BARE `@@` with NO line numbers. A unified-diff `@@ -1,3 +1,4 @@` is therefore
12
+ * not a stricter spelling of the same thing — it is an envelope we have never seen, and guessing at
13
+ * it is how a guard silently judges the wrong bytes. It is rejected.
14
+ *
15
+ * Paths are sometimes relative and sometimes absolute (measured: subagents used relative, the
16
+ * coordinator absolute), so a relative path is resolved against the payload's `cwd`.
17
+ *
18
+ * FAIL CLOSED. Every malformed envelope THROWS `InformAiError`, which the hook's one top-level handler
19
+ * turns into a deny naming the offending line. A parser that "did its best" on an envelope it did not
20
+ * understand would hand the rule engine a subset of the real edit and allow the rest unjudged, which is
21
+ * strictly worse than refusing the call.
22
+ */
23
+ export declare class ApplyPatchParser {
24
+ parse(command: string, cwd: string): readonly FileOperation[];
25
+ /** `*** Add File: <p>` plus a body of `+` lines → a Write of the joined body. */
26
+ private parseAdd;
27
+ /**
28
+ * `*** Update File: <p>`, an optional `*** Move to: <q>`, then one or more bare-`@@` hunks.
29
+ *
30
+ * A `Move to` makes the DESTINATION the judged path. That is the file whose bytes will exist after
31
+ * the call, so it is the one a path-scoped rule must see; judging the source would let a rename
32
+ * into a guarded directory land unjudged.
33
+ */
34
+ private parseUpdate;
35
+ private resolve;
36
+ }
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApplyPatchParser = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const path = tslib_1.__importStar(require("path"));
6
+ const agent_event_1 = require("./agent-event");
7
+ const types_1 = require("./types");
8
+ const BEGIN_PATCH = '*** Begin Patch';
9
+ const END_PATCH = '*** End Patch';
10
+ const ADD_FILE = '*** Add File: ';
11
+ const UPDATE_FILE = '*** Update File: ';
12
+ const DELETE_FILE = '*** Delete File: ';
13
+ const MOVE_TO = '*** Move to: ';
14
+ const HUNK_HEADER = '@@';
15
+ /**
16
+ * Parses Codex's `apply_patch` envelope into the same `FileOperation[]` the rest of the hook already
17
+ * understands, so a Codex edit is judged by the identical rules a Claude edit is.
18
+ *
19
+ * The grammar is MEASURED from a live codex-cli 0.151.0 session, not inferred from a diff format it
20
+ * resembles. Two properties of it drive every design choice here:
21
+ *
22
+ * - ONE envelope carries MANY files with MIXED operations, which is why the result is a LIST of
23
+ * `FileOperation` (each with its own kind) rather than one kind for the call.
24
+ * - Hunk headers are a BARE `@@` with NO line numbers. A unified-diff `@@ -1,3 +1,4 @@` is therefore
25
+ * not a stricter spelling of the same thing — it is an envelope we have never seen, and guessing at
26
+ * it is how a guard silently judges the wrong bytes. It is rejected.
27
+ *
28
+ * Paths are sometimes relative and sometimes absolute (measured: subagents used relative, the
29
+ * coordinator absolute), so a relative path is resolved against the payload's `cwd`.
30
+ *
31
+ * FAIL CLOSED. Every malformed envelope THROWS `InformAiError`, which the hook's one top-level handler
32
+ * turns into a deny naming the offending line. A parser that "did its best" on an envelope it did not
33
+ * understand would hand the rule engine a subset of the real edit and allow the rest unjudged, which is
34
+ * strictly worse than refusing the call.
35
+ */
36
+ class ApplyPatchParser {
37
+ parse(command, cwd) {
38
+ const lines = command.split('\n');
39
+ const begin = lines.findIndex((l) => l.trim() === BEGIN_PATCH);
40
+ if (begin < 0) {
41
+ throw new types_1.InformAiError(`[apply-patch] envelope has no '${BEGIN_PATCH}' line — refusing to judge a patch this parser does not recognise.`);
42
+ }
43
+ const end = lines.findIndex((l) => l.trim() === END_PATCH);
44
+ if (end < 0 || end < begin) {
45
+ throw new types_1.InformAiError(`[apply-patch] envelope has no '${END_PATCH}' line after '${BEGIN_PATCH}' — refusing to judge a truncated patch.`);
46
+ }
47
+ const body = lines.slice(begin + 1, end);
48
+ const ops = [];
49
+ let index = 0;
50
+ while (index < body.length) {
51
+ const line = body[index];
52
+ if (line.startsWith(ADD_FILE)) {
53
+ index = this.parseAdd(body, index, cwd, ops);
54
+ }
55
+ else if (line.startsWith(DELETE_FILE)) {
56
+ ops.push(new agent_event_1.FileOperation('Delete', new types_1.NormalizedToolInput(this.resolve(line.slice(DELETE_FILE.length), cwd), [])));
57
+ index += 1;
58
+ }
59
+ else if (line.startsWith(UPDATE_FILE)) {
60
+ index = this.parseUpdate(body, index, cwd, ops);
61
+ }
62
+ else if (line.trim() === '') {
63
+ index += 1;
64
+ }
65
+ else {
66
+ throw new types_1.InformAiError(`[apply-patch] unexpected line outside any file section: ${JSON.stringify(line)}`);
67
+ }
68
+ }
69
+ if (ops.length === 0) {
70
+ throw new types_1.InformAiError('[apply-patch] envelope names no files — refusing to allow a patch whose targets could not be read.');
71
+ }
72
+ return ops;
73
+ }
74
+ /** `*** Add File: <p>` plus a body of `+` lines → a Write of the joined body. */
75
+ parseAdd(body, start, cwd, ops) {
76
+ const filePath = this.resolve(body[start].slice(ADD_FILE.length), cwd);
77
+ const content = [];
78
+ let index = start + 1;
79
+ while (index < body.length && !body[index].startsWith('*** ')) {
80
+ const line = body[index];
81
+ if (!line.startsWith('+')) {
82
+ throw new types_1.InformAiError(`[apply-patch] '${ADD_FILE.trim()}' body line must begin with '+', got ${JSON.stringify(line)}`);
83
+ }
84
+ content.push(line.slice(1));
85
+ index += 1;
86
+ }
87
+ ops.push(new agent_event_1.FileOperation('Write', new types_1.NormalizedToolInput(filePath, [new types_1.NormalizedEdit('', content.join('\n'))])));
88
+ return index;
89
+ }
90
+ /**
91
+ * `*** Update File: <p>`, an optional `*** Move to: <q>`, then one or more bare-`@@` hunks.
92
+ *
93
+ * A `Move to` makes the DESTINATION the judged path. That is the file whose bytes will exist after
94
+ * the call, so it is the one a path-scoped rule must see; judging the source would let a rename
95
+ * into a guarded directory land unjudged.
96
+ */
97
+ parseUpdate(body, start, cwd, ops) {
98
+ let index = start + 1;
99
+ let filePath = this.resolve(body[start].slice(UPDATE_FILE.length), cwd);
100
+ if (index < body.length && body[index].startsWith(MOVE_TO)) {
101
+ filePath = this.resolve(body[index].slice(MOVE_TO.length), cwd);
102
+ index += 1;
103
+ }
104
+ const edits = [];
105
+ while (index < body.length && !body[index].startsWith('*** ')) {
106
+ const header = body[index];
107
+ if (header.trim() !== HUNK_HEADER) {
108
+ throw new types_1.InformAiError(`[apply-patch] malformed hunk header ${JSON.stringify(header)} — a Codex hunk header is a bare '@@' with no line numbers.`);
109
+ }
110
+ index += 1;
111
+ const oldLines = [];
112
+ const newLines = [];
113
+ while (index < body.length && !body[index].startsWith('*** ') && body[index].trim() !== HUNK_HEADER) {
114
+ const line = body[index];
115
+ // A bare empty line is a CONTEXT line whose content is empty — the leading space is
116
+ // routinely stripped by whatever carried the patch, so treating it as malformed would
117
+ // deny ordinary edits to files with blank lines.
118
+ if (line === '') {
119
+ oldLines.push('');
120
+ newLines.push('');
121
+ }
122
+ else if (line.startsWith(' ')) {
123
+ oldLines.push(line.slice(1));
124
+ newLines.push(line.slice(1));
125
+ }
126
+ else if (line.startsWith('-')) {
127
+ oldLines.push(line.slice(1));
128
+ }
129
+ else if (line.startsWith('+')) {
130
+ newLines.push(line.slice(1));
131
+ }
132
+ else {
133
+ throw new types_1.InformAiError(`[apply-patch] hunk line must begin with ' ', '-' or '+', got ${JSON.stringify(line)}`);
134
+ }
135
+ index += 1;
136
+ }
137
+ edits.push(new types_1.NormalizedEdit(oldLines.join('\n'), newLines.join('\n')));
138
+ }
139
+ if (edits.length === 0) {
140
+ throw new types_1.InformAiError(`[apply-patch] '${UPDATE_FILE.trim()}' section for ${filePath} carries no '@@' hunk — refusing to allow an edit whose content could not be read.`);
141
+ }
142
+ ops.push(new agent_event_1.FileOperation('Edit', new types_1.NormalizedToolInput(filePath, edits)));
143
+ return index;
144
+ }
145
+ resolve(rawPath, cwd) {
146
+ const trimmed = rawPath.trim();
147
+ if (trimmed === '') {
148
+ throw new types_1.InformAiError('[apply-patch] a file directive named an empty path.');
149
+ }
150
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
151
+ }
152
+ }
153
+ exports.ApplyPatchParser = ApplyPatchParser;
154
+ //# sourceMappingURL=apply-patch-parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-patch-parse.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/apply-patch-parse.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { FileOperation } from './agent-event';\nimport { NormalizedEdit, NormalizedToolInput, InformAiError } from './types';\n\nconst BEGIN_PATCH = '*** Begin Patch';\nconst END_PATCH = '*** End Patch';\nconst ADD_FILE = '*** Add File: ';\nconst UPDATE_FILE = '*** Update File: ';\nconst DELETE_FILE = '*** Delete File: ';\nconst MOVE_TO = '*** Move to: ';\nconst HUNK_HEADER = '@@';\n\n/**\n * Parses Codex's `apply_patch` envelope into the same `FileOperation[]` the rest of the hook already\n * understands, so a Codex edit is judged by the identical rules a Claude edit is.\n *\n * The grammar is MEASURED from a live codex-cli 0.151.0 session, not inferred from a diff format it\n * resembles. Two properties of it drive every design choice here:\n *\n * - ONE envelope carries MANY files with MIXED operations, which is why the result is a LIST of\n * `FileOperation` (each with its own kind) rather than one kind for the call.\n * - Hunk headers are a BARE `@@` with NO line numbers. A unified-diff `@@ -1,3 +1,4 @@` is therefore\n * not a stricter spelling of the same thing — it is an envelope we have never seen, and guessing at\n * it is how a guard silently judges the wrong bytes. It is rejected.\n *\n * Paths are sometimes relative and sometimes absolute (measured: subagents used relative, the\n * coordinator absolute), so a relative path is resolved against the payload's `cwd`.\n *\n * FAIL CLOSED. Every malformed envelope THROWS `InformAiError`, which the hook's one top-level handler\n * turns into a deny naming the offending line. A parser that \"did its best\" on an envelope it did not\n * understand would hand the rule engine a subset of the real edit and allow the rest unjudged, which is\n * strictly worse than refusing the call.\n */\nexport class ApplyPatchParser {\n parse(command: string, cwd: string): readonly FileOperation[] {\n const lines = command.split('\\n');\n const begin = lines.findIndex((l: string): boolean => l.trim() === BEGIN_PATCH);\n if (begin < 0) {\n throw new InformAiError(`[apply-patch] envelope has no '${BEGIN_PATCH}' line — refusing to judge a patch this parser does not recognise.`);\n }\n const end = lines.findIndex((l: string): boolean => l.trim() === END_PATCH);\n if (end < 0 || end < begin) {\n throw new InformAiError(`[apply-patch] envelope has no '${END_PATCH}' line after '${BEGIN_PATCH}' — refusing to judge a truncated patch.`);\n }\n\n const body = lines.slice(begin + 1, end);\n const ops: FileOperation[] = [];\n let index = 0;\n while (index < body.length) {\n const line = body[index];\n if (line.startsWith(ADD_FILE)) {\n index = this.parseAdd(body, index, cwd, ops);\n } else if (line.startsWith(DELETE_FILE)) {\n ops.push(new FileOperation('Delete', new NormalizedToolInput(this.resolve(line.slice(DELETE_FILE.length), cwd), [])));\n index += 1;\n } else if (line.startsWith(UPDATE_FILE)) {\n index = this.parseUpdate(body, index, cwd, ops);\n } else if (line.trim() === '') {\n index += 1;\n } else {\n throw new InformAiError(`[apply-patch] unexpected line outside any file section: ${JSON.stringify(line)}`);\n }\n }\n if (ops.length === 0) {\n throw new InformAiError('[apply-patch] envelope names no files — refusing to allow a patch whose targets could not be read.');\n }\n return ops;\n }\n\n /** `*** Add File: <p>` plus a body of `+` lines → a Write of the joined body. */\n private parseAdd(body: readonly string[], start: number, cwd: string, ops: FileOperation[]): number {\n const filePath = this.resolve(body[start].slice(ADD_FILE.length), cwd);\n const content: string[] = [];\n let index = start + 1;\n while (index < body.length && !body[index].startsWith('*** ')) {\n const line = body[index];\n if (!line.startsWith('+')) {\n throw new InformAiError(`[apply-patch] '${ADD_FILE.trim()}' body line must begin with '+', got ${JSON.stringify(line)}`);\n }\n content.push(line.slice(1));\n index += 1;\n }\n ops.push(new FileOperation('Write', new NormalizedToolInput(filePath, [new NormalizedEdit('', content.join('\\n'))])));\n return index;\n }\n\n /**\n * `*** Update File: <p>`, an optional `*** Move to: <q>`, then one or more bare-`@@` hunks.\n *\n * A `Move to` makes the DESTINATION the judged path. That is the file whose bytes will exist after\n * the call, so it is the one a path-scoped rule must see; judging the source would let a rename\n * into a guarded directory land unjudged.\n */\n private parseUpdate(body: readonly string[], start: number, cwd: string, ops: FileOperation[]): number {\n let index = start + 1;\n let filePath = this.resolve(body[start].slice(UPDATE_FILE.length), cwd);\n if (index < body.length && body[index].startsWith(MOVE_TO)) {\n filePath = this.resolve(body[index].slice(MOVE_TO.length), cwd);\n index += 1;\n }\n const edits: NormalizedEdit[] = [];\n while (index < body.length && !body[index].startsWith('*** ')) {\n const header = body[index];\n if (header.trim() !== HUNK_HEADER) {\n throw new InformAiError(`[apply-patch] malformed hunk header ${JSON.stringify(header)} — a Codex hunk header is a bare '@@' with no line numbers.`);\n }\n index += 1;\n const oldLines: string[] = [];\n const newLines: string[] = [];\n while (index < body.length && !body[index].startsWith('*** ') && body[index].trim() !== HUNK_HEADER) {\n const line = body[index];\n // A bare empty line is a CONTEXT line whose content is empty — the leading space is\n // routinely stripped by whatever carried the patch, so treating it as malformed would\n // deny ordinary edits to files with blank lines.\n if (line === '') {\n oldLines.push('');\n newLines.push('');\n } else if (line.startsWith(' ')) {\n oldLines.push(line.slice(1));\n newLines.push(line.slice(1));\n } else if (line.startsWith('-')) {\n oldLines.push(line.slice(1));\n } else if (line.startsWith('+')) {\n newLines.push(line.slice(1));\n } else {\n throw new InformAiError(`[apply-patch] hunk line must begin with ' ', '-' or '+', got ${JSON.stringify(line)}`);\n }\n index += 1;\n }\n edits.push(new NormalizedEdit(oldLines.join('\\n'), newLines.join('\\n')));\n }\n if (edits.length === 0) {\n throw new InformAiError(`[apply-patch] '${UPDATE_FILE.trim()}' section for ${filePath} carries no '@@' hunk — refusing to allow an edit whose content could not be read.`);\n }\n ops.push(new FileOperation('Edit', new NormalizedToolInput(filePath, edits)));\n return index;\n }\n\n private resolve(rawPath: string, cwd: string): string {\n const trimmed = rawPath.trim();\n if (trimmed === '') {\n throw new InformAiError('[apply-patch] a file directive named an empty path.');\n }\n return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { Rule, ToolKind } from './types';
2
+ export declare class DeleteScopedRules {
3
+ /**
4
+ * `rules` unchanged for every other ToolKind; for a Delete, narrowed to the rules that opted into
5
+ * judging one — today, none of them, so a delete is always allowed.
6
+ */
7
+ narrow(toolKind: ToolKind, rules: readonly Rule[]): readonly Rule[];
8
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeleteScopedRules = void 0;
4
+ /**
5
+ * The rules that judge a FILE DELETION. Empty, deliberately, and changing that is a human's call.
6
+ *
7
+ * `Delete` is a new ToolKind, reachable only from Codex's `apply_patch` `*** Delete File:` directive —
8
+ * Claude Code has no delete tool at all. Every rule in this engine was written against the bytes an
9
+ * edit ADDS (an EditContext's `addedContent`), and a deletion has none, so running them on one would
10
+ * either produce nothing or produce a verdict about content that is going away. Defaulting all of them
11
+ * OFF keeps every existing rule's behaviour exactly what it was.
12
+ *
13
+ * Two families would arguably be RIGHT to fire here, and are listed rather than enabled because the
14
+ * decision is a policy call, not a refactor: rules that keep a barrel/index in step with the files it
15
+ * exports, and rules that keep a doc in step with the file it documents. Deleting a file is precisely
16
+ * when those two go stale.
17
+ */
18
+ const DELETE_SCOPED_RULES = new Set([]);
19
+ class DeleteScopedRules {
20
+ /**
21
+ * `rules` unchanged for every other ToolKind; for a Delete, narrowed to the rules that opted into
22
+ * judging one — today, none of them, so a delete is always allowed.
23
+ */
24
+ narrow(toolKind, rules) {
25
+ if (toolKind !== 'Delete')
26
+ return rules;
27
+ return rules.filter((r) => DELETE_SCOPED_RULES.has(r.name));
28
+ }
29
+ }
30
+ exports.DeleteScopedRules = DeleteScopedRules;
31
+ //# sourceMappingURL=delete-scoped-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-scoped-rules.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/delete-scoped-rules.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;AAE7D,MAAa,iBAAiB;IAC1B;;;OAGG;IACH,MAAM,CAAC,QAAkB,EAAE,KAAsB;QAC7C,IAAI,QAAQ,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACxC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAO,EAAW,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/E,CAAC;CACJ;AATD,8CASC","sourcesContent":["import { Rule, ToolKind } from './types';\n\n/**\n * The rules that judge a FILE DELETION. Empty, deliberately, and changing that is a human's call.\n *\n * `Delete` is a new ToolKind, reachable only from Codex's `apply_patch` `*** Delete File:` directive —\n * Claude Code has no delete tool at all. Every rule in this engine was written against the bytes an\n * edit ADDS (an EditContext's `addedContent`), and a deletion has none, so running them on one would\n * either produce nothing or produce a verdict about content that is going away. Defaulting all of them\n * OFF keeps every existing rule's behaviour exactly what it was.\n *\n * Two families would arguably be RIGHT to fire here, and are listed rather than enabled because the\n * decision is a policy call, not a refactor: rules that keep a barrel/index in step with the files it\n * exports, and rules that keep a doc in step with the file it documents. Deleting a file is precisely\n * when those two go stale.\n */\nconst DELETE_SCOPED_RULES: ReadonlySet<string> = new Set([]);\n\nexport class DeleteScopedRules {\n /**\n * `rules` unchanged for every other ToolKind; for a Delete, narrowed to the rules that opted into\n * judging one — today, none of them, so a delete is always allowed.\n */\n narrow(toolKind: ToolKind, rules: readonly Rule[]): readonly Rule[] {\n if (toolKind !== 'Delete') return rules;\n return rules.filter((r: Rule): boolean => DELETE_SCOPED_RULES.has(r.name));\n }\n}\n"]}
@@ -11,6 +11,7 @@ const tslib_1 = require("tslib");
11
11
  const path = tslib_1.__importStar(require("path"));
12
12
  const rules_config_1 = require("@webpieces/rules-config");
13
13
  const build_context_1 = require("./build-context");
14
+ const delete_scoped_rules_1 = require("./delete-scoped-rules");
14
15
  const version_sync_1 = require("./version-sync");
15
16
  const effective_tree_1 = require("./effective-tree");
16
17
  const excluded_path_escape_1 = require("./excluded-path-escape");
@@ -124,7 +125,7 @@ function runInternal(toolKind, input, cwd, mode) {
124
125
  // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole
125
126
  // rule set and is fully hands-off — no violations AND no config-sync nag on those files.
126
127
  const relativePath = path.relative(workspaceRoot, input.filePath);
127
- const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);
128
+ const rules = new delete_scoped_rules_1.DeleteScopedRules().narrow(toolKind, filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths));
128
129
  if (rules.length === 0)
129
130
  return null;
130
131
  // Config-sync applies only to built-in/custom rules; match-rules have their own validated section