@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
@@ -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