@webpieces/ai-hook-rules 0.4.609 → 0.4.611

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 (74) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/adapters/claude-code-response.js +6 -2
  4. package/src/adapters/claude-code-response.js.map +1 -1
  5. package/src/adapters/hook-core.js +13 -10
  6. package/src/adapters/hook-core.js.map +1 -1
  7. package/src/bin/dev-hook-install.js +3 -3
  8. package/src/bin/dev-hook-install.js.map +1 -1
  9. package/src/bin/guarantee-root.js +23 -10
  10. package/src/bin/guarantee-root.js.map +1 -1
  11. package/src/bin/l0-allowlist.js +1 -1
  12. package/src/bin/l0-allowlist.js.map +1 -1
  13. package/src/bin/setup.js +6 -3
  14. package/src/bin/setup.js.map +1 -1
  15. package/src/bin/shim-audit-log.d.ts +0 -3
  16. package/src/bin/shim-audit-log.js +14 -13
  17. package/src/bin/shim-audit-log.js.map +1 -1
  18. package/src/bin/shim.js +2 -2
  19. package/src/bin/shim.js.map +1 -1
  20. package/src/core/decision-log.d.ts +75 -10
  21. package/src/core/decision-log.js +91 -28
  22. package/src/core/decision-log.js.map +1 -1
  23. package/src/core/l0-fault-codes.d.ts +1 -1
  24. package/src/core/l0-fault-codes.js +1 -1
  25. package/src/core/l0-fault-codes.js.map +1 -1
  26. package/src/core/l1-doc.js +6 -0
  27. package/src/core/l1-doc.js.map +1 -1
  28. package/src/core/l1-rows.d.ts +13 -0
  29. package/src/core/l1-rows.js +24 -11
  30. package/src/core/l1-rows.js.map +1 -1
  31. package/src/core/load-rules.js +2 -0
  32. package/src/core/load-rules.js.map +1 -1
  33. package/src/core/log-stream.d.ts +32 -18
  34. package/src/core/log-stream.js +33 -19
  35. package/src/core/log-stream.js.map +1 -1
  36. package/src/core/log-streams.d.ts +38 -0
  37. package/src/core/log-streams.js +45 -0
  38. package/src/core/log-streams.js.map +1 -0
  39. package/src/core/main-sync-log.d.ts +2 -2
  40. package/src/core/main-sync-log.js +22 -15
  41. package/src/core/main-sync-log.js.map +1 -1
  42. package/src/core/main-sync-refresh.d.ts +3 -3
  43. package/src/core/main-sync-refresh.js +4 -4
  44. package/src/core/main-sync-refresh.js.map +1 -1
  45. package/src/core/rejection-log.d.ts +1 -1
  46. package/src/core/rejection-log.js +13 -12
  47. package/src/core/rejection-log.js.map +1 -1
  48. package/src/core/rules/feature-branch-guard.d.ts +11 -0
  49. package/src/core/rules/feature-branch-guard.js +20 -5
  50. package/src/core/rules/feature-branch-guard.js.map +1 -1
  51. package/src/core/rules/index.js +1 -0
  52. package/src/core/rules/index.js.map +1 -1
  53. package/src/core/rules/merged-branch-bash-guard.d.ts +12 -1
  54. package/src/core/rules/merged-branch-bash-guard.js +21 -6
  55. package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
  56. package/src/core/rules/read-stale-guard.d.ts +11 -0
  57. package/src/core/rules/read-stale-guard.js +25 -10
  58. package/src/core/rules/read-stale-guard.js.map +1 -1
  59. package/src/core/rules/stale-main-bash-guard.d.ts +11 -0
  60. package/src/core/rules/stale-main-bash-guard.js +22 -7
  61. package/src/core/rules/stale-main-bash-guard.js.map +1 -1
  62. package/src/core/rules/whole-repo-build-guard.d.ts +88 -0
  63. package/src/core/rules/whole-repo-build-guard.js +176 -0
  64. package/src/core/rules/whole-repo-build-guard.js.map +1 -0
  65. package/src/core/rules/whole-repo-build-scan.d.ts +35 -0
  66. package/src/core/rules/whole-repo-build-scan.js +216 -0
  67. package/src/core/rules/whole-repo-build-scan.js.map +1 -0
  68. package/src/core/runner.js +34 -15
  69. package/src/core/runner.js.map +1 -1
  70. package/src/core/sync-main.d.ts +1 -1
  71. package/src/core/sync-main.js +2 -2
  72. package/src/core/sync-main.js.map +1 -1
  73. package/templates/ai-hook.sh +11 -8
  74. package/templates/guarantee-root.sh +10 -7
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WholeRepoBuildScan = exports.WholeRepoBuildHit = void 0;
4
+ /**
5
+ * Decides the one question whole-repo-build-guard asks of a command: would this segment build (or
6
+ * test) the ENTIRE monorepo rather than the part the change touches?
7
+ *
8
+ * ─── Why this is a SHAPE test and not a string match ───────────────────────────────────────────────
9
+ * The command that motivated the guard is `pnpm run build-all`, but matching that literal buys about a
10
+ * day: `npm run build-all`, `pnpm nx run-many -t ci`, `pnpm exec vitest run` and `time pnpm build-all`
11
+ * are the same build with a different spelling, and an agent that is blocked reaches for the next
12
+ * spelling rather than the smaller build. So the classifier normalizes away the package-manager
13
+ * wrapper first, then judges the PROGRAM and its narrowing arguments:
14
+ *
15
+ * BLOCKED a whole-repo script name — `build-all`, `webpieces:ci`, `wp-ci`, … — under any runner
16
+ * BLOCKED `nx run-many` of a BUILD target with no `-p`/`--projects`, or with `--all`
17
+ * BLOCKED `nx affected` of a BUILD target with no `--base` (a far wider base than the fork point)
18
+ * BLOCKED `nx <target>` with no project argument (`nx test`)
19
+ * BLOCKED `vitest`/`vitest run` with no path, `--project` or `--dir` to narrow it
20
+ * BLOCKED a bare `pnpm test` AT THE WORKSPACE ROOT — the root `test` script is the whole suite
21
+ *
22
+ * ALLOWED `nx affected --target=ci --base=<anything>` — the gate's own command
23
+ * ALLOWED `nx run <project>:<target>`, `nx run-many -t test -p a b`, `nx test core-util`
24
+ * ALLOWED `vitest run packages/core/core-util`, `vitest run --project core-util`
25
+ * ALLOWED anything carrying `--filter` (pnpm's own narrowing), and every non-build command
26
+ * ALLOWED a workspace-wide REGENERATION — `nx run-many --target=di-graph-generate` — which is
27
+ * supposed to cover every project and which this repo's own docs prescribe
28
+ *
29
+ * Narrowing is judged POSITIVELY: a command is blocked only when the shape is whole-repo AND nothing
30
+ * in it narrows the scope. A flag this file has never heard of therefore fails toward ALLOW, which is
31
+ * the right direction for a guard whose false positive costs an agent its next legitimate command.
32
+ */
33
+ // Package-manager wrappers that precede the real program (`pnpm exec vitest`, `npx nx`, `yarn build-all`).
34
+ const RUNNERS = new Set(['pnpm', 'npm', 'yarn', 'npx', 'pnpx', 'bun', 'bunx']);
35
+ // The runner's own verb, consumed with it: `pnpm run build-all` and `pnpm build-all` are one command.
36
+ const RUNNER_VERBS = new Set(['run', 'run-script', 'exec', 'dlx', 'x']);
37
+ // Any of these ANYWHERE in the segment means the human/agent already scoped the work, so nothing here
38
+ // applies. `--filter` is pnpm's project selector; the nx selectors are checked again per-subcommand.
39
+ const NARROWING_FLAGS = ['--filter', '-p', '--projects', '--project', '--dir', '--projectPath'];
40
+ // Script/bin names that ARE "build the world" in every repo that defines them. `wp-ci` is webpieces'
41
+ // own whole-repo validator and is listed as a PROGRAM as well as a script: blocking `build-all` while
42
+ // leaving the thing it delegates to open would be a guard with a labelled side door.
43
+ const WHOLE_REPO_SCRIPTS = new Set([
44
+ 'build-all', 'build:all', 'buildall',
45
+ 'test-all', 'test:all', 'lint-all', 'ci-all', 'ci:all',
46
+ 'webpieces:ci', 'wp-ci',
47
+ ]);
48
+ // nx verbs that are NOT "run this target across the workspace". Anything else in the first position is
49
+ // read as a TARGET name (`nx test`, `nx build`), which is whole-repo unless a project follows.
50
+ const NX_COMMANDS = new Set([
51
+ 'run', 'run-many', 'affected', 'affected:graph', 'print-affected',
52
+ 'show', 'graph', 'dep-graph', 'reset', 'daemon', 'list', 'report', 'migrate',
53
+ 'generate', 'g', 'new', 'format', 'repair', 'connect', 'add', 'init', 'release',
54
+ 'sync', 'watch', 'exec', 'login', 'logout', 'record', 'view-logs', 'import',
55
+ ]);
56
+ /**
57
+ * The targets this guard is about. A workspace-wide `nx run-many -t di-graph-generate` or
58
+ * `-t architecture:generate` is a REGENERATION and is supposed to cover every project — the repo's own
59
+ * docs prescribe exactly that, and blocking it would make the guard wrong about a command an agent has
60
+ * to run. Only the compile/test/lint family is a "build of the world".
61
+ */
62
+ const BUILD_TARGETS = new Set([
63
+ 'ci', 'build', 'test', 'lint', 'e2e', 'typecheck', 'check', 'compile', 'verify',
64
+ ]);
65
+ // vitest's leading verb, dropped before looking for a path filter.
66
+ const VITEST_VERBS = new Set(['run', 'watch', 'dev', 'related', 'bench', 'list', 'typecheck', 'init']);
67
+ // vitest flags whose VALUE is the next token, so the value is never mistaken for a path filter.
68
+ const VITEST_VALUE_FLAGS = new Set([
69
+ '--reporter', '--config', '-c', '--mode', '-m', '--shard', '--pool', '--environment',
70
+ '--outputFile', '--maxWorkers', '--minWorkers', '--testNamePattern', '-t', '--retry',
71
+ '--testTimeout', '--hookTimeout', '--exclude', '--include', '--coverage.reporter',
72
+ ]);
73
+ /** One whole-repo build found in a command: the segment, and the SHAPE that made it one. Data-only. */
74
+ class WholeRepoBuildHit {
75
+ segment;
76
+ shape;
77
+ constructor(segment, shape) {
78
+ this.segment = segment;
79
+ this.shape = shape;
80
+ }
81
+ }
82
+ exports.WholeRepoBuildHit = WholeRepoBuildHit;
83
+ class WholeRepoBuildScan {
84
+ scanner;
85
+ atWorkspaceRoot;
86
+ constructor(scanner,
87
+ // True when the command runs at the workspace ROOT, where a bare `pnpm test` is the whole suite.
88
+ atWorkspaceRoot) {
89
+ this.scanner = scanner;
90
+ this.atWorkspaceRoot = atWorkspaceRoot;
91
+ }
92
+ /** The first segment of `command` that builds the whole repo, or null when none does. */
93
+ firstHit(command) {
94
+ for (const segment of this.scanner.commandSegments(command)) {
95
+ const shape = this.shapeOf(segment);
96
+ if (shape !== null)
97
+ return new WholeRepoBuildHit(segment, shape);
98
+ }
99
+ return null;
100
+ }
101
+ // The whole-repo shape this segment is, or null. One dispatch per program family.
102
+ shapeOf(segment) {
103
+ // Narrowing is judged on the RAW words, BEFORE the runner is stripped: `pnpm --filter core-util
104
+ // test` carries its selector on the RUNNER, and stripping first throws that fact away — which is
105
+ // exactly how a scoped command ends up blocked.
106
+ if (this.isNarrowed(this.scanner.words(segment)))
107
+ return null;
108
+ const words = this.effectiveWords(segment);
109
+ if (words.length === 0)
110
+ return null;
111
+ const program = this.programName(words[0]);
112
+ const args = words.slice(1);
113
+ if (WHOLE_REPO_SCRIPTS.has(program))
114
+ return `${program} builds every project`;
115
+ if (program === 'nx')
116
+ return this.nxShape(args);
117
+ if (program === 'vitest')
118
+ return this.vitestShape(args);
119
+ if (program === 'test' && args.length === 0 && this.atWorkspaceRoot) {
120
+ return 'the root `test` script runs every spec in the repo';
121
+ }
122
+ return null;
123
+ }
124
+ /**
125
+ * The segment's words with wrappers stripped: shell prefixes (`time`, `sudo`, env assignments) by
126
+ * CommandScanner, then the package-manager runner and its verb. `pnpm exec vitest run` and
127
+ * `vitest run` reduce to the same words, which is the whole point.
128
+ */
129
+ effectiveWords(segment) {
130
+ let words = this.scanner.words(segment);
131
+ while (words.length > 0 && RUNNERS.has(this.programName(words[0]))) {
132
+ words = words.slice(1);
133
+ // The runner's own flags (`--silent`, `-r`) sit between it and the program.
134
+ while (words.length > 0 && words[0].startsWith('-'))
135
+ words = words.slice(1);
136
+ if (words.length > 0 && RUNNER_VERBS.has(words[0]))
137
+ words = words.slice(1);
138
+ }
139
+ return words;
140
+ }
141
+ // `./node_modules/.bin/nx` and `/usr/local/bin/pnpm` are the same programs as `nx` and `pnpm`.
142
+ programName(token) {
143
+ const base = token.split('/').pop() ?? token;
144
+ return base.endsWith('.cmd') ? base.slice(0, -'.cmd'.length) : base;
145
+ }
146
+ // Did the caller already scope this command? Judged over the WHOLE segment, so a selector attached
147
+ // to the runner (`pnpm --filter core-util test`) counts just as much as one attached to nx.
148
+ isNarrowed(words) {
149
+ return words.some((w) => NARROWING_FLAGS.some((flag) => w === flag || w.startsWith(`${flag}=`)));
150
+ }
151
+ nxShape(args) {
152
+ const positional = args.filter((a) => !a.startsWith('-'));
153
+ const subcommand = positional[0] ?? '';
154
+ if (subcommand === 'run-many') {
155
+ if (!this.buildsAnything(args, positional))
156
+ return null;
157
+ if (this.hasFlag(args, '--all'))
158
+ return 'nx run-many --all builds every project';
159
+ return 'nx run-many with no --projects builds every project';
160
+ }
161
+ if (subcommand === 'affected') {
162
+ if (this.hasFlag(args, '--base'))
163
+ return null;
164
+ if (!this.buildsAnything(args, positional))
165
+ return null;
166
+ return 'nx affected with no --base compares against a far wider base than the fork point';
167
+ }
168
+ if (subcommand === '' || NX_COMMANDS.has(subcommand))
169
+ return null;
170
+ // `nx <target>` — whole-workspace unless a project name follows it.
171
+ if (positional.length > 1)
172
+ return null;
173
+ if (!BUILD_TARGETS.has(subcommand))
174
+ return null;
175
+ return `nx ${subcommand} with no project runs that target everywhere`;
176
+ }
177
+ vitestShape(args) {
178
+ let rest = args;
179
+ if (rest.length > 0 && VITEST_VERBS.has(rest[0]))
180
+ rest = rest.slice(1);
181
+ for (let i = 0; i < rest.length; i++) {
182
+ const word = rest[i];
183
+ if (!word.startsWith('-'))
184
+ return null; // a path filter — narrowed
185
+ if (VITEST_VALUE_FLAGS.has(word))
186
+ i++; // skip the flag's value
187
+ }
188
+ return 'vitest with no path filter runs every spec in the repo';
189
+ }
190
+ /**
191
+ * Does this nx invocation run a COMPILE/TEST target? Targets come from `-t`/`--target`/`--targets`
192
+ * in either spelling, comma-separated, plus (for `run-many <target>`) a second positional.
193
+ *
194
+ * An invocation naming NO target is not judged a build: nx errors on it, and guessing would block a
195
+ * typo with a message about build scope. An invocation naming only regeneration targets
196
+ * (`di-graph-generate`, `generate`) is deliberately allowed workspace-wide — see BUILD_TARGETS.
197
+ */
198
+ buildsAnything(args, positional) {
199
+ const targets = positional.slice(1);
200
+ for (let i = 0; i < args.length; i++) {
201
+ const word = args[i];
202
+ const flag = ['-t', '--target', '--targets'].find((f) => word === f || word.startsWith(`${f}=`));
203
+ if (flag === undefined)
204
+ continue;
205
+ const value = word === flag ? (args[i + 1] ?? '') : word.slice(flag.length + 1);
206
+ targets.push(...value.split(','));
207
+ }
208
+ return targets.some((t) => BUILD_TARGETS.has(t.trim()));
209
+ }
210
+ // `--base` / `--base=<x>` / `--base <x>` — the attached and detached spellings of one flag.
211
+ hasFlag(args, flag) {
212
+ return args.some((a) => a === flag || a.startsWith(`${flag}=`));
213
+ }
214
+ }
215
+ exports.WholeRepoBuildScan = WholeRepoBuildScan;
216
+ //# sourceMappingURL=whole-repo-build-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whole-repo-build-scan.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/whole-repo-build-scan.ts"],"names":[],"mappings":"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","sourcesContent":["import { CommandScanner } from '../command-scan';\n\n/**\n * Decides the one question whole-repo-build-guard asks of a command: would this segment build (or\n * test) the ENTIRE monorepo rather than the part the change touches?\n *\n * ─── Why this is a SHAPE test and not a string match ───────────────────────────────────────────────\n * The command that motivated the guard is `pnpm run build-all`, but matching that literal buys about a\n * day: `npm run build-all`, `pnpm nx run-many -t ci`, `pnpm exec vitest run` and `time pnpm build-all`\n * are the same build with a different spelling, and an agent that is blocked reaches for the next\n * spelling rather than the smaller build. So the classifier normalizes away the package-manager\n * wrapper first, then judges the PROGRAM and its narrowing arguments:\n *\n * BLOCKED a whole-repo script name — `build-all`, `webpieces:ci`, `wp-ci`, … — under any runner\n * BLOCKED `nx run-many` of a BUILD target with no `-p`/`--projects`, or with `--all`\n * BLOCKED `nx affected` of a BUILD target with no `--base` (a far wider base than the fork point)\n * BLOCKED `nx <target>` with no project argument (`nx test`)\n * BLOCKED `vitest`/`vitest run` with no path, `--project` or `--dir` to narrow it\n * BLOCKED a bare `pnpm test` AT THE WORKSPACE ROOT — the root `test` script is the whole suite\n *\n * ALLOWED `nx affected --target=ci --base=<anything>` — the gate's own command\n * ALLOWED `nx run <project>:<target>`, `nx run-many -t test -p a b`, `nx test core-util`\n * ALLOWED `vitest run packages/core/core-util`, `vitest run --project core-util`\n * ALLOWED anything carrying `--filter` (pnpm's own narrowing), and every non-build command\n * ALLOWED a workspace-wide REGENERATION — `nx run-many --target=di-graph-generate` — which is\n * supposed to cover every project and which this repo's own docs prescribe\n *\n * Narrowing is judged POSITIVELY: a command is blocked only when the shape is whole-repo AND nothing\n * in it narrows the scope. A flag this file has never heard of therefore fails toward ALLOW, which is\n * the right direction for a guard whose false positive costs an agent its next legitimate command.\n */\n\n// Package-manager wrappers that precede the real program (`pnpm exec vitest`, `npx nx`, `yarn build-all`).\nconst RUNNERS: ReadonlySet<string> = new Set(['pnpm', 'npm', 'yarn', 'npx', 'pnpx', 'bun', 'bunx']);\n\n// The runner's own verb, consumed with it: `pnpm run build-all` and `pnpm build-all` are one command.\nconst RUNNER_VERBS: ReadonlySet<string> = new Set(['run', 'run-script', 'exec', 'dlx', 'x']);\n\n// Any of these ANYWHERE in the segment means the human/agent already scoped the work, so nothing here\n// applies. `--filter` is pnpm's project selector; the nx selectors are checked again per-subcommand.\nconst NARROWING_FLAGS: readonly string[] = ['--filter', '-p', '--projects', '--project', '--dir', '--projectPath'];\n\n// Script/bin names that ARE \"build the world\" in every repo that defines them. `wp-ci` is webpieces'\n// own whole-repo validator and is listed as a PROGRAM as well as a script: blocking `build-all` while\n// leaving the thing it delegates to open would be a guard with a labelled side door.\nconst WHOLE_REPO_SCRIPTS: ReadonlySet<string> = new Set([\n 'build-all', 'build:all', 'buildall',\n 'test-all', 'test:all', 'lint-all', 'ci-all', 'ci:all',\n 'webpieces:ci', 'wp-ci',\n]);\n\n// nx verbs that are NOT \"run this target across the workspace\". Anything else in the first position is\n// read as a TARGET name (`nx test`, `nx build`), which is whole-repo unless a project follows.\nconst NX_COMMANDS: ReadonlySet<string> = new Set([\n 'run', 'run-many', 'affected', 'affected:graph', 'print-affected',\n 'show', 'graph', 'dep-graph', 'reset', 'daemon', 'list', 'report', 'migrate',\n 'generate', 'g', 'new', 'format', 'repair', 'connect', 'add', 'init', 'release',\n 'sync', 'watch', 'exec', 'login', 'logout', 'record', 'view-logs', 'import',\n]);\n\n/**\n * The targets this guard is about. A workspace-wide `nx run-many -t di-graph-generate` or\n * `-t architecture:generate` is a REGENERATION and is supposed to cover every project — the repo's own\n * docs prescribe exactly that, and blocking it would make the guard wrong about a command an agent has\n * to run. Only the compile/test/lint family is a \"build of the world\".\n */\nconst BUILD_TARGETS: ReadonlySet<string> = new Set([\n 'ci', 'build', 'test', 'lint', 'e2e', 'typecheck', 'check', 'compile', 'verify',\n]);\n\n// vitest's leading verb, dropped before looking for a path filter.\nconst VITEST_VERBS: ReadonlySet<string> = new Set(['run', 'watch', 'dev', 'related', 'bench', 'list', 'typecheck', 'init']);\n\n// vitest flags whose VALUE is the next token, so the value is never mistaken for a path filter.\nconst VITEST_VALUE_FLAGS: ReadonlySet<string> = new Set([\n '--reporter', '--config', '-c', '--mode', '-m', '--shard', '--pool', '--environment',\n '--outputFile', '--maxWorkers', '--minWorkers', '--testNamePattern', '-t', '--retry',\n '--testTimeout', '--hookTimeout', '--exclude', '--include', '--coverage.reporter',\n]);\n\n/** One whole-repo build found in a command: the segment, and the SHAPE that made it one. Data-only. */\nexport class WholeRepoBuildHit {\n segment: string;\n shape: string;\n\n constructor(segment: string, shape: string) {\n this.segment = segment;\n this.shape = shape;\n }\n}\n\nexport class WholeRepoBuildScan {\n constructor(\n private readonly scanner: CommandScanner,\n // True when the command runs at the workspace ROOT, where a bare `pnpm test` is the whole suite.\n private readonly atWorkspaceRoot: boolean,\n ) {}\n\n /** The first segment of `command` that builds the whole repo, or null when none does. */\n firstHit(command: string): WholeRepoBuildHit | null {\n for (const segment of this.scanner.commandSegments(command)) {\n const shape = this.shapeOf(segment);\n if (shape !== null) return new WholeRepoBuildHit(segment, shape);\n }\n return null;\n }\n\n // The whole-repo shape this segment is, or null. One dispatch per program family.\n private shapeOf(segment: string): string | null {\n // Narrowing is judged on the RAW words, BEFORE the runner is stripped: `pnpm --filter core-util\n // test` carries its selector on the RUNNER, and stripping first throws that fact away — which is\n // exactly how a scoped command ends up blocked.\n if (this.isNarrowed(this.scanner.words(segment))) return null;\n\n const words = this.effectiveWords(segment);\n if (words.length === 0) return null;\n\n const program = this.programName(words[0]);\n const args = words.slice(1);\n\n if (WHOLE_REPO_SCRIPTS.has(program)) return `${program} builds every project`;\n if (program === 'nx') return this.nxShape(args);\n if (program === 'vitest') return this.vitestShape(args);\n if (program === 'test' && args.length === 0 && this.atWorkspaceRoot) {\n return 'the root `test` script runs every spec in the repo';\n }\n return null;\n }\n\n /**\n * The segment's words with wrappers stripped: shell prefixes (`time`, `sudo`, env assignments) by\n * CommandScanner, then the package-manager runner and its verb. `pnpm exec vitest run` and\n * `vitest run` reduce to the same words, which is the whole point.\n */\n private effectiveWords(segment: string): readonly string[] {\n let words = this.scanner.words(segment);\n while (words.length > 0 && RUNNERS.has(this.programName(words[0]))) {\n words = words.slice(1);\n // The runner's own flags (`--silent`, `-r`) sit between it and the program.\n while (words.length > 0 && words[0].startsWith('-')) words = words.slice(1);\n if (words.length > 0 && RUNNER_VERBS.has(words[0])) words = words.slice(1);\n }\n return words;\n }\n\n // `./node_modules/.bin/nx` and `/usr/local/bin/pnpm` are the same programs as `nx` and `pnpm`.\n private programName(token: string): string {\n const base = token.split('/').pop() ?? token;\n return base.endsWith('.cmd') ? base.slice(0, -'.cmd'.length) : base;\n }\n\n // Did the caller already scope this command? Judged over the WHOLE segment, so a selector attached\n // to the runner (`pnpm --filter core-util test`) counts just as much as one attached to nx.\n private isNarrowed(words: readonly string[]): boolean {\n return words.some((w: string): boolean =>\n NARROWING_FLAGS.some((flag: string): boolean => w === flag || w.startsWith(`${flag}=`)));\n }\n\n private nxShape(args: readonly string[]): string | null {\n const positional = args.filter((a: string): boolean => !a.startsWith('-'));\n const subcommand = positional[0] ?? '';\n\n if (subcommand === 'run-many') {\n if (!this.buildsAnything(args, positional)) return null;\n if (this.hasFlag(args, '--all')) return 'nx run-many --all builds every project';\n return 'nx run-many with no --projects builds every project';\n }\n if (subcommand === 'affected') {\n if (this.hasFlag(args, '--base')) return null;\n if (!this.buildsAnything(args, positional)) return null;\n return 'nx affected with no --base compares against a far wider base than the fork point';\n }\n if (subcommand === '' || NX_COMMANDS.has(subcommand)) return null;\n // `nx <target>` — whole-workspace unless a project name follows it.\n if (positional.length > 1) return null;\n if (!BUILD_TARGETS.has(subcommand)) return null;\n return `nx ${subcommand} with no project runs that target everywhere`;\n }\n\n private vitestShape(args: readonly string[]): string | null {\n let rest = args;\n if (rest.length > 0 && VITEST_VERBS.has(rest[0])) rest = rest.slice(1);\n for (let i = 0; i < rest.length; i++) {\n const word = rest[i];\n if (!word.startsWith('-')) return null; // a path filter — narrowed\n if (VITEST_VALUE_FLAGS.has(word)) i++; // skip the flag's value\n }\n return 'vitest with no path filter runs every spec in the repo';\n }\n\n /**\n * Does this nx invocation run a COMPILE/TEST target? Targets come from `-t`/`--target`/`--targets`\n * in either spelling, comma-separated, plus (for `run-many <target>`) a second positional.\n *\n * An invocation naming NO target is not judged a build: nx errors on it, and guessing would block a\n * typo with a message about build scope. An invocation naming only regeneration targets\n * (`di-graph-generate`, `generate`) is deliberately allowed workspace-wide — see BUILD_TARGETS.\n */\n private buildsAnything(args: readonly string[], positional: readonly string[]): boolean {\n const targets: string[] = positional.slice(1);\n for (let i = 0; i < args.length; i++) {\n const word = args[i];\n const flag = ['-t', '--target', '--targets'].find(\n (f: string): boolean => word === f || word.startsWith(`${f}=`));\n if (flag === undefined) continue;\n const value = word === flag ? (args[i + 1] ?? '') : word.slice(flag.length + 1);\n targets.push(...value.split(','));\n }\n return targets.some((t: string): boolean => BUILD_TARGETS.has(t.trim()));\n }\n\n // `--base` / `--base=<x>` / `--base <x>` — the attached and detached spellings of one flag.\n private hasFlag(args: readonly string[], flag: string): boolean {\n return args.some((a: string): boolean => a === flag || a.startsWith(`${flag}=`));\n }\n}\n"]}
@@ -241,7 +241,6 @@ function gitFromSubdirBlock(command, tree) {
241
241
  ` ${(0, effective_tree_1.atRoot)(tree.root, command)}\n` +
242
242
  ` A leading \`cd <path> &&\` is ACCEPTED by the guards — it cannot change what the command\n` +
243
243
  ` does to the repo. (The webpieces guards evaluate the repo's git state at its root.)`;
244
- (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision('force-to-root', 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK', 'git/gh from subdir'));
245
244
  return new types_1.BlockedResult(report);
246
245
  }
247
246
  // The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder
@@ -251,7 +250,7 @@ function gitFromSubdirBlock(command, tree) {
251
250
  // webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape
252
251
  function logL0CureBypass(command, cwd) {
253
252
  const root = new rules_config_1.RepoRootFinder().resolveRepoRoot(cwd);
254
- (0, decision_log_1.logGuardDecision)(root, new decision_log_1.GuardDecision('-', 'Bash', command, (0, decision_log_1.branchForLog)(root), 'ALLOW', 'L0 cure bypass (always allowed)'));
253
+ (0, decision_log_1.logGuardDecision)(root, new decision_log_1.GuardDecision('-', 'Bash', command, (0, decision_log_1.branchForLog)(root), 'ALLOW', 'L0 cure bypass (always allowed)', '-', l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.MATRIX_L0));
255
254
  }
256
255
  /**
257
256
  * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to
@@ -293,7 +292,6 @@ function coordinatorInWorktreeBlock(command, tree, agent) {
293
292
  const report = new coordinator_worktree_1.CoordinatorWorktreeGuard().block(command, tree, agent);
294
293
  if (report === null)
295
294
  return null;
296
- (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision('coordinator-in-worktree', 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK', 'coordinator working inside a linked worktree'));
297
295
  return new types_1.BlockedResult(report);
298
296
  }
299
297
  // Where the command lands in L1's five dimensions (K/A/R/G/P). The ONE place the runner's view of the
@@ -320,23 +318,45 @@ function l1Classify(command, tree, agent) {
320
318
  // reason FROM the resolved tree. Classifying it would mean asking L1_ROWS a question whose answer the
321
319
  // classification itself depends on. It is the same shape as L2's `bareCheckoutOfMain` pre-stage.
322
320
  //
323
- // KNOWN GAP, stated rather than discovered later: that makes it an L1 block with NO row in L1_ROWS, so
324
- // guards/L1-location.md does not describe it the exact drift this table exists to prevent, arriving
325
- // while the table was being built. It needs a row (its "state" would be a command shape, the way L2
326
- // row 2 carries one), which changes the rendered doc and so belongs in its own change, not in a merge
327
- // resolution.
321
+ // It is therefore ROW 0 "pre-stage, decided from command text" rather than a row among 1-6. That
322
+ // keeps it IN the table (the drift this table exists to prevent) without pretending it is classified
323
+ // over the same five dimensions, which is the thing that cannot be true. `L1_PRESTAGE_ROW` is the
324
+ // number the doc prints and the number `row=` logs, so the two still join.
325
+ //
326
+ // THIS FUNCTION IS THE ONE PLACE L1 REPORTS. It is the only scope holding the classification, the
327
+ // matched row, the resolved tree and the agent at once — so logging here, BEFORE the early return,
328
+ // is what finally records the outcomes that were previously invisible: the exempt row and the three
329
+ // hand-downs wrote nothing at all, which is why "L1 had no objection" could not be observed and
330
+ // "show me every L1 decision" had no answer. The three block helpers no longer log for themselves.
328
331
  // webpieces-disable no-function-outside-class -- sibling of gitFromSubdirBlock() and the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape
329
332
  function l1LocationBlock(command, tree, agent) {
330
333
  const misplacedCd = misplacedCdBlock(command, tree);
331
- if (misplacedCd !== null)
334
+ if (misplacedCd !== null) {
335
+ logL1(tree, command, 'BLOCK_AI_CURE', l1_rows_1.L1_PRESTAGE_ROW, 'cd-must-be-first', 'cd not leading/literal');
332
336
  return misplacedCd;
337
+ }
333
338
  const row = (0, l1_rows_1.firstMatchingL1Row)(l1Classify(command, tree, agent));
334
- if (row.blockId === null)
335
- return null; // exempt (row 1) or hand down to L2 (rows 2, 4, 6)
339
+ const rowNum = String(row.num);
340
+ if (row.blockId === null) {
341
+ // ALLOW_EXEMPT stops here; ALLOW means "no objection, handed down to L2". Recording the
342
+ // difference is the point — see Verdict. Neither is a claim that the call actually ran: the
343
+ // parallel L-1 hook may still have denied it.
344
+ logL1(tree, command, row.action.kind === 'exempt' ? 'ALLOW_EXEMPT' : 'ALLOW', rowNum, '-', row.why);
345
+ return null;
346
+ }
347
+ logL1(tree, command, 'BLOCK_AI_CURE', rowNum, row.blockId, row.why);
336
348
  if (row.blockId === 'coordinator-in-worktree')
337
349
  return coordinatorInWorktreeBlock(command, tree, agent);
338
350
  return gitFromSubdirBlock(command, tree);
339
351
  }
352
+ // One L1 line, into L1's OWN stream. `row` is the number the generated doc prints, so a reader who
353
+ // sees `row=5` can open guards/L1-location.md at row 5 and read the state, the cure and the use cases
354
+ // that row is supposed to cover.
355
+ // eslint-disable-next-line @typescript-eslint/max-params -- the six fields of one log line, and a class here would break this file's shape
356
+ // webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions
357
+ function logL1(tree, command, verdict, row, rule, why) {
358
+ (0, decision_log_1.logL1Decision)(tree.root, new decision_log_1.GuardDecision(rule, 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), verdict, why, '-', l0_fault_codes_1.L0_FAULT_NONE, new decision_log_1.MatrixRef('L1', row)));
359
+ }
340
360
  /**
341
361
  * ONE legal shape for relocating a command: `cd <literal path> && <work>`. Anything else — a `cd` after
342
362
  * another command or after a `VAR=…` assignment, or a `cd "$DIR"` the guard cannot expand — is refused
@@ -365,7 +385,6 @@ function misplacedCdBlock(command, tree) {
365
385
  ` Fix Option 2: drop the \`cd\` — ${tree.root} is where the command is judged anyway.\n` +
366
386
  ` Fix Option 3: split it — run the work in one call, the \`cd\` in another (a \`cd\` alone still\n` +
367
387
  ` moves nothing the guards judge; see EffectiveTree on why cwd cannot be assumed).`;
368
- (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision('cd-must-be-first', 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK', 'cd not leading/literal'));
369
388
  return new types_1.BlockedResult(report);
370
389
  }
371
390
  // webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape
@@ -391,7 +410,7 @@ function runBashInternal(command, cwd, mode, agent) {
391
410
  // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it
392
411
  // is the same project, so the guards run against THAT tree's branch and cache.
393
412
  if (tree.kind === 'foreign') {
394
- (0, decision_log_1.logGuardDecision)(workspaceRoot, new decision_log_1.GuardDecision('-', 'Bash', command, (0, decision_log_1.branchForLog)(workspaceRoot), 'ALLOW', 'foreign git repo (out of scope)'));
413
+ (0, decision_log_1.logGuardDecision)(workspaceRoot, new decision_log_1.GuardDecision('-', 'Bash', command, (0, decision_log_1.branchForLog)(workspaceRoot), 'ALLOW_EXEMPT', 'foreign git repo (out of scope)', '-', l0_fault_codes_1.L0_FAULT_NONE, new decision_log_1.MatrixRef('L1', '1')));
395
414
  return null;
396
415
  }
397
416
  // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective
@@ -421,12 +440,12 @@ function runBashInternal(command, cwd, mode, agent) {
421
440
  // focused (the whole point of the log is "why did/didn't a guard fire?"). Blocks are always
422
441
  // logged below.
423
442
  if (/\b(?:git|gh)\b/.test(command)) {
424
- (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision('-', 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'ALLOW', 'no bash-guard block'));
443
+ (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision('-', 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'ALLOW', 'no bash-guard block', '-', l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.MATRIX_L2));
425
444
  }
426
445
  return null;
427
446
  }
428
447
  const ruleNames = groups.map((g) => g.ruleName).join(',');
429
- (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision(ruleNames, 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK', 'bash-guard block'));
448
+ (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision(ruleNames, 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK_AI_CURE', 'bash-guard block', '-', l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.MATRIX_L2));
430
449
  const report = (0, report_1.formatReport)(commandLabel(command), groups, report_1.BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);
431
450
  return new types_1.BlockedResult(report);
432
451
  }