@webpieces/ai-hook-rules 0.4.622 → 0.4.624
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 +49 -19
- package/package.json +2 -2
- package/src/adapters/hook-core.js +13 -14
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/hook-registration.d.ts +160 -50
- package/src/bin/hook-registration.js +227 -96
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/managed-env.d.ts +46 -0
- package/src/bin/managed-env.js +50 -0
- package/src/bin/managed-env.js.map +1 -0
- package/src/bin/setup.d.ts +1 -2
- package/src/bin/setup.js +38 -46
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.js +12 -1
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +6 -0
- package/src/bin/shim-deny-reason.js +82 -0
- package/src/bin/shim-deny-reason.js.map +1 -0
- package/src/bin/shim.d.ts +0 -1
- package/src/bin/shim.js +3 -57
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.js +140 -30
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/decision-log.d.ts +3 -3
- package/src/core/decision-log.js +8 -8
- package/src/core/decision-log.js.map +1 -1
- package/src/core/effective-tree.d.ts +5 -2
- package/src/core/effective-tree.js +1 -1
- package/src/core/effective-tree.js.map +1 -1
- package/src/core/l0-matrix.js +15 -13
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l1-doc.js +29 -68
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l1-rows.d.ts +17 -9
- package/src/core/l1-rows.js +18 -13
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/log-stream.d.ts +6 -4
- package/src/core/log-stream.js +6 -4
- package/src/core/log-stream.js.map +1 -1
- package/src/core/log-streams.d.ts +13 -3
- package/src/core/log-streams.js +15 -5
- package/src/core/log-streams.js.map +1 -1
- package/src/core/runner.d.ts +1 -2
- package/src/core/runner.js +29 -24
- package/src/core/runner.js.map +1 -1
- package/src/core/version-sync.d.ts +67 -0
- package/src/core/version-sync.js +148 -0
- package/src/core/version-sync.js.map +1 -0
- package/src/core/webpieces-versions.d.ts +83 -0
- package/src/core/webpieces-versions.js +169 -0
- package/src/core/webpieces-versions.js.map +1 -0
- package/templates/ai-hook.sh +15 -4
- package/templates/claude-settings-hook.json +6 -3
- package/src/bin/guarantee-root.d.ts +0 -95
- package/src/bin/guarantee-root.js +0 -297
- package/src/bin/guarantee-root.js.map +0 -1
- package/src/core/coordinator-worktree.d.ts +0 -61
- package/src/core/coordinator-worktree.js +0 -94
- package/src/core/coordinator-worktree.js.map +0 -1
- package/templates/guarantee-root.sh +0 -113
|
@@ -1,38 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE INSTALLED HOOK SURFACE —
|
|
2
|
+
* THE INSTALLED HOOK SURFACE — two hooks, both ABSOLUTE, and the ONE place their spelling is defined.
|
|
3
3
|
*
|
|
4
|
-
* ───
|
|
5
|
-
* `.claude/settings.json` used to register two hooks, BOTH absolute via `$CLAUDE_PROJECT_DIR`. That
|
|
6
|
-
* variable NEVER moves — proven from four separate worktrees' own logs, every line reading
|
|
7
|
-
* `root=<worktree> projectDir=<primary>` — so every tree was governed by the PRIMARY's shim, the
|
|
8
|
-
* PRIMARY's binary and the PRIMARY's pin, forever. A worktree could never be judged by the release its
|
|
9
|
-
* own branch pins, and measuring one tree while running another's binary is the non-convergent
|
|
10
|
-
* "two-tree straddle" recorded in shim.ts (an agent gave up after four cures).
|
|
4
|
+
* ─── ONE GOVERNOR: the MAIN tree judges every tree ─────────────────────────────────────────────────
|
|
11
5
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* H1 sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" wp-ai-guards-hook Write|…|Bash|Read
|
|
7
|
+
* H2 sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" wp-ai-rules-hook Write|Edit|MultiEdit
|
|
14
8
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* H3 relative sh ".claude/webpieces/ai-hook.sh" wp-ai-rules-hook matcher Write|Edit|MultiEdit
|
|
9
|
+
* This REPLACES a three-hook form in which these two were RELATIVE (`sh ".claude/webpieces/ai-hook.sh"`)
|
|
10
|
+
* and a third hook, L-1 `guarantee-root.sh`, existed solely to guarantee that relative path resolved.
|
|
18
11
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
12
|
+
* ─── Why the relative experiment was reversed ──────────────────────────────────────────────────────
|
|
13
|
+
* Relative registration was adopted so each git tree would be governed by "its own release, binary and
|
|
14
|
+
* pin". MEASURED 2026-08-10: it never delivered that. A linked worktree has NO `node_modules`, so
|
|
15
|
+
* ai-hook.sh's upward walk executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin
|
|
16
|
+
* to `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own
|
|
17
|
+
* CONFIG; it never ran its own release. Governance was already the primary's, in every tree, the whole
|
|
18
|
+
* time.
|
|
23
19
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* The price of that fiction was an entire guard layer. A relative path only resolves at a tree root, and
|
|
21
|
+
* a hook that cannot resolve exits 127 — per the hooks reference a NON-BLOCKING error, i.e. a SILENT
|
|
22
|
+
* UNGUARDED ALLOW. So L-1 had to deny every `cd` into a project subdirectory. That denial produced the
|
|
23
|
+
* force-to-root bug class, the reaped-worktree `cd` prescription, and a measured hard deadlock: L-1 told
|
|
24
|
+
* a worktree-isolated agent to `cd` to the primary clone, which the harness refuses for an isolated
|
|
25
|
+
* agent, leaving it unable to stay or to follow the cure.
|
|
26
|
+
*
|
|
27
|
+
* An absolute path resolves from ANY cwd. So the launch guarantee is structural, L-1 has no job left and
|
|
28
|
+
* is deleted, and `cd` into a subdirectory is simply allowed. What used to be true only by policing the
|
|
29
|
+
* shell is now true by construction.
|
|
30
|
+
*
|
|
31
|
+
* ─── What replaces the property that was lost ──────────────────────────────────────────────────────
|
|
32
|
+
* Nothing is lost that was ever delivered — but the case relative registration WANTED to handle (a tree
|
|
33
|
+
* that genuinely needs a different @webpieces) is now DETECTED instead of silently mis-governed:
|
|
34
|
+
* `VersionSyncGuard` (L1 row 8) blocks when a worktree's pin disagrees with the main tree's, and
|
|
35
|
+
* prescribes either aligning the pins (same git hash → same tracked pin → one install in the main tree)
|
|
36
|
+
* or using a separate CLONE, which — unlike a worktree — really does get its own node_modules and its
|
|
37
|
+
* own governance.
|
|
27
38
|
*
|
|
28
39
|
* ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────
|
|
29
|
-
* Nothing used to validate `.claude/settings.json` at all, so a settings file left on
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
40
|
+
* Nothing used to validate `.claude/settings.json` at all, so a settings file left on a superseded form
|
|
41
|
+
* silently changed who governs. The installed surface is THREE things (ai-hook.sh, the registration, and
|
|
42
|
+
* the managed `env` entry — see managed-env.ts), all three are compared against this release, and
|
|
43
|
+
* `wp-upgrade-shim` regenerates all three. A cure that fixes two of three is worse than no cure, because
|
|
44
|
+
* it reports success.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* The RETIRED L-1 hook's committed path. Named here, in the one module that must still recognise it, and
|
|
48
|
+
* matched ONLY by isManagedCommand() so repairRegistration() can delete the stale entry. There is no
|
|
49
|
+
* guarantee-root.ts any more — this literal is all that remains of it, deliberately.
|
|
50
|
+
*
|
|
51
|
+
* It is a one-way RECOGNISER, not a shim: nothing emits it, `expectedEntries()` never returns it, and a
|
|
52
|
+
* settings file carrying it is reported STALE. Its only job is to make the retired entry findable so it
|
|
53
|
+
* can be REMOVED.
|
|
54
|
+
*/
|
|
55
|
+
export declare const LEGACY_GUARANTEE_ROOT_MARKER = ".claude/webpieces/guarantee-root.sh";
|
|
56
|
+
/**
|
|
57
|
+
* When this recogniser may be deleted, as a value rather than a comment nobody re-reads.
|
|
58
|
+
*
|
|
59
|
+
* A removal-only migration is still dead weight once no consumer can be carrying the old shape. The
|
|
60
|
+
* hazard of deleting it EARLY is severe and silent — repair would stop stripping the retired entry, and
|
|
61
|
+
* a hook registered against a deleted file exits 127, which the Claude Code hooks reference defines as a
|
|
62
|
+
* NON-BLOCKING error, i.e. every `cd` unjudged while the cure reports success. So it gets a stated date
|
|
63
|
+
* and a test that fails once the date passes, instead of an intention.
|
|
64
|
+
*
|
|
65
|
+
* `legacy-marker-expiry.spec.ts` reads this and fails after it, which is the reminder.
|
|
35
66
|
*/
|
|
67
|
+
export declare const LEGACY_MARKER_REMOVE_AFTER = "2026-12-01";
|
|
36
68
|
/** One PreToolUse hook entry as webpieces registers it. Data-only → a class, per CLAUDE.md. */
|
|
37
69
|
export declare class HookRegistrationEntry {
|
|
38
70
|
readonly matcher: string;
|
|
@@ -52,28 +84,50 @@ export interface ClaudeSettings {
|
|
|
52
84
|
hooks?: {
|
|
53
85
|
PreToolUse?: HookEntry[];
|
|
54
86
|
};
|
|
87
|
+
env?: Record<string, string>;
|
|
55
88
|
[key: string]: unknown;
|
|
56
89
|
}
|
|
57
90
|
export declare const RULES_BIN = "wp-ai-rules-hook";
|
|
58
91
|
export declare const GUARDS_BIN = "wp-ai-guards-hook";
|
|
59
92
|
export declare const RULES_MATCHER = "Write|Edit|MultiEdit";
|
|
60
93
|
export declare const GUARDS_MATCHER = "Write|Edit|MultiEdit|Bash|Read";
|
|
61
|
-
export declare const GUARANTEE_ROOT_MATCHER = "Bash";
|
|
62
94
|
/**
|
|
63
|
-
* The
|
|
95
|
+
* The guard-hook command — `sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin>`.
|
|
96
|
+
*
|
|
97
|
+
* ABSOLUTE. This replaced a RELATIVE spelling, and the reversal is the whole point of this release.
|
|
98
|
+
*
|
|
99
|
+
* The relative form existed to give each git tree its own release, binary and pin. **It never delivered
|
|
100
|
+
* that.** Measured 2026-08-10: a linked worktree has no `node_modules`, so ai-hook.sh's upward walk
|
|
101
|
+
* executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin to
|
|
102
|
+
* `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own CONFIG,
|
|
103
|
+
* never its own release. The property was fiction, and paying for it cost an entire guard layer: a
|
|
104
|
+
* relative path only resolves at a tree root, so L-1 (guarantee-root.sh) had to deny every `cd` into a
|
|
105
|
+
* project subdirectory, which produced the force-to-root bug class, the reaped-worktree `cd`
|
|
106
|
+
* prescription, and a measured hard deadlock where L-1 told a worktree-isolated agent to `cd` to the
|
|
107
|
+
* primary clone — which the harness refuses for an isolated agent.
|
|
108
|
+
*
|
|
109
|
+
* Absolute resolves from ANY cwd, so L-1 has no job left and is deleted. One governor: the MAIN tree
|
|
110
|
+
* judges every tree, which is what was already happening via the borrowed binary — the design now says
|
|
111
|
+
* so out loud, and `VersionSyncGuard` blocks the case where that is the wrong answer.
|
|
64
112
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* `Permission denied` on every tool call. Quoted to survive spaces in the path.
|
|
113
|
+
* Invoked via `sh <file>` rather than executed directly so a missing executable bit on the checked-in
|
|
114
|
+
* shim (fresh clone, a filesystem that drops the bit, git core.fileMode quirks) can never break the hook
|
|
115
|
+
* with a raw `Permission denied` on every tool call. Quoted to survive spaces in the path.
|
|
69
116
|
*/
|
|
70
117
|
export declare function shimCommand(bin: string): string;
|
|
71
|
-
/** H1's command. ABSOLUTE on purpose — it must resolve from ANY cwd or it cannot fail closed. */
|
|
72
|
-
export declare const GUARANTEE_ROOT_COMMAND = "sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/guarantee-root.sh\"";
|
|
73
|
-
export declare const GUARANTEE_ROOT_ENTRY: HookRegistrationEntry;
|
|
74
118
|
/** The registration entry for one guard bin. */
|
|
75
119
|
export declare function guardHookEntry(bin: string): HookRegistrationEntry;
|
|
76
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* True when this PreToolUse command is one webpieces owns — in ANY spelling it has ever shipped.
|
|
122
|
+
*
|
|
123
|
+
* THE SINGLE MOST IMPORTANT LINE IN THIS RELEASE. `LEGACY_GUARANTEE_ROOT_MARKER` is matched here and
|
|
124
|
+
* NOWHERE else: it is how `repairRegistration()` finds and REMOVES the retired H1 entry from a settings
|
|
125
|
+
* file written by an older release. Drop it and the repair silently leaves a live L-1 hook registered
|
|
126
|
+
* against a file this release deletes — exit 127, which the Claude Code hooks reference defines as a
|
|
127
|
+
* NON-BLOCKING error, so every `cd` goes unjudged while `wp-upgrade-shim` reports success and no drift
|
|
128
|
+
* check can name it. Removal-only, never emitted: it appears in no `expectedEntries()` result, so it is
|
|
129
|
+
* a one-way migration and not a second accepted spelling.
|
|
130
|
+
*/
|
|
77
131
|
export declare function isManagedCommand(command: string): boolean;
|
|
78
132
|
/** Every webpieces-managed entry of one settings file, flattened to matcher + command pairs. */
|
|
79
133
|
export declare function managedEntries(settings: ClaudeSettings): readonly HookRegistrationEntry[];
|
|
@@ -86,14 +140,14 @@ export declare function registeredBins(settings: ClaudeSettings): readonly strin
|
|
|
86
140
|
/**
|
|
87
141
|
* The exact set of entries THIS RELEASE expects in a settings file that registers `bins`.
|
|
88
142
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
143
|
+
* ONE entry per registered bin, and nothing else. There used to be a third, `guarantee-root.sh`, added
|
|
144
|
+
* whenever the GUARDS bin was present; it is retired, and a settings file still carrying it is STALE —
|
|
145
|
+
* `repairRegistration()` removes it via isManagedCommand()'s legacy marker.
|
|
92
146
|
*/
|
|
93
147
|
export declare function expectedEntries(bins: readonly string[]): readonly HookRegistrationEntry[];
|
|
94
148
|
/**
|
|
95
|
-
* True when a settings file registers webpieces hooks in a shape this release does not expect
|
|
96
|
-
*
|
|
149
|
+
* True when a settings file registers webpieces hooks in a shape this release does not expect: a
|
|
150
|
+
* RELATIVE shim command, a leftover guarantee-root entry, a stray duplicate, or a wrong matcher.
|
|
97
151
|
*
|
|
98
152
|
* Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order
|
|
99
153
|
* carries no meaning and reordering must not read as drift.
|
|
@@ -104,12 +158,45 @@ export declare function dropManagedEntries(settings: ClaudeSettings): boolean;
|
|
|
104
158
|
/** Append one PreToolUse entry. */
|
|
105
159
|
export declare function addHookEntry(settings: ClaudeSettings, entry: HookRegistrationEntry): void;
|
|
106
160
|
/**
|
|
107
|
-
*
|
|
161
|
+
* True when a settings file that registers webpieces hooks is missing the managed `env` entry, or
|
|
162
|
+
* carries any value other than the one required one.
|
|
163
|
+
*
|
|
164
|
+
* Gated on `registeredBins()` for the SAME reason `registrationStale()` is: a settings file that
|
|
165
|
+
* registers no webpieces hooks is not a project install and is never judged — a global install names
|
|
166
|
+
* the bin path directly and carries no shim marker at all, so there is nothing here to keep in step.
|
|
108
167
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
168
|
+
* NOTE what this entry is for NOW. It was originally justified by keeping the then-RELATIVE hook path
|
|
169
|
+
* resolvable; that job is retired, because both hooks are absolute and resolve from any cwd. It is kept
|
|
170
|
+
* for VERDICT STABILITY — a guard's answer must depend on the command, not on where an earlier `cd` left
|
|
171
|
+
* the shell — and because settings `env` is inherited, every subagent shares that cwd and therefore that
|
|
172
|
+
* verdict. See managed-env.ts, which states this at length.
|
|
173
|
+
*/
|
|
174
|
+
export declare function envStale(settings: ClaudeSettings): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Set the managed `env` entry IN PLACE, whatever was there before. Returns true when it changed.
|
|
177
|
+
*
|
|
178
|
+
* A user-set `"0"` is BROUGHT TO `"1"`, not honoured: this is a managed surface, and "webpieces sets it
|
|
179
|
+
* unless you disagreed" would be a second, invisible spelling of the decision. Turning it off means
|
|
180
|
+
* uninstalling the hooks, exactly as it does for every other managed thing.
|
|
181
|
+
*
|
|
182
|
+
* UNGATED on purpose — the installer calls it for the file it is writing hooks into, and the gate lives
|
|
183
|
+
* at the caller (repairRegistration / applyHook), so a file with no webpieces hooks is never touched.
|
|
184
|
+
*/
|
|
185
|
+
export declare function applyManagedEnv(settings: ClaudeSettings): boolean;
|
|
186
|
+
/**
|
|
187
|
+
* Bring one settings object to the two-hook ABSOLUTE form AND the managed `env` entry IN PLACE. Returns
|
|
188
|
+
* true when it changed anything.
|
|
189
|
+
*
|
|
190
|
+
* REMOVE-then-ADD for the hooks, never add-beside: two spellings of one registration is exactly the
|
|
191
|
+
* compatibility shim the backwards-compat reviewer rejects, and leaving a RELATIVE entry beside the
|
|
192
|
+
* absolute one would run two shims per call — the
|
|
193
|
+
* straddle this whole change exists to delete.
|
|
194
|
+
*
|
|
195
|
+
* THE TWO REPAIRS ARE EVALUATED INDEPENDENTLY, and that is load-bearing rather than tidy: this used to
|
|
196
|
+
* early-return on `!registrationStale(settings)`, so a repo whose hooks are already current but whose
|
|
197
|
+
* `env` entry is missing — the state EVERY existing consumer is in the moment this release lands — would
|
|
198
|
+
* have been reported as drifted by fault S and then left unrepaired by its own prescribed cure. A cure
|
|
199
|
+
* that skips half the surface is the failure mode `upgrade-shim.ts`'s header exists to prevent.
|
|
113
200
|
*/
|
|
114
201
|
export declare function repairRegistration(settings: ClaudeSettings): boolean;
|
|
115
202
|
/** The two project settings files the installer can write. */
|
|
@@ -118,12 +205,35 @@ export declare function readSettings(settingsPath: string): ClaudeSettings;
|
|
|
118
205
|
export declare function writeSettings(settingsPath: string, settings: ClaudeSettings): void;
|
|
119
206
|
/** True when either project settings file under `root` carries a stale registration. */
|
|
120
207
|
export declare function registrationStaleAt(root: string | null): boolean;
|
|
121
|
-
/**
|
|
122
|
-
export declare function
|
|
123
|
-
/**
|
|
208
|
+
/** True when either project settings file under `root` is missing the managed `env` entry. */
|
|
209
|
+
export declare function envStaleAt(root: string | null): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* WHAT was rewritten in ONE settings file. Data-only → a class, per CLAUDE.md.
|
|
212
|
+
*
|
|
213
|
+
* Two independent flags rather than one path, because the cure has to be able to SAY which repair it
|
|
214
|
+
* made: "rewrote the hook registration" printed for a file whose registration was already current and
|
|
215
|
+
* whose `env` entry was the only thing missing is a cure lying about its own work.
|
|
216
|
+
*/
|
|
217
|
+
export declare class SettingsRepair {
|
|
218
|
+
readonly settingsPath: string;
|
|
219
|
+
readonly registration: boolean;
|
|
220
|
+
readonly env: boolean;
|
|
221
|
+
constructor(settingsPath: string, registration: boolean, env: boolean);
|
|
222
|
+
}
|
|
223
|
+
/** Rewrite every stale project settings file under `root`; returns what changed, per file. */
|
|
224
|
+
export declare function repairRegistrationAt(root: string): readonly SettingsRepair[];
|
|
225
|
+
/**
|
|
226
|
+
* The THREE names the drift check reports, so a deny can say WHICH of them moved.
|
|
227
|
+
*
|
|
228
|
+
* There were four. `GUARANTEE_ROOT_SURFACE` is gone with L-1 itself: an absolutely-registered shim
|
|
229
|
+
* resolves from any cwd, so there is no launch guarantee left to police and no second .sh file to keep
|
|
230
|
+
* byte-locked. A settings file still carrying the retired H1 entry is not its own surface any more —
|
|
231
|
+
* it is ordinary REGISTRATION drift, which `registrationStaleAt()` already reports and
|
|
232
|
+
* `repairRegistration()` already fixes by removing it.
|
|
233
|
+
*/
|
|
124
234
|
export declare const SHIM_SURFACE = ".claude/webpieces/ai-hook.sh";
|
|
125
|
-
export declare const GUARANTEE_ROOT_SURFACE = ".claude/webpieces/guarantee-root.sh";
|
|
126
235
|
export declare const REGISTRATION_SURFACE = ".claude/settings.json hook registration";
|
|
236
|
+
export declare const ENV_SURFACE = ".claude/settings.json env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR";
|
|
127
237
|
/**
|
|
128
238
|
* WHICH of the three managed surfaces disagree with this release — the input to fault S.
|
|
129
239
|
*
|