@webpieces/ai-hook-rules 0.4.622 → 0.4.623
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 +29 -6
- package/package.json +2 -2
- package/src/adapters/hook-core.js +10 -3
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/hook-registration.d.ts +60 -14
- package/src/bin/hook-registration.js +123 -22
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/managed-env.d.ts +42 -0
- package/src/bin/managed-env.js +46 -0
- package/src/bin/managed-env.js.map +1 -0
- package/src/bin/setup.js +10 -0
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +6 -0
- package/src/bin/shim-deny-reason.js +80 -0
- package/src/bin/shim-deny-reason.js.map +1 -0
- package/src/bin/shim.d.ts +0 -1
- package/src/bin/shim.js +0 -54
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.d.ts +2 -2
- package/src/bin/upgrade-shim.js +95 -13
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/l0-matrix.js +14 -12
- package/src/core/l0-matrix.js.map +1 -1
- package/templates/claude-settings-hook.json +16 -3
package/src/bin/upgrade-shim.js
CHANGED
|
@@ -5,15 +5,19 @@ exports.runUpgradeShim = runUpgradeShim;
|
|
|
5
5
|
exports.main = main;
|
|
6
6
|
const tslib_1 = require("tslib");
|
|
7
7
|
const fs = tslib_1.__importStar(require("fs"));
|
|
8
|
+
const path = tslib_1.__importStar(require("path"));
|
|
8
9
|
const shim_1 = require("./shim");
|
|
9
10
|
const guarantee_root_1 = require("./guarantee-root");
|
|
10
11
|
const hook_registration_1 = require("./hook-registration");
|
|
12
|
+
const managed_env_1 = require("./managed-env");
|
|
11
13
|
const to_error_1 = require("../core/to-error");
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
15
|
// The `wp-upgrade-shim` entry point — the CURE for the managed-hook-surface self-guard (L0 fault S).
|
|
14
16
|
//
|
|
15
|
-
// WHAT IT REPAIRS, and why all
|
|
16
|
-
// and touch nothing else. That was correct while the installed surface WAS one file. It is
|
|
17
|
+
// WHAT IT REPAIRS, and why all four (2026-08-07, extended). This used to write EXACTLY ONE FILE,
|
|
18
|
+
// ai-hook.sh, and touch nothing else. That was correct while the installed surface WAS one file. It is
|
|
19
|
+
// now four (the name `wp-upgrade-shim` is older than the job and is NOT renamed — a rename with no
|
|
20
|
+
// functional change is a cost with no payer; the prose is what gets corrected):
|
|
17
21
|
//
|
|
18
22
|
// 1. .claude/webpieces/ai-hook.sh the guard shim, registered RELATIVE so each git tree runs
|
|
19
23
|
// its own release, its own binary and its own pin
|
|
@@ -23,6 +27,9 @@ const to_error_1 = require("../core/to-error");
|
|
|
23
27
|
// reference that is a NON-BLOCKING error, i.e. a SILENT
|
|
24
28
|
// UNGUARDED ALLOW
|
|
25
29
|
// 3. the .claude/settings.json registration itself
|
|
30
|
+
// 4. the .claude/settings.json `env` entry CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1, which pins the
|
|
31
|
+
// Bash cwd to the project root so the RELATIVE hooks in (1) always resolve — and, because settings
|
|
32
|
+
// `env` is inherited, pins it identically for every subagent (see hook-registration.ts)
|
|
26
33
|
//
|
|
27
34
|
// Leaving (2) and (3) out would have made the upgrade path silently useless: an upgrading consumer
|
|
28
35
|
// would take the new shim, KEEP the old two-absolute-hook registration, never receive guarantee-root.sh
|
|
@@ -55,8 +62,11 @@ function runUpgradeShim(cwd) {
|
|
|
55
62
|
// writeFileSync's mode only applies on create; force it on overwrite too (matches writeShim).
|
|
56
63
|
fs.chmodSync(target, 0o755);
|
|
57
64
|
(0, guarantee_root_1.writeGuaranteeRoot)(root);
|
|
58
|
-
const
|
|
59
|
-
reportRepairs(target, (0, guarantee_root_1.guaranteeRootPath)(root),
|
|
65
|
+
const repairs = (0, hook_registration_1.repairRegistrationAt)(root);
|
|
66
|
+
reportRepairs(target, (0, guarantee_root_1.guaranteeRootPath)(root), repairs);
|
|
67
|
+
// ADVISORY ONLY, and deliberately after the ✅ lines: it never touches the exit code (see
|
|
68
|
+
// reportTreeDivergence).
|
|
69
|
+
reportTreeDivergence(root);
|
|
60
70
|
return verifyRepaired(root);
|
|
61
71
|
}
|
|
62
72
|
catch (err) {
|
|
@@ -67,24 +77,96 @@ function runUpgradeShim(cwd) {
|
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
69
79
|
* Say what was actually done, per managed thing. The old single line ("regenerated the managed shim")
|
|
70
|
-
* would now be a lie by omission on the
|
|
80
|
+
* would now be a lie by omission on the three most important repairs — and an agent reading a cure's
|
|
71
81
|
* output is how it decides whether the cure worked.
|
|
82
|
+
*
|
|
83
|
+
* Each settings file reports the repairs IT needed, from the flags recorded before the rewrite. Printing
|
|
84
|
+
* "rewrote the hook registration" for a file whose hooks were already current and whose `env` entry was
|
|
85
|
+
* the only thing missing would be the same class of dishonesty one level down.
|
|
72
86
|
*/
|
|
73
87
|
// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module
|
|
74
|
-
function reportRepairs(shimFile, guaranteeFile,
|
|
88
|
+
function reportRepairs(shimFile, guaranteeFile, repairs) {
|
|
75
89
|
console.log(`✅ @webpieces: regenerated the managed shim at ${shimFile} — tool calls are re-armed.`);
|
|
76
90
|
console.log(`✅ @webpieces: regenerated the L-1 hook at ${guaranteeFile}.`);
|
|
77
|
-
if (
|
|
78
|
-
console.log(' .claude/settings.json hook registration already matches this release — no change.');
|
|
91
|
+
if (repairs.length === 0) {
|
|
92
|
+
console.log(' .claude/settings.json (hook registration + managed env) already matches this release — no change.');
|
|
79
93
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.log(`✅ @webpieces: rewrote the hook registration in ${
|
|
94
|
+
for (const repair of repairs) {
|
|
95
|
+
if (repair.registration) {
|
|
96
|
+
console.log(`✅ @webpieces: rewrote the hook registration in ${repair.settingsPath} to the three-hook form`);
|
|
83
97
|
console.log(' (L-1 absolute + the two guard hooks RELATIVE, so each git tree runs its own release).');
|
|
84
98
|
}
|
|
99
|
+
if (repair.env) {
|
|
100
|
+
console.log(`✅ @webpieces: set env.${managed_env_1.BASH_CWD_ENV_KEY}=${managed_env_1.BASH_CWD_ENV_VALUE} in ${repair.settingsPath}`);
|
|
101
|
+
console.log(' (pins the Bash cwd to the project root, so the RELATIVE guard hooks always resolve — for');
|
|
102
|
+
console.log(' this session and, because settings env is inherited, for every subagent it spawns).');
|
|
103
|
+
}
|
|
85
104
|
}
|
|
86
105
|
console.log(' These files are generated + committed by webpieces; do not revert or hand-edit them.');
|
|
87
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* WAS THE REPAIRED TREE THE TREE THE HOOKS LAUNCH FROM — the second way this cure can report success
|
|
109
|
+
* while changing nothing the session is actually governed by.
|
|
110
|
+
*
|
|
111
|
+
* H1 is registered ABSOLUTE, `sh "$CLAUDE_PROJECT_DIR/…"`, and `$CLAUDE_PROJECT_DIR` never moves off the
|
|
112
|
+
* PRIMARY clone (the two-tree straddle recorded in shim.ts, and the whole reason H2/H3 are relative
|
|
113
|
+
* while H1 is not). So repairing a LINKED WORKTREE leaves the running session still loading the
|
|
114
|
+
* PRIMARY's files, the PRIMARY's binary and the PRIMARY's pin. Four green lines, and the block does not
|
|
115
|
+
* lift. Nothing printed above is false — but the question the reader has ("will the block lift?") went
|
|
116
|
+
* unanswered, which is the same failure this file's header exists to prevent, one level out.
|
|
117
|
+
*
|
|
118
|
+
* THE PREDICATE IS TREE DIVERGENCE, NOT "am I a subagent". There is no runtime subagent marker in the
|
|
119
|
+
* hook environment to read, and divergence is the more accurate question anyway: a MAIN agent in a
|
|
120
|
+
* linked worktree HAS this problem (a subagent test would miss it), and a SUBAGENT in the same tree does
|
|
121
|
+
* NOT (a subagent test would cry wolf). Both paths are realpath'd before comparing — a worktree path can
|
|
122
|
+
* arrive symlinked, and /tmp vs /private/tmp on darwin is a live case in this repo's own specs.
|
|
123
|
+
*
|
|
124
|
+
* SILENT when `$CLAUDE_PROJECT_DIR` is unset: a plain CLI run outside Claude Code has no second tree to
|
|
125
|
+
* talk about. And ADVISORY always — it must never turn a verified repair into a failure, so it returns
|
|
126
|
+
* nothing and `verifyRepaired()`'s contract (non-zero only when a surface in THIS tree still differs) is
|
|
127
|
+
* untouched.
|
|
128
|
+
*/
|
|
129
|
+
// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module
|
|
130
|
+
function reportTreeDivergence(root) {
|
|
131
|
+
const projectDir = process.env['CLAUDE_PROJECT_DIR'];
|
|
132
|
+
if (projectDir === undefined || projectDir === '')
|
|
133
|
+
return;
|
|
134
|
+
if (sameTree(root, projectDir))
|
|
135
|
+
return;
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log('⚠️ @webpieces: the tree just repaired is NOT the tree the hooks launch from.');
|
|
138
|
+
console.log(` repaired: ${root}`);
|
|
139
|
+
console.log(` CLAUDE_PROJECT_DIR: ${projectDir}`);
|
|
140
|
+
console.log(' The hooks governing this session resolve through CLAUDE_PROJECT_DIR (H1 is registered');
|
|
141
|
+
console.log(' absolute), so this repair has not changed what is currently enforcing — it made THIS');
|
|
142
|
+
console.log(' tree correct for when its own branch is the one being judged, which is not wasted work.');
|
|
143
|
+
console.log(' To change what is enforcing NOW, run the same repair in the primary tree, and install');
|
|
144
|
+
console.log(' there too — the hooks execute the INSTALLED release, not this tree\'s source:');
|
|
145
|
+
console.log(` cd ${projectDir} && pnpm install && pnpm exec wp-upgrade-shim`);
|
|
146
|
+
console.log(' Repaired in BOTH trees is the aligned end state, and running it twice is safe.');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Do two paths name the same tree? realpath'd (symlinked worktrees, /tmp vs /private/tmp) and stripped
|
|
150
|
+
* of a trailing separator before comparing. A path that cannot be realpath'd falls back to `resolve`,
|
|
151
|
+
* so an absent CLAUDE_PROJECT_DIR directory reads as "different" rather than throwing inside a cure.
|
|
152
|
+
*/
|
|
153
|
+
// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module
|
|
154
|
+
function sameTree(a, b) {
|
|
155
|
+
return canonicalTree(a) === canonicalTree(b);
|
|
156
|
+
}
|
|
157
|
+
// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module
|
|
158
|
+
function canonicalTree(dir) {
|
|
159
|
+
// webpieces-disable no-unmanaged-exceptions -- realpath throws on a path that does not exist; the fallback IS the handling, and an advisory notice must never crash the cure it annotates.
|
|
160
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
161
|
+
try {
|
|
162
|
+
return path.resolve(fs.realpathSync(dir));
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const error = (0, to_error_1.toError)(err);
|
|
166
|
+
void error; // best-effort: an unresolvable path simply compares as itself
|
|
167
|
+
return path.resolve(dir);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
88
170
|
/**
|
|
89
171
|
* DID THE CURE ACTUALLY CURE IT — asked of the same predicate the guard asks, not of our own writes.
|
|
90
172
|
*
|
|
@@ -114,8 +196,8 @@ function verifyRepaired(root) {
|
|
|
114
196
|
*
|
|
115
197
|
* Up to and including 0.4.588 this module ENDED at the closing brace above. `pnpm exec wp-upgrade-shim`
|
|
116
198
|
* loaded it, defined two functions, and exited 0 having printed nothing and changed no file. Fault S
|
|
117
|
-
* names this command as OPTION 1, the only option that repairs all
|
|
118
|
-
* guard's own "THIS IS NOT A DEADLOCK" promise was false: OPTION 2 repairs one of
|
|
199
|
+
* names this command as OPTION 1, the only option that repairs all four managed surfaces, so the
|
|
200
|
+
* guard's own "THIS IS NOT A DEADLOCK" promise was false: OPTION 2 repairs one of four, and OPTION 1
|
|
119
201
|
* did nothing at all. Twenty-one unit tests missed it because every one of them called
|
|
120
202
|
* `runUpgradeShim()` as a FUNCTION — the defect lived entirely in what the module does when SPAWNED.
|
|
121
203
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade-shim.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/upgrade-shim.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport * as fs from 'fs';\n\nimport { renderShim, shimPath, findShimRoot } from './shim';\nimport { guaranteeRootPath, writeGuaranteeRoot } from './guarantee-root';\nimport { repairRegistrationAt, managedSurfaceDrift } from './hook-registration';\nimport { toError } from '../core/to-error';\n\n// ---------------------------------------------------------------------------\n// The `wp-upgrade-shim` entry point — the CURE for the managed-hook-surface self-guard (L0 fault S).\n//\n// WHAT IT REPAIRS, and why all three (2026-08-07). This used to write EXACTLY ONE FILE, ai-hook.sh,\n// and touch nothing else. That was correct while the installed surface WAS one file. It is now three:\n//\n// 1. .claude/webpieces/ai-hook.sh the guard shim, registered RELATIVE so each git tree runs\n// its own release, its own binary and its own pin\n// 2. .claude/webpieces/guarantee-root.sh the L-1 hook, registered ABSOLUTE, which refuses any `cd`\n// that would park the shell where the RELATIVE hooks cannot\n// launch — an unresolvable hook exits 127, and per the hooks\n// reference that is a NON-BLOCKING error, i.e. a SILENT\n// UNGUARDED ALLOW\n// 3. the .claude/settings.json registration itself\n//\n// Leaving (2) and (3) out would have made the upgrade path silently useless: an upgrading consumer\n// would take the new shim, KEEP the old two-absolute-hook registration, never receive guarantee-root.sh\n// at all, and L-1 would never activate — with the drift check reporting nothing, because nothing\n// validated settings.json. A cure that fixes one of three is worse than no cure, because it reports\n// success. This bin is already the sanctioned cure named in fault S's message and already on the L0\n// allowlist, so extending it keeps the existing self-healing path working end to end.\n//\n// Deliberately imports only ./shim, ./guarantee-root and ./hook-registration (fs + path) + toError,\n// exactly like install-entry: the whole job is to rewrite webpieces-managed files, which never needed\n// the rule engine, and it must stay runnable on a tree too broken to load it.\n// ---------------------------------------------------------------------------\nconst RED = '[31;1m';\nconst RESET = '[0m';\n\n// Returns the process exit code (0 = ok). Kept as a function (not top-level code) so it is unit-testable\n// without spawning node.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header), mirroring install-entry.ts. A DI-managed class would pull the container in and reintroduce the require-time crash this dependency-free path exists to survive.\nexport function runUpgradeShim(cwd: string): number {\n const root = findShimRoot(cwd);\n if (root === null) {\n console.error(`${RED}🛑 @webpieces: no committed .claude/webpieces/ai-hook.sh found to regenerate.${RESET}`);\n console.error(' Run this from a repo that installs @webpieces/ai-hook-rules, or run the installer (pnpm wp-install-ai-hooks).');\n return 1;\n }\n const target = shimPath(root);\n // webpieces-disable no-unmanaged-exceptions -- bin entry chokepoint: turn an fs error into an actionable line + non-zero exit rather than a raw node trace; there is no caller above a bin to handle it.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode only applies on create; force it on overwrite too (matches writeShim).\n fs.chmodSync(target, 0o755);\n writeGuaranteeRoot(root);\n const rewired = repairRegistrationAt(root);\n reportRepairs(target, guaranteeRootPath(root), rewired);\n return verifyRepaired(root);\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`${RED}🛑 @webpieces: could not write under ${root}: ${error.message}${RESET}`);\n return 1;\n }\n}\n\n/**\n * Say what was actually done, per managed thing. The old single line (\"regenerated the managed shim\")\n * would now be a lie by omission on the two most important repairs — and an agent reading a cure's\n * output is how it decides whether the cure worked.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction reportRepairs(shimFile: string, guaranteeFile: string, rewired: readonly string[]): void {\n console.log(`✅ @webpieces: regenerated the managed shim at ${shimFile} — tool calls are re-armed.`);\n console.log(`✅ @webpieces: regenerated the L-1 hook at ${guaranteeFile}.`);\n if (rewired.length === 0) {\n console.log(' .claude/settings.json hook registration already matches this release — no change.');\n } else {\n for (const file of rewired) {\n console.log(`✅ @webpieces: rewrote the hook registration in ${file} to the three-hook form`);\n console.log(' (L-1 absolute + the two guard hooks RELATIVE, so each git tree runs its own release).');\n }\n }\n console.log(' These files are generated + committed by webpieces; do not revert or hand-edit them.');\n}\n\n/**\n * DID THE CURE ACTUALLY CURE IT — asked of the same predicate the guard asks, not of our own writes.\n *\n * A cure that cannot fail loudly is worse than no cure. This bin used to print three ✅ lines and\n * return 0 the moment `writeFileSync` did not throw, which asserts only \"the bytes we chose were\n * written\", never \"the surface the guard measures now agrees\". Fault S blocks EVERY tool call, so the\n * one thing a blocked agent must be able to trust is whether the block will lift — and a success line\n * that is not backed by the guard's own check is exactly the false certainty that leaves it retrying a\n * cure that cannot work. So re-run `managedSurfaceDrift()`, the very function `enforceCommittedShim()`\n * calls, and return NON-ZERO naming whatever still differs.\n *\n * Measured against `root` (the tree we just repaired), not `governingShimRoot()` (the tree the running\n * binary came from). Those differ when the cure is run across trees, and the honest claim here is about\n * the files this invocation wrote.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction verifyRepaired(root: string): number {\n const stillDrifted = managedSurfaceDrift(root);\n if (stillDrifted.length === 0) return 0;\n console.error(`${RED}🛑 @webpieces: the repair ran but ${stillDrifted.length} managed surface(s) STILL differ: ${stillDrifted.join(', ')}.${RESET}`);\n console.error(` The guard will keep blocking. This is a webpieces bug or an unwritable tree under ${root} - do not retry this command in a loop; report it with the list above.`);\n return 1;\n}\n\n/**\n * THE PROCESS ENTRY POINT — the thing whose absence made this whole bin a lie.\n *\n * Up to and including 0.4.588 this module ENDED at the closing brace above. `pnpm exec wp-upgrade-shim`\n * loaded it, defined two functions, and exited 0 having printed nothing and changed no file. Fault S\n * names this command as OPTION 1, the only option that repairs all three managed surfaces, so the\n * guard's own \"THIS IS NOT A DEADLOCK\" promise was false: OPTION 2 repairs one of three, and OPTION 1\n * did nothing at all. Twenty-one unit tests missed it because every one of them called\n * `runUpgradeShim()` as a FUNCTION — the defect lived entirely in what the module does when SPAWNED.\n *\n * `runMain` from @webpieces/rules-config is the repo-wide wrapper and is deliberately NOT used here:\n * this bin must load with fs+path only (see the header) so it still runs on the broken tree it exists\n * to repair. `main()` is the sanctioned exit site instead, and `bin-process-entry.spec.ts` spawns\n * this file as a process so a future refactor cannot silently drop the launcher again.\n */\n// webpieces-disable no-function-outside-class -- bin entry point in this deliberately dependency-free module; see header\nexport function main(): void {\n process.exit(runUpgradeShim(process.cwd()));\n}\n\nif (require.main === module) {\n main();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"upgrade-shim.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/upgrade-shim.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { renderShim, shimPath, findShimRoot } from './shim';\nimport { guaranteeRootPath, writeGuaranteeRoot } from './guarantee-root';\nimport { repairRegistrationAt, managedSurfaceDrift, SettingsRepair } from './hook-registration';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { toError } from '../core/to-error';\n\n// ---------------------------------------------------------------------------\n// The `wp-upgrade-shim` entry point — the CURE for the managed-hook-surface self-guard (L0 fault S).\n//\n// WHAT IT REPAIRS, and why all four (2026-08-07, extended). This used to write EXACTLY ONE FILE,\n// ai-hook.sh, and touch nothing else. That was correct while the installed surface WAS one file. It is\n// now four (the name `wp-upgrade-shim` is older than the job and is NOT renamed — a rename with no\n// functional change is a cost with no payer; the prose is what gets corrected):\n//\n// 1. .claude/webpieces/ai-hook.sh the guard shim, registered RELATIVE so each git tree runs\n// its own release, its own binary and its own pin\n// 2. .claude/webpieces/guarantee-root.sh the L-1 hook, registered ABSOLUTE, which refuses any `cd`\n// that would park the shell where the RELATIVE hooks cannot\n// launch — an unresolvable hook exits 127, and per the hooks\n// reference that is a NON-BLOCKING error, i.e. a SILENT\n// UNGUARDED ALLOW\n// 3. the .claude/settings.json registration itself\n// 4. the .claude/settings.json `env` entry CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1, which pins the\n// Bash cwd to the project root so the RELATIVE hooks in (1) always resolve — and, because settings\n// `env` is inherited, pins it identically for every subagent (see hook-registration.ts)\n//\n// Leaving (2) and (3) out would have made the upgrade path silently useless: an upgrading consumer\n// would take the new shim, KEEP the old two-absolute-hook registration, never receive guarantee-root.sh\n// at all, and L-1 would never activate — with the drift check reporting nothing, because nothing\n// validated settings.json. A cure that fixes one of three is worse than no cure, because it reports\n// success. This bin is already the sanctioned cure named in fault S's message and already on the L0\n// allowlist, so extending it keeps the existing self-healing path working end to end.\n//\n// Deliberately imports only ./shim, ./guarantee-root and ./hook-registration (fs + path) + toError,\n// exactly like install-entry: the whole job is to rewrite webpieces-managed files, which never needed\n// the rule engine, and it must stay runnable on a tree too broken to load it.\n// ---------------------------------------------------------------------------\nconst RED = '[31;1m';\nconst RESET = '[0m';\n\n// Returns the process exit code (0 = ok). Kept as a function (not top-level code) so it is unit-testable\n// without spawning node.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header), mirroring install-entry.ts. A DI-managed class would pull the container in and reintroduce the require-time crash this dependency-free path exists to survive.\nexport function runUpgradeShim(cwd: string): number {\n const root = findShimRoot(cwd);\n if (root === null) {\n console.error(`${RED}🛑 @webpieces: no committed .claude/webpieces/ai-hook.sh found to regenerate.${RESET}`);\n console.error(' Run this from a repo that installs @webpieces/ai-hook-rules, or run the installer (pnpm wp-install-ai-hooks).');\n return 1;\n }\n const target = shimPath(root);\n // webpieces-disable no-unmanaged-exceptions -- bin entry chokepoint: turn an fs error into an actionable line + non-zero exit rather than a raw node trace; there is no caller above a bin to handle it.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode only applies on create; force it on overwrite too (matches writeShim).\n fs.chmodSync(target, 0o755);\n writeGuaranteeRoot(root);\n const repairs = repairRegistrationAt(root);\n reportRepairs(target, guaranteeRootPath(root), repairs);\n // ADVISORY ONLY, and deliberately after the ✅ lines: it never touches the exit code (see\n // reportTreeDivergence).\n reportTreeDivergence(root);\n return verifyRepaired(root);\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`${RED}🛑 @webpieces: could not write under ${root}: ${error.message}${RESET}`);\n return 1;\n }\n}\n\n/**\n * Say what was actually done, per managed thing. The old single line (\"regenerated the managed shim\")\n * would now be a lie by omission on the three most important repairs — and an agent reading a cure's\n * output is how it decides whether the cure worked.\n *\n * Each settings file reports the repairs IT needed, from the flags recorded before the rewrite. Printing\n * \"rewrote the hook registration\" for a file whose hooks were already current and whose `env` entry was\n * the only thing missing would be the same class of dishonesty one level down.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction reportRepairs(shimFile: string, guaranteeFile: string, repairs: readonly SettingsRepair[]): void {\n console.log(`✅ @webpieces: regenerated the managed shim at ${shimFile} — tool calls are re-armed.`);\n console.log(`✅ @webpieces: regenerated the L-1 hook at ${guaranteeFile}.`);\n if (repairs.length === 0) {\n console.log(' .claude/settings.json (hook registration + managed env) already matches this release — no change.');\n }\n for (const repair of repairs) {\n if (repair.registration) {\n console.log(`✅ @webpieces: rewrote the hook registration in ${repair.settingsPath} to the three-hook form`);\n console.log(' (L-1 absolute + the two guard hooks RELATIVE, so each git tree runs its own release).');\n }\n if (repair.env) {\n console.log(`✅ @webpieces: set env.${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} in ${repair.settingsPath}`);\n console.log(' (pins the Bash cwd to the project root, so the RELATIVE guard hooks always resolve — for');\n console.log(' this session and, because settings env is inherited, for every subagent it spawns).');\n }\n }\n console.log(' These files are generated + committed by webpieces; do not revert or hand-edit them.');\n}\n\n/**\n * WAS THE REPAIRED TREE THE TREE THE HOOKS LAUNCH FROM — the second way this cure can report success\n * while changing nothing the session is actually governed by.\n *\n * H1 is registered ABSOLUTE, `sh \"$CLAUDE_PROJECT_DIR/…\"`, and `$CLAUDE_PROJECT_DIR` never moves off the\n * PRIMARY clone (the two-tree straddle recorded in shim.ts, and the whole reason H2/H3 are relative\n * while H1 is not). So repairing a LINKED WORKTREE leaves the running session still loading the\n * PRIMARY's files, the PRIMARY's binary and the PRIMARY's pin. Four green lines, and the block does not\n * lift. Nothing printed above is false — but the question the reader has (\"will the block lift?\") went\n * unanswered, which is the same failure this file's header exists to prevent, one level out.\n *\n * THE PREDICATE IS TREE DIVERGENCE, NOT \"am I a subagent\". There is no runtime subagent marker in the\n * hook environment to read, and divergence is the more accurate question anyway: a MAIN agent in a\n * linked worktree HAS this problem (a subagent test would miss it), and a SUBAGENT in the same tree does\n * NOT (a subagent test would cry wolf). Both paths are realpath'd before comparing — a worktree path can\n * arrive symlinked, and /tmp vs /private/tmp on darwin is a live case in this repo's own specs.\n *\n * SILENT when `$CLAUDE_PROJECT_DIR` is unset: a plain CLI run outside Claude Code has no second tree to\n * talk about. And ADVISORY always — it must never turn a verified repair into a failure, so it returns\n * nothing and `verifyRepaired()`'s contract (non-zero only when a surface in THIS tree still differs) is\n * untouched.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction reportTreeDivergence(root: string): void {\n const projectDir = process.env['CLAUDE_PROJECT_DIR'];\n if (projectDir === undefined || projectDir === '') return;\n if (sameTree(root, projectDir)) return;\n console.log('');\n console.log('⚠️ @webpieces: the tree just repaired is NOT the tree the hooks launch from.');\n console.log(` repaired: ${root}`);\n console.log(` CLAUDE_PROJECT_DIR: ${projectDir}`);\n console.log(' The hooks governing this session resolve through CLAUDE_PROJECT_DIR (H1 is registered');\n console.log(' absolute), so this repair has not changed what is currently enforcing — it made THIS');\n console.log(' tree correct for when its own branch is the one being judged, which is not wasted work.');\n console.log(' To change what is enforcing NOW, run the same repair in the primary tree, and install');\n console.log(' there too — the hooks execute the INSTALLED release, not this tree\\'s source:');\n console.log(` cd ${projectDir} && pnpm install && pnpm exec wp-upgrade-shim`);\n console.log(' Repaired in BOTH trees is the aligned end state, and running it twice is safe.');\n}\n\n/**\n * Do two paths name the same tree? realpath'd (symlinked worktrees, /tmp vs /private/tmp) and stripped\n * of a trailing separator before comparing. A path that cannot be realpath'd falls back to `resolve`,\n * so an absent CLAUDE_PROJECT_DIR directory reads as \"different\" rather than throwing inside a cure.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction sameTree(a: string, b: string): boolean {\n return canonicalTree(a) === canonicalTree(b);\n}\n\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction canonicalTree(dir: string): string {\n // webpieces-disable no-unmanaged-exceptions -- realpath throws on a path that does not exist; the fallback IS the handling, and an advisory notice must never crash the cure it annotates.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return path.resolve(fs.realpathSync(dir));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: an unresolvable path simply compares as itself\n return path.resolve(dir);\n }\n}\n\n/**\n * DID THE CURE ACTUALLY CURE IT — asked of the same predicate the guard asks, not of our own writes.\n *\n * A cure that cannot fail loudly is worse than no cure. This bin used to print three ✅ lines and\n * return 0 the moment `writeFileSync` did not throw, which asserts only \"the bytes we chose were\n * written\", never \"the surface the guard measures now agrees\". Fault S blocks EVERY tool call, so the\n * one thing a blocked agent must be able to trust is whether the block will lift — and a success line\n * that is not backed by the guard's own check is exactly the false certainty that leaves it retrying a\n * cure that cannot work. So re-run `managedSurfaceDrift()`, the very function `enforceCommittedShim()`\n * calls, and return NON-ZERO naming whatever still differs.\n *\n * Measured against `root` (the tree we just repaired), not `governingShimRoot()` (the tree the running\n * binary came from). Those differ when the cure is run across trees, and the honest claim here is about\n * the files this invocation wrote.\n */\n// webpieces-disable no-function-outside-class -- sibling of runUpgradeShim in this deliberately dependency-free bin module\nfunction verifyRepaired(root: string): number {\n const stillDrifted = managedSurfaceDrift(root);\n if (stillDrifted.length === 0) return 0;\n console.error(`${RED}🛑 @webpieces: the repair ran but ${stillDrifted.length} managed surface(s) STILL differ: ${stillDrifted.join(', ')}.${RESET}`);\n console.error(` The guard will keep blocking. This is a webpieces bug or an unwritable tree under ${root} - do not retry this command in a loop; report it with the list above.`);\n return 1;\n}\n\n/**\n * THE PROCESS ENTRY POINT — the thing whose absence made this whole bin a lie.\n *\n * Up to and including 0.4.588 this module ENDED at the closing brace above. `pnpm exec wp-upgrade-shim`\n * loaded it, defined two functions, and exited 0 having printed nothing and changed no file. Fault S\n * names this command as OPTION 1, the only option that repairs all four managed surfaces, so the\n * guard's own \"THIS IS NOT A DEADLOCK\" promise was false: OPTION 2 repairs one of four, and OPTION 1\n * did nothing at all. Twenty-one unit tests missed it because every one of them called\n * `runUpgradeShim()` as a FUNCTION — the defect lived entirely in what the module does when SPAWNED.\n *\n * `runMain` from @webpieces/rules-config is the repo-wide wrapper and is deliberately NOT used here:\n * this bin must load with fs+path only (see the header) so it still runs on the broken tree it exists\n * to repair. `main()` is the sanctioned exit site instead, and `bin-process-entry.spec.ts` spawns\n * this file as a process so a future refactor cannot silently drop the launcher again.\n */\n// webpieces-disable no-function-outside-class -- bin entry point in this deliberately dependency-free module; see header\nexport function main(): void {\n process.exit(runUpgradeShim(process.cwd()));\n}\n\nif (require.main === module) {\n main();\n}\n"]}
|
package/src/core/l0-matrix.js
CHANGED
|
@@ -8,6 +8,7 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
8
8
|
const shim_1 = require("../bin/shim");
|
|
9
9
|
const guarantee_root_1 = require("../bin/guarantee-root");
|
|
10
10
|
const hook_registration_1 = require("../bin/hook-registration");
|
|
11
|
+
const shim_deny_reason_1 = require("../bin/shim-deny-reason");
|
|
11
12
|
const l0_fault_codes_1 = require("./l0-fault-codes");
|
|
12
13
|
const to_error_1 = require("./to-error");
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
@@ -157,24 +158,25 @@ exports.L0_FAULTS = [
|
|
|
157
158
|
+ '"Lockfile is up to date" and leaves the tree exactly as broken as it found it')], (0, shim_1.renderShim)()),
|
|
158
159
|
new L0Fault(l0_fault_codes_1.L0_FAULT_BIN_BROKEN, 'guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)', 'sh, before the bin runs', 'sh', [bashCure(shim_1.RECOVERY_CMD, true, 'this fault fires at all — a BARE pnpm install SKIPS the corrupt package, because pnpm sees '
|
|
159
160
|
+ 'the right version on disk and considers it installed; only the delete forces a rewrite')], (0, shim_1.renderShim)()),
|
|
160
|
-
// S covers the WHOLE managed hook surface, not just the shim: the two committed .sh files
|
|
161
|
-
// .claude/settings.json entries that register them
|
|
161
|
+
// S covers the WHOLE managed hook surface, not just the shim: the two committed .sh files, the
|
|
162
|
+
// .claude/settings.json entries that register them AND the managed `env` entry that pins the Bash
|
|
163
|
+
// cwd so those RELATIVE entries always resolve. They only work as a set — a settings file left
|
|
162
164
|
// on the old two-absolute-hook form disables the L-1 hook and re-pins every worktree to the
|
|
163
165
|
// primary's release — and nothing validated the registration at all before it joined this fault.
|
|
164
|
-
new L0Fault(l0_fault_codes_1.L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file
|
|
165
|
-
// wp-upgrade-shim leads because it is now the ONLY cure that repairs all
|
|
166
|
-
// still surgical: it rewrites the two .sh files
|
|
167
|
-
// and it imports only fs/path so it runs on a tree too broken to load
|
|
168
|
-
// INSTALLER is deliberately NOT a cure here: it also migrates the config
|
|
169
|
-
// target twice, which hangs a non-interactive agent.
|
|
170
|
-
bashCure(shim_1.UPGRADE_SHIM_CMD, true, 'this fault fires at all — it is the only cure that repairs all
|
|
171
|
-
+ '(both .sh files
|
|
172
|
-
+ 'installed @webpieces/ai-hook-rules 0.4.408 or newer'),
|
|
166
|
+
new L0Fault(l0_fault_codes_1.L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file, the .claude/settings.json registration or its managed env entry does not match this release', 'the guard bin', 'JS', [
|
|
167
|
+
// wp-upgrade-shim leads because it is now the ONLY cure that repairs all four, and it is
|
|
168
|
+
// still surgical: it rewrites the two .sh files, the registration and the managed env entry
|
|
169
|
+
// and touches no config, and it imports only fs/path so it runs on a tree too broken to load
|
|
170
|
+
// the rule engine. The INSTALLER is deliberately NOT a cure here: it also migrates the config
|
|
171
|
+
// and prompts for a target twice, which hangs a non-interactive agent.
|
|
172
|
+
bashCure(shim_1.UPGRADE_SHIM_CMD, true, 'this fault fires at all — it is the only cure that repairs all four managed things '
|
|
173
|
+
+ '(both .sh files, the settings.json registration and its managed env entry) and it '
|
|
174
|
+
+ 'touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer'),
|
|
173
175
|
// 2026-07-21: the version gap below caused a real "command not found" deadlock.
|
|
174
176
|
bashCure(shim_1.RESTORE_SHIM_CMD, false, 'the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does '
|
|
175
177
|
+ 'not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade '
|
|
176
178
|
+ '@webpieces afterwards and run Option 1 to finish'),
|
|
177
|
-
], (0,
|
|
179
|
+
], (0, shim_deny_reason_1.shimStaleDenyReason)('', '', [shim_1.SHIM_MARKER, guarantee_root_1.GUARANTEE_ROOT_MARKER, hook_registration_1.REGISTRATION_SURFACE, hook_registration_1.ENV_SURFACE], false)),
|
|
178
180
|
new L0Fault(l0_fault_codes_1.L0_FAULT_CONFIG_MISSING, `${rules_config_1.CONFIG_FILENAME} missing`, 'the guard bin', 'JS', [
|
|
179
181
|
CONFIG_WRITE_CURE,
|
|
180
182
|
// Kept, but demoted: it seeds the file and then PROMPTS twice for a hook target.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l0-matrix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-matrix.ts"],"names":[],"mappings":"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","sourcesContent":["import { CONFIG_FILENAME, writeTemplate } from '@webpieces/rules-config';\n\nimport {\n ADD_HOOK_PKG_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call, L0_ALLOWLIST, RECOVERY_CMD,\n RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, renderShim, shimStaleDenyReason,\n} from '../bin/shim';\nimport { GUARANTEE_ROOT_MARKER } from '../bin/guarantee-root';\nimport { REGISTRATION_SURFACE } from '../bin/hook-registration';\nimport {\n L0_FAULT_BIN_BROKEN, L0_FAULT_BIN_MISSING, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n L0_FAULT_DRIFT, L0_FAULT_SHIM_STALE, L0_FAULT_UNDECLARED,\n} from './l0-fault-codes';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// L0 — the TOOLING-INTEGRITY layer, as data.\n//\n// L0 is the outermost guard: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its faults are\n// enumerated in L0_FAULTS below and — drawn as a decision matrix — have NO genuine second dimension:\n//\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n//\n// so the only thing that varies per fault is the MESSAGE. This module holds the fault table and\n// renders it, together with L0_ALLOWLIST (../bin/shim), into webpieces.guard-matrix.md — the doc the\n// deny messages point the AI at. Doc and code come from the SAME arrays, so they cannot drift.\n// ---------------------------------------------------------------------------\n\n/**\n * The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,\n * and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.\n *\n * That generated doc is the AUTHORITY for the fault table and the allowlist (same arrays, cannot\n * drift). guards/L0-tooling.md is the hand-written companion: it adds L0's evaluation\n * ORDER, the use cases and the known gaps, and it documents L1, none of which are rendered from code.\n * Change L0_FAULTS or L0_ALLOWLIST and the generated doc follows automatically — guards/L0-tooling.md does\n * not, so update it in the same PR.\n */\nexport const GUARD_MATRIX_DOC = 'webpieces.guard-matrix.md';\n\n/**\n * One CURE for a fault: the exact call, plus the `mention` that must appear in that fault's deny text.\n *\n * Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny\n * message must actually name it. That pairing is the anti-deadlock invariant — a message that\n * prescribes a command the allowlist rejects is exactly the shape of the three deadlocks CLAUDE.md\n * records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.\n */\nexport class L0Cure {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly mention: string,\n readonly call: L0Call,\n /**\n * The one to reach for first when a fault has several. Exactly one cure per fault carries it,\n * so the rendered Fix section never asks the reader to choose between equals.\n */\n readonly preferred: boolean,\n /**\n * WHEN to pick this cure over its siblings — the sentence that makes a list of commands\n * actionable instead of a menu (\"when the PIN is the stale side\", not \"an alternative\").\n */\n readonly discriminator: string,\n ) {}\n\n /** A Bash cure renders as a literal command; a tool-shaped one renders as the edit it stands for. */\n isCommand(): boolean {\n return this.call.toolName === 'Bash';\n }\n}\n\n/** One L0 fault. Data-only → a class, per CLAUDE.md. */\nexport class L0Fault {\n constructor(\n readonly code: string,\n readonly name: string,\n readonly detectedBy: string,\n readonly enforcedIn: string,\n readonly cures: readonly L0Cure[],\n /**\n * The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for\n * D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —\n * the same bytes the consumer runs, which is what the mention assertion needs to search.\n */\n readonly denyText: string,\n ) {}\n}\n\n// The deny for fault C, and the ONLY message L0 has for a repo with no webpieces.config.json at all.\n// Moved here from runner.ts so the fault table and the runner cannot state different cures.\n//\n// It used to name `./node_modules/.bin/wp-setup-ai-hooks` — a bin that HAS NOT EXISTED since it was\n// renamed to wp-install-ai-hooks. So the one command this deny prescribed was (a) not installable and\n// (b) not on the L0 allowlist in that spelling, i.e. the AI was handed a cure it could neither run nor\n// get past the guard. Now it names the installer that actually seeds the config AND is entry 8 of the\n// allowlist, and it says out loud that writing the config yourself is allowed through.\n//\n// ORDERING (2026-08-02): writing the file yourself now LEADS. The bare installer used to be OPTION 1,\n// but it seeds the config and then PROMPTS twice for a hook target, which hangs a non-interactive\n// agent. Writing the file is the one cure that always works, and it is the same cure every other config\n// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every\n// error at once, so the write/validate loop converges in a couple of passes.\nexport const CONFIG_MISSING_REPORT =\n `${CONFIG_FILENAME} not found — the webpieces guards cannot run without it, so every other tool call is blocked.\\n` +\n 'THIS IS NOT A DEADLOCK: both options below are explicitly allowed through while this guard is up.\\n' +\n `OPTION 1 (preferred — it needs no other tool and it never prompts) - create ${CONFIG_FILENAME}\\n` +\n 'yourself: any Read, and any Write/Edit whose target is that file, is always allowed through, so\\n' +\n 'you can inspect the repo and write it. The validator reports EVERY missing/invalid entry at once\\n' +\n '(each with the snippet to paste), so a minimal first draft converges in about two passes.\\n' +\n 'OPTION 2 (pick this ONLY at an interactive terminal where you can answer its two prompts) - run\\n' +\n 'EXACTLY this command to seed the config: `pnpm exec wp-install-ai-hooks`. It goes on to wire the\\n' +\n 'Claude Code hooks and asks for a target twice, which hangs a non-interactive session.\\n' +\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.';\n\n// The first line of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail). Kept here so the fault table quotes the same text the runner emits.\nexport const CONFIG_OUT_OF_SYNC_HEADER =\n `${CONFIG_FILENAME} is out of sync — new built-in rules are present that have no entry in ${CONFIG_FILENAME}.`;\n\n// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation\n// invariant in guards/L0-tooling.md: every config problem cures to \"make the file right\", the validator\n// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits\n// this edit unconditionally. (That section is the authority — do not restate its reasoning here.)\nconst CONFIG_WRITE_CURE = new L0Cure(\n CONFIG_FILENAME, new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`), true,\n 'this fault fires at all — it is the only cure that needs no other tool, and it is never denied',\n);\n\n// Cure calls are spelled exactly as the deny messages spell them, so the mention assertion is a real\n// string search rather than a paraphrase.\n// webpieces-disable no-function-outside-class -- pure constructor helper for the L0_FAULTS literal below, in this data module\nfunction bashCure(command: string, preferred: boolean, discriminator: string): L0Cure {\n return new L0Cure(command, new L0Call('Bash', command, ''), preferred, discriminator);\n}\n\n/**\n * THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a\n * stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside\n * the bin, in JS. One model, two enforcement points.\n */\nexport const L0_FAULTS: readonly L0Fault[] = [\n new L0Fault(L0_FAULT_DRIFT, 'version drift — root package.json pin != installed version',\n 'sh, before the bin runs', 'sh',\n [\n // `pnpm install` clears D in BOTH directions — it makes installed == pin by definition — so\n // it is always the preferred cure. The direction only decides whether the PIN is the version\n // you WANT, which is what the second cure is for.\n bashCure('pnpm install', true,\n 'node_modules is OLDER than the pin, OR you are on a feature branch and want YOUR '\n + 'branch pin (usually the case) — it always clears the drift'),\n bashCure('git pull', false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'pull first and install second; a bare install would downgrade you'),\n ], renderShim()),\n new L0Fault(L0_FAULT_BIN_MISSING, 'guard bin missing (fresh clone / new worktree / package removed)',\n 'sh, before the bin runs', 'sh',\n [bashCure('pnpm install', true,\n 'this fault fires at all — nothing is installed in THIS tree, and a new git worktree '\n + 'copies no node_modules')],\n renderShim()),\n // U is X with the ONE input that inverts X's cure, which is why it is a separate fault and not a\n // sentence inside X's message: when nothing declares the package, `pnpm install` is not a weaker fix,\n // it is a PROVABLE no-op, and an agent that trusts the X text will run it until it gives up. See the\n // ADD_HOOK_PKG entry in l0-allowlist.ts for the incident.\n new L0Fault(L0_FAULT_UNDECLARED, `guard bin missing AND ${HOOK_PKG} is not declared in package.json`,\n 'sh, before the bin runs', 'sh',\n [bashCure(ADD_HOOK_PKG_CMD, true,\n 'this fault fires at all — package.json asks for nothing, so pnpm install reports '\n + '\"Lockfile is up to date\" and leaves the tree exactly as broken as it found it')],\n renderShim()),\n new L0Fault(L0_FAULT_BIN_BROKEN, 'guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)',\n 'sh, before the bin runs', 'sh',\n [bashCure(RECOVERY_CMD, true,\n 'this fault fires at all — a BARE pnpm install SKIPS the corrupt package, because pnpm sees '\n + 'the right version on disk and considers it installed; only the delete forces a rewrite')],\n renderShim()),\n // S covers the WHOLE managed hook surface, not just the shim: the two committed .sh files AND the\n // .claude/settings.json entries that register them. They only work as a set — a settings file left\n // on the old two-absolute-hook form disables the L-1 hook and re-pins every worktree to the\n // primary's release — and nothing validated the registration at all before it joined this fault.\n new L0Fault(L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file or the .claude/settings.json registration does not match this release',\n 'the guard bin', 'JS',\n [\n // wp-upgrade-shim leads because it is now the ONLY cure that repairs all three, and it is\n // still surgical: it rewrites the two .sh files and the registration and touches no config,\n // and it imports only fs/path so it runs on a tree too broken to load the rule engine. The\n // INSTALLER is deliberately NOT a cure here: it also migrates the config and prompts for a\n // target twice, which hangs a non-interactive agent.\n bashCure(UPGRADE_SHIM_CMD, true,\n 'this fault fires at all — it is the only cure that repairs all three managed things '\n + '(both .sh files and the settings.json registration) and it touches no config; needs '\n + 'installed @webpieces/ai-hook-rules 0.4.408 or newer'),\n // 2026-07-21: the version gap below caused a real \"command not found\" deadlock.\n bashCure(RESTORE_SHIM_CMD, false,\n 'the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does '\n + 'not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade '\n + '@webpieces afterwards and run Option 1 to finish'),\n ],\n shimStaleDenyReason('', '', [SHIM_MARKER, GUARANTEE_ROOT_MARKER, REGISTRATION_SURFACE])),\n new L0Fault(L0_FAULT_CONFIG_MISSING, `${CONFIG_FILENAME} missing`,\n 'the guard bin', 'JS',\n [\n CONFIG_WRITE_CURE,\n // Kept, but demoted: it seeds the file and then PROMPTS twice for a hook target.\n bashCure(INSTALL_HOOKS_CMD, false,\n 'you are at an INTERACTIVE terminal and can answer its two hook-target prompts'),\n ], CONFIG_MISSING_REPORT),\n new L0Fault(L0_FAULT_CONFIG_OUT_OF_SYNC, `a loaded rule has no ${CONFIG_FILENAME} key`,\n 'the guard bin', 'JS',\n [CONFIG_WRITE_CURE], CONFIG_OUT_OF_SYNC_HEADER),\n];\n\n/**\n * One fault's FIX section, rendered from its `cures` array — literal commands only, never prose.\n *\n * This is the half that used to live in hand-written docs and drift. The three fields of L0Cure map\n * onto the three things a blocked reader needs and nothing else: WHAT to type (the call), WHETHER it is\n * the default (preferred), and WHEN to pick a sibling instead (discriminator). A cure with no\n * discriminator would render as a menu of equals, which is how an agent picks the wrong one.\n */\n// webpieces-disable no-function-outside-class -- pure string builder for renderGuardMatrixDoc below, beside the arrays it reads\nfunction renderFixSection(fault: L0Fault): string[] {\n const options = fault.cures.map((cure: L0Cure, i: number): string => {\n // A Bash cure is the command verbatim; a tool-shaped one is the file it edits (allowlist entry 2).\n const literal = cure.isCommand() ? `\\`${cure.call.command}\\`` : `edit \\`${cure.mention}\\` yourself`;\n const label = cure.preferred ? `Option ${i + 1} (preferred)` : `Option ${i + 1}`;\n return `- **${label}**: ${literal} ← pick this when ${cure.discriminator}`;\n });\n return [`### \\`${fault.code}\\` — ${fault.name}`, '', ...options, ''];\n}\n\n/**\n * Render webpieces.guard-matrix.md from L0_FAULTS + L0_ALLOWLIST.\n *\n * A unit test locks the committed template byte-identical to this output, the same way\n * templates/ai-hook.sh is locked to renderShim(). That is what makes the doc assertable instead of\n * aspirational: the table in the doc IS the array the guard consults.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over the two exported arrays, beside them in this module\nexport function renderGuardMatrixDoc(): string {\n return [\n '# webpieces guard matrix — L0 (tooling integrity)',\n '',\n 'GENERATED from `L0_FAULTS` + `L0_ALLOWLIST` in `@webpieces/ai-hook-rules`. Do not hand-edit —',\n 'a unit test locks this file byte-identical to `renderGuardMatrixDoc()`, so the table below is',\n 'the array the guard actually consults, not a description of it.',\n '',\n 'L0 is the OUTERMOST guard layer. It blocks work while `node_modules`, the committed shim, or',\n '`webpieces.config.json` are in a state that makes every other guard untrustworthy. If you are',\n 'reading this, one of the faults below fired and named this file.',\n '',\n '## The faults',\n '',\n '| code | fault | detected by | enforced in |',\n '|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string => `| \\`${f.code}\\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),\n '',\n 'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',\n 'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',\n '',\n '## The fix, per fault',\n '',\n 'Every command below is rendered from that fault\\'s `cures` array and is asserted, by unit test,',\n 'to be accepted by `isAllowed()` — so nothing here can be a command the guard then rejects. Type',\n 'the option you pick EXACTLY as written and run nothing else on that line.',\n '',\n ...L0_FAULTS.flatMap(renderFixSection),\n ...renderMatrixAndAllowlist(),\n ].join('\\n');\n}\n\n/**\n * The second half of the doc: the three-row matrix and the ONE allowlist. Split out of\n * renderGuardMatrixDoc solely to keep it inside the method-line budget — the join order is what makes\n * the two halves one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- second half of renderGuardMatrixDoc's string, beside it in this module\nfunction renderMatrixAndAllowlist(): string[] {\n return [\n '## The matrix',\n '',\n 'L0 has NO genuine second dimension. Every branch reduces to one question:',\n '',\n '| # | fault | on the allowlist? | outcome |',\n '|---|---|---|---|',\n '| 1 | none | — | hand down to the next guard layer |',\n '| 2 | any | yes | PASS or ALLOW (see the entry) |',\n '| 3 | any | no | BLOCK — **only the message varies by fault** |',\n '',\n 'The tool is not a dimension either: \"any Read\" is an allowlist ENTRY, not a tool check.',\n '',\n '## The allowlist',\n '',\n 'ONE list, consulted identically by every fault. A cure that cannot help a given fault also',\n 'cannot hurt it, and gating each entry on a fault is what produced four real defects (a stale',\n 'shim that denied `pnpm install` and `git pull`; faults that denied every Read; a config fault',\n 'that denied `rm -rf node_modules && pnpm install` while allowing a bare `pnpm install`).',\n '',\n '| # | allowed | outcome |',\n '|---|---|---|',\n ...L0_ALLOWLIST.map((e: L0AllowEntry, i: number): string => `| ${i + 1} | ${e.label} | ${e.kind.toUpperCase()} |`),\n '',\n '- **PASS** — L0 has no objection; the call falls THROUGH so the downstream guards still judge it.',\n '- **ALLOW** — terminal; bypasses everything, because a cure must stay reachable even when a',\n ' downstream guard would block it.',\n '',\n 'Every Bash entry is anchored to the WHOLE command. A leading `cd <dir> &&`, a trailing `2>&1`',\n 'and a pipe into `tail`/`head` are tolerated; nothing else is. Appending `&& git status` makes it',\n 'a DIFFERENT command and it is rejected again — that is not the guard refusing its own cure.',\n '',\n '`git merge` is deliberately NOT on this list. Main is merged ONLY through the 3-point fork merge',\n '(`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is already open).',\n '',\n '## Known asymmetry',\n '',\n 'Under `S`/`C`/`Y` the guard bin IS running, so a PASS really does fall through to the downstream',\n 'guards. Under `D`/`X`/`U`/`K` the bin is never executed, so there is nothing to fall through to and a',\n 'PASS degenerates into a terminal allow — reads are unguarded during those three faults.',\n '',\n '## Widening L0',\n '',\n 'Add an entry to `L0_ALLOWLIST` in `packages/tooling/ai-hook-rules/src/bin/shim.ts`. That array is',\n 'the single source for the JS allowlist, the `grep -E` inside the rendered shim, and this file.',\n '',\n ];\n}\n\n/**\n * Drop the matrix doc where the AI can read it, and return its absolute path ('' if it could not be\n * written). Called from the L0 BLOCK path so the deny can say `READ <path>`.\n *\n * Best-effort by design: this runs while the tree is already known-broken, and a missing template (an\n * @webpieces/rules-config older than this package) must degrade the deny message, never replace it\n * with a crash.\n */\n// webpieces-disable no-function-outside-class -- sibling of renderGuardMatrixDoc in this module\nexport function writeGuardMatrixDoc(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return writeTemplate(workspaceRoot, GUARD_MATRIX_DOC);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: no doc → the deny simply omits the pointer\n return '';\n }\n}\n\n/** The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written. */\n// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module\nexport function guardMatrixPointer(docPath: string): string {\n if (docPath === '') return '';\n return ` The full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"l0-matrix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-matrix.ts"],"names":[],"mappings":"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","sourcesContent":["import { CONFIG_FILENAME, writeTemplate } from '@webpieces/rules-config';\n\nimport {\n ADD_HOOK_PKG_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call, L0_ALLOWLIST, RECOVERY_CMD,\n RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, renderShim,\n} from '../bin/shim';\nimport { GUARANTEE_ROOT_MARKER } from '../bin/guarantee-root';\nimport { ENV_SURFACE, REGISTRATION_SURFACE } from '../bin/hook-registration';\nimport { shimStaleDenyReason } from '../bin/shim-deny-reason';\nimport {\n L0_FAULT_BIN_BROKEN, L0_FAULT_BIN_MISSING, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n L0_FAULT_DRIFT, L0_FAULT_SHIM_STALE, L0_FAULT_UNDECLARED,\n} from './l0-fault-codes';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// L0 — the TOOLING-INTEGRITY layer, as data.\n//\n// L0 is the outermost guard: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its faults are\n// enumerated in L0_FAULTS below and — drawn as a decision matrix — have NO genuine second dimension:\n//\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n//\n// so the only thing that varies per fault is the MESSAGE. This module holds the fault table and\n// renders it, together with L0_ALLOWLIST (../bin/shim), into webpieces.guard-matrix.md — the doc the\n// deny messages point the AI at. Doc and code come from the SAME arrays, so they cannot drift.\n// ---------------------------------------------------------------------------\n\n/**\n * The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,\n * and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.\n *\n * That generated doc is the AUTHORITY for the fault table and the allowlist (same arrays, cannot\n * drift). guards/L0-tooling.md is the hand-written companion: it adds L0's evaluation\n * ORDER, the use cases and the known gaps, and it documents L1, none of which are rendered from code.\n * Change L0_FAULTS or L0_ALLOWLIST and the generated doc follows automatically — guards/L0-tooling.md does\n * not, so update it in the same PR.\n */\nexport const GUARD_MATRIX_DOC = 'webpieces.guard-matrix.md';\n\n/**\n * One CURE for a fault: the exact call, plus the `mention` that must appear in that fault's deny text.\n *\n * Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny\n * message must actually name it. That pairing is the anti-deadlock invariant — a message that\n * prescribes a command the allowlist rejects is exactly the shape of the three deadlocks CLAUDE.md\n * records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.\n */\nexport class L0Cure {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly mention: string,\n readonly call: L0Call,\n /**\n * The one to reach for first when a fault has several. Exactly one cure per fault carries it,\n * so the rendered Fix section never asks the reader to choose between equals.\n */\n readonly preferred: boolean,\n /**\n * WHEN to pick this cure over its siblings — the sentence that makes a list of commands\n * actionable instead of a menu (\"when the PIN is the stale side\", not \"an alternative\").\n */\n readonly discriminator: string,\n ) {}\n\n /** A Bash cure renders as a literal command; a tool-shaped one renders as the edit it stands for. */\n isCommand(): boolean {\n return this.call.toolName === 'Bash';\n }\n}\n\n/** One L0 fault. Data-only → a class, per CLAUDE.md. */\nexport class L0Fault {\n constructor(\n readonly code: string,\n readonly name: string,\n readonly detectedBy: string,\n readonly enforcedIn: string,\n readonly cures: readonly L0Cure[],\n /**\n * The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for\n * D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —\n * the same bytes the consumer runs, which is what the mention assertion needs to search.\n */\n readonly denyText: string,\n ) {}\n}\n\n// The deny for fault C, and the ONLY message L0 has for a repo with no webpieces.config.json at all.\n// Moved here from runner.ts so the fault table and the runner cannot state different cures.\n//\n// It used to name `./node_modules/.bin/wp-setup-ai-hooks` — a bin that HAS NOT EXISTED since it was\n// renamed to wp-install-ai-hooks. So the one command this deny prescribed was (a) not installable and\n// (b) not on the L0 allowlist in that spelling, i.e. the AI was handed a cure it could neither run nor\n// get past the guard. Now it names the installer that actually seeds the config AND is entry 8 of the\n// allowlist, and it says out loud that writing the config yourself is allowed through.\n//\n// ORDERING (2026-08-02): writing the file yourself now LEADS. The bare installer used to be OPTION 1,\n// but it seeds the config and then PROMPTS twice for a hook target, which hangs a non-interactive\n// agent. Writing the file is the one cure that always works, and it is the same cure every other config\n// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every\n// error at once, so the write/validate loop converges in a couple of passes.\nexport const CONFIG_MISSING_REPORT =\n `${CONFIG_FILENAME} not found — the webpieces guards cannot run without it, so every other tool call is blocked.\\n` +\n 'THIS IS NOT A DEADLOCK: both options below are explicitly allowed through while this guard is up.\\n' +\n `OPTION 1 (preferred — it needs no other tool and it never prompts) - create ${CONFIG_FILENAME}\\n` +\n 'yourself: any Read, and any Write/Edit whose target is that file, is always allowed through, so\\n' +\n 'you can inspect the repo and write it. The validator reports EVERY missing/invalid entry at once\\n' +\n '(each with the snippet to paste), so a minimal first draft converges in about two passes.\\n' +\n 'OPTION 2 (pick this ONLY at an interactive terminal where you can answer its two prompts) - run\\n' +\n 'EXACTLY this command to seed the config: `pnpm exec wp-install-ai-hooks`. It goes on to wire the\\n' +\n 'Claude Code hooks and asks for a target twice, which hangs a non-interactive session.\\n' +\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.';\n\n// The first line of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail). Kept here so the fault table quotes the same text the runner emits.\nexport const CONFIG_OUT_OF_SYNC_HEADER =\n `${CONFIG_FILENAME} is out of sync — new built-in rules are present that have no entry in ${CONFIG_FILENAME}.`;\n\n// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation\n// invariant in guards/L0-tooling.md: every config problem cures to \"make the file right\", the validator\n// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits\n// this edit unconditionally. (That section is the authority — do not restate its reasoning here.)\nconst CONFIG_WRITE_CURE = new L0Cure(\n CONFIG_FILENAME, new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`), true,\n 'this fault fires at all — it is the only cure that needs no other tool, and it is never denied',\n);\n\n// Cure calls are spelled exactly as the deny messages spell them, so the mention assertion is a real\n// string search rather than a paraphrase.\n// webpieces-disable no-function-outside-class -- pure constructor helper for the L0_FAULTS literal below, in this data module\nfunction bashCure(command: string, preferred: boolean, discriminator: string): L0Cure {\n return new L0Cure(command, new L0Call('Bash', command, ''), preferred, discriminator);\n}\n\n/**\n * THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a\n * stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside\n * the bin, in JS. One model, two enforcement points.\n */\nexport const L0_FAULTS: readonly L0Fault[] = [\n new L0Fault(L0_FAULT_DRIFT, 'version drift — root package.json pin != installed version',\n 'sh, before the bin runs', 'sh',\n [\n // `pnpm install` clears D in BOTH directions — it makes installed == pin by definition — so\n // it is always the preferred cure. The direction only decides whether the PIN is the version\n // you WANT, which is what the second cure is for.\n bashCure('pnpm install', true,\n 'node_modules is OLDER than the pin, OR you are on a feature branch and want YOUR '\n + 'branch pin (usually the case) — it always clears the drift'),\n bashCure('git pull', false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'pull first and install second; a bare install would downgrade you'),\n ], renderShim()),\n new L0Fault(L0_FAULT_BIN_MISSING, 'guard bin missing (fresh clone / new worktree / package removed)',\n 'sh, before the bin runs', 'sh',\n [bashCure('pnpm install', true,\n 'this fault fires at all — nothing is installed in THIS tree, and a new git worktree '\n + 'copies no node_modules')],\n renderShim()),\n // U is X with the ONE input that inverts X's cure, which is why it is a separate fault and not a\n // sentence inside X's message: when nothing declares the package, `pnpm install` is not a weaker fix,\n // it is a PROVABLE no-op, and an agent that trusts the X text will run it until it gives up. See the\n // ADD_HOOK_PKG entry in l0-allowlist.ts for the incident.\n new L0Fault(L0_FAULT_UNDECLARED, `guard bin missing AND ${HOOK_PKG} is not declared in package.json`,\n 'sh, before the bin runs', 'sh',\n [bashCure(ADD_HOOK_PKG_CMD, true,\n 'this fault fires at all — package.json asks for nothing, so pnpm install reports '\n + '\"Lockfile is up to date\" and leaves the tree exactly as broken as it found it')],\n renderShim()),\n new L0Fault(L0_FAULT_BIN_BROKEN, 'guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)',\n 'sh, before the bin runs', 'sh',\n [bashCure(RECOVERY_CMD, true,\n 'this fault fires at all — a BARE pnpm install SKIPS the corrupt package, because pnpm sees '\n + 'the right version on disk and considers it installed; only the delete forces a rewrite')],\n renderShim()),\n // S covers the WHOLE managed hook surface, not just the shim: the two committed .sh files, the\n // .claude/settings.json entries that register them AND the managed `env` entry that pins the Bash\n // cwd so those RELATIVE entries always resolve. They only work as a set — a settings file left\n // on the old two-absolute-hook form disables the L-1 hook and re-pins every worktree to the\n // primary's release — and nothing validated the registration at all before it joined this fault.\n new L0Fault(L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file, the .claude/settings.json registration or its managed env entry does not match this release',\n 'the guard bin', 'JS',\n [\n // wp-upgrade-shim leads because it is now the ONLY cure that repairs all four, and it is\n // still surgical: it rewrites the two .sh files, the registration and the managed env entry\n // and touches no config, and it imports only fs/path so it runs on a tree too broken to load\n // the rule engine. The INSTALLER is deliberately NOT a cure here: it also migrates the config\n // and prompts for a target twice, which hangs a non-interactive agent.\n bashCure(UPGRADE_SHIM_CMD, true,\n 'this fault fires at all — it is the only cure that repairs all four managed things '\n + '(both .sh files, the settings.json registration and its managed env entry) and it '\n + 'touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer'),\n // 2026-07-21: the version gap below caused a real \"command not found\" deadlock.\n bashCure(RESTORE_SHIM_CMD, false,\n 'the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does '\n + 'not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade '\n + '@webpieces afterwards and run Option 1 to finish'),\n ],\n shimStaleDenyReason('', '', [SHIM_MARKER, GUARANTEE_ROOT_MARKER, REGISTRATION_SURFACE, ENV_SURFACE], false)),\n new L0Fault(L0_FAULT_CONFIG_MISSING, `${CONFIG_FILENAME} missing`,\n 'the guard bin', 'JS',\n [\n CONFIG_WRITE_CURE,\n // Kept, but demoted: it seeds the file and then PROMPTS twice for a hook target.\n bashCure(INSTALL_HOOKS_CMD, false,\n 'you are at an INTERACTIVE terminal and can answer its two hook-target prompts'),\n ], CONFIG_MISSING_REPORT),\n new L0Fault(L0_FAULT_CONFIG_OUT_OF_SYNC, `a loaded rule has no ${CONFIG_FILENAME} key`,\n 'the guard bin', 'JS',\n [CONFIG_WRITE_CURE], CONFIG_OUT_OF_SYNC_HEADER),\n];\n\n/**\n * One fault's FIX section, rendered from its `cures` array — literal commands only, never prose.\n *\n * This is the half that used to live in hand-written docs and drift. The three fields of L0Cure map\n * onto the three things a blocked reader needs and nothing else: WHAT to type (the call), WHETHER it is\n * the default (preferred), and WHEN to pick a sibling instead (discriminator). A cure with no\n * discriminator would render as a menu of equals, which is how an agent picks the wrong one.\n */\n// webpieces-disable no-function-outside-class -- pure string builder for renderGuardMatrixDoc below, beside the arrays it reads\nfunction renderFixSection(fault: L0Fault): string[] {\n const options = fault.cures.map((cure: L0Cure, i: number): string => {\n // A Bash cure is the command verbatim; a tool-shaped one is the file it edits (allowlist entry 2).\n const literal = cure.isCommand() ? `\\`${cure.call.command}\\`` : `edit \\`${cure.mention}\\` yourself`;\n const label = cure.preferred ? `Option ${i + 1} (preferred)` : `Option ${i + 1}`;\n return `- **${label}**: ${literal} ← pick this when ${cure.discriminator}`;\n });\n return [`### \\`${fault.code}\\` — ${fault.name}`, '', ...options, ''];\n}\n\n/**\n * Render webpieces.guard-matrix.md from L0_FAULTS + L0_ALLOWLIST.\n *\n * A unit test locks the committed template byte-identical to this output, the same way\n * templates/ai-hook.sh is locked to renderShim(). That is what makes the doc assertable instead of\n * aspirational: the table in the doc IS the array the guard consults.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over the two exported arrays, beside them in this module\nexport function renderGuardMatrixDoc(): string {\n return [\n '# webpieces guard matrix — L0 (tooling integrity)',\n '',\n 'GENERATED from `L0_FAULTS` + `L0_ALLOWLIST` in `@webpieces/ai-hook-rules`. Do not hand-edit —',\n 'a unit test locks this file byte-identical to `renderGuardMatrixDoc()`, so the table below is',\n 'the array the guard actually consults, not a description of it.',\n '',\n 'L0 is the OUTERMOST guard layer. It blocks work while `node_modules`, the committed shim, or',\n '`webpieces.config.json` are in a state that makes every other guard untrustworthy. If you are',\n 'reading this, one of the faults below fired and named this file.',\n '',\n '## The faults',\n '',\n '| code | fault | detected by | enforced in |',\n '|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string => `| \\`${f.code}\\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),\n '',\n 'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',\n 'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',\n '',\n '## The fix, per fault',\n '',\n 'Every command below is rendered from that fault\\'s `cures` array and is asserted, by unit test,',\n 'to be accepted by `isAllowed()` — so nothing here can be a command the guard then rejects. Type',\n 'the option you pick EXACTLY as written and run nothing else on that line.',\n '',\n ...L0_FAULTS.flatMap(renderFixSection),\n ...renderMatrixAndAllowlist(),\n ].join('\\n');\n}\n\n/**\n * The second half of the doc: the three-row matrix and the ONE allowlist. Split out of\n * renderGuardMatrixDoc solely to keep it inside the method-line budget — the join order is what makes\n * the two halves one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- second half of renderGuardMatrixDoc's string, beside it in this module\nfunction renderMatrixAndAllowlist(): string[] {\n return [\n '## The matrix',\n '',\n 'L0 has NO genuine second dimension. Every branch reduces to one question:',\n '',\n '| # | fault | on the allowlist? | outcome |',\n '|---|---|---|---|',\n '| 1 | none | — | hand down to the next guard layer |',\n '| 2 | any | yes | PASS or ALLOW (see the entry) |',\n '| 3 | any | no | BLOCK — **only the message varies by fault** |',\n '',\n 'The tool is not a dimension either: \"any Read\" is an allowlist ENTRY, not a tool check.',\n '',\n '## The allowlist',\n '',\n 'ONE list, consulted identically by every fault. A cure that cannot help a given fault also',\n 'cannot hurt it, and gating each entry on a fault is what produced four real defects (a stale',\n 'shim that denied `pnpm install` and `git pull`; faults that denied every Read; a config fault',\n 'that denied `rm -rf node_modules && pnpm install` while allowing a bare `pnpm install`).',\n '',\n '| # | allowed | outcome |',\n '|---|---|---|',\n ...L0_ALLOWLIST.map((e: L0AllowEntry, i: number): string => `| ${i + 1} | ${e.label} | ${e.kind.toUpperCase()} |`),\n '',\n '- **PASS** — L0 has no objection; the call falls THROUGH so the downstream guards still judge it.',\n '- **ALLOW** — terminal; bypasses everything, because a cure must stay reachable even when a',\n ' downstream guard would block it.',\n '',\n 'Every Bash entry is anchored to the WHOLE command. A leading `cd <dir> &&`, a trailing `2>&1`',\n 'and a pipe into `tail`/`head` are tolerated; nothing else is. Appending `&& git status` makes it',\n 'a DIFFERENT command and it is rejected again — that is not the guard refusing its own cure.',\n '',\n '`git merge` is deliberately NOT on this list. Main is merged ONLY through the 3-point fork merge',\n '(`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is already open).',\n '',\n '## Known asymmetry',\n '',\n 'Under `S`/`C`/`Y` the guard bin IS running, so a PASS really does fall through to the downstream',\n 'guards. Under `D`/`X`/`U`/`K` the bin is never executed, so there is nothing to fall through to and a',\n 'PASS degenerates into a terminal allow — reads are unguarded during those three faults.',\n '',\n '## Widening L0',\n '',\n 'Add an entry to `L0_ALLOWLIST` in `packages/tooling/ai-hook-rules/src/bin/shim.ts`. That array is',\n 'the single source for the JS allowlist, the `grep -E` inside the rendered shim, and this file.',\n '',\n ];\n}\n\n/**\n * Drop the matrix doc where the AI can read it, and return its absolute path ('' if it could not be\n * written). Called from the L0 BLOCK path so the deny can say `READ <path>`.\n *\n * Best-effort by design: this runs while the tree is already known-broken, and a missing template (an\n * @webpieces/rules-config older than this package) must degrade the deny message, never replace it\n * with a crash.\n */\n// webpieces-disable no-function-outside-class -- sibling of renderGuardMatrixDoc in this module\nexport function writeGuardMatrixDoc(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return writeTemplate(workspaceRoot, GUARD_MATRIX_DOC);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: no doc → the deny simply omits the pointer\n return '';\n }\n}\n\n/** The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written. */\n// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module\nexport function guardMatrixPointer(docPath: string): string {\n if (docPath === '') return '';\n return ` The full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;\n}\n"]}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "
|
|
2
|
+
"_comment": "The webpieces-managed PreToolUse wiring, for reference — install it with `wp-install-ai-hooks` (which lets you pick a location per hook) rather than copying this by hand. THREE hooks, and the asymmetry is the design: the Rules hook validates code-style on Write/Edit/MultiEdit and the Guards hook protects git/PR/branch — it matches Write|Edit|MultiEdit|Bash|Read so ALL guards run there (bash git/PR guards on Bash, file guards like feature-branch-guard on Write/Edit) plus a log-and-allow audit of Read (records opened files under .webpieces/logs; never blocks a read). Both are registered RELATIVE, `sh \".claude/webpieces/ai-hook.sh\" <bin>`, pointing at a checked-in shim that is generated and committed on purpose: relative means each git worktree runs its own release, its own binary and its own pin, and the shim turns a missing bin — fresh clone before `pnpm install`, or the package removed — into a friendly 'run pnpm install' line instead of a raw error on every tool call. The third hook, guarantee-root.sh, is registered ABSOLUTE via $CLAUDE_PROJECT_DIR because it is the one hook that must resolve from ANY cwd: a relative hook that cannot resolve exits 127, which per the hooks reference is a NON-BLOCKING error, i.e. a SILENT UNGUARDED ALLOW. It matches Bash alone, because only Bash can move the shell. A global ~/.claude/settings.json install instead uses the absolute `node <repoRoot>/node_modules/.bin/<bin>` and gets no L-1 hook, since it has no relative path that could fail to resolve.",
|
|
3
|
+
"_comment_env": "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1 is the FOURTH webpieces-managed thing, and it is guard integrity rather than ergonomics: it pins the Bash cwd back to the project root after every Bash call, so the RELATIVE hooks above always resolve, and because settings `env` is INHERITED it puts the main agent and every subagent on the same cwd — same hook resolution, same guard verdict. The trade, said out loud: the cwd reset becomes silent and unconditional, so a deliberate `cd` no longer persists between Bash calls — chain instead (`cd <dir> && <cmd>`). `pnpm exec wp-upgrade-shim` regenerates all four of these and reports which it changed.",
|
|
4
|
+
"env": {
|
|
5
|
+
"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1"
|
|
6
|
+
},
|
|
3
7
|
"hooks": {
|
|
4
8
|
"PreToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Bash",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": "sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/guarantee-root.sh\""
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
5
18
|
{
|
|
6
19
|
"matcher": "Write|Edit|MultiEdit",
|
|
7
20
|
"hooks": [
|
|
8
21
|
{
|
|
9
22
|
"type": "command",
|
|
10
|
-
"command": "
|
|
23
|
+
"command": "sh \".claude/webpieces/ai-hook.sh\" wp-ai-rules-hook"
|
|
11
24
|
}
|
|
12
25
|
]
|
|
13
26
|
},
|
|
@@ -16,7 +29,7 @@
|
|
|
16
29
|
"hooks": [
|
|
17
30
|
{
|
|
18
31
|
"type": "command",
|
|
19
|
-
"command": "
|
|
32
|
+
"command": "sh \".claude/webpieces/ai-hook.sh\" wp-ai-guards-hook"
|
|
20
33
|
}
|
|
21
34
|
]
|
|
22
35
|
}
|