@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.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/adapters/claude-code-response.js +6 -2
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +13 -10
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/dev-hook-install.js +3 -3
- package/src/bin/dev-hook-install.js.map +1 -1
- package/src/bin/guarantee-root.js +23 -10
- package/src/bin/guarantee-root.js.map +1 -1
- package/src/bin/l0-allowlist.js +1 -1
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/setup.js +6 -3
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +0 -3
- package/src/bin/shim-audit-log.js +14 -13
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim.js +2 -2
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +75 -10
- package/src/core/decision-log.js +91 -28
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-fault-codes.d.ts +1 -1
- package/src/core/l0-fault-codes.js +1 -1
- package/src/core/l0-fault-codes.js.map +1 -1
- package/src/core/l1-doc.js +6 -0
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l1-rows.d.ts +13 -0
- package/src/core/l1-rows.js +24 -11
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/load-rules.js +2 -0
- package/src/core/load-rules.js.map +1 -1
- package/src/core/log-stream.d.ts +32 -18
- package/src/core/log-stream.js +33 -19
- package/src/core/log-stream.js.map +1 -1
- package/src/core/log-streams.d.ts +38 -0
- package/src/core/log-streams.js +45 -0
- package/src/core/log-streams.js.map +1 -0
- package/src/core/main-sync-log.d.ts +2 -2
- package/src/core/main-sync-log.js +22 -15
- package/src/core/main-sync-log.js.map +1 -1
- package/src/core/main-sync-refresh.d.ts +3 -3
- package/src/core/main-sync-refresh.js +4 -4
- package/src/core/main-sync-refresh.js.map +1 -1
- package/src/core/rejection-log.d.ts +1 -1
- package/src/core/rejection-log.js +13 -12
- package/src/core/rejection-log.js.map +1 -1
- package/src/core/rules/feature-branch-guard.d.ts +11 -0
- package/src/core/rules/feature-branch-guard.js +20 -5
- package/src/core/rules/feature-branch-guard.js.map +1 -1
- package/src/core/rules/index.js +1 -0
- package/src/core/rules/index.js.map +1 -1
- package/src/core/rules/merged-branch-bash-guard.d.ts +12 -1
- package/src/core/rules/merged-branch-bash-guard.js +21 -6
- package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
- package/src/core/rules/read-stale-guard.d.ts +11 -0
- package/src/core/rules/read-stale-guard.js +25 -10
- package/src/core/rules/read-stale-guard.js.map +1 -1
- package/src/core/rules/stale-main-bash-guard.d.ts +11 -0
- package/src/core/rules/stale-main-bash-guard.js +22 -7
- package/src/core/rules/stale-main-bash-guard.js.map +1 -1
- package/src/core/rules/whole-repo-build-guard.d.ts +88 -0
- package/src/core/rules/whole-repo-build-guard.js +176 -0
- package/src/core/rules/whole-repo-build-guard.js.map +1 -0
- package/src/core/rules/whole-repo-build-scan.d.ts +35 -0
- package/src/core/rules/whole-repo-build-scan.js +216 -0
- package/src/core/rules/whole-repo-build-scan.js.map +1 -0
- package/src/core/runner.js +34 -15
- package/src/core/runner.js.map +1 -1
- package/src/core/sync-main.d.ts +1 -1
- package/src/core/sync-main.js +2 -2
- package/src/core/sync-main.js.map +1 -1
- package/templates/ai-hook.sh +11 -8
- 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"]}
|
package/src/core/runner.js
CHANGED
|
@@ -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
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
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
|
-
|
|
335
|
-
|
|
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), '
|
|
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), '
|
|
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
|
}
|