@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.
Files changed (60) hide show
  1. package/README.md +49 -19
  2. package/package.json +2 -2
  3. package/src/adapters/hook-core.js +13 -14
  4. package/src/adapters/hook-core.js.map +1 -1
  5. package/src/bin/hook-registration.d.ts +160 -50
  6. package/src/bin/hook-registration.js +227 -96
  7. package/src/bin/hook-registration.js.map +1 -1
  8. package/src/bin/managed-env.d.ts +46 -0
  9. package/src/bin/managed-env.js +50 -0
  10. package/src/bin/managed-env.js.map +1 -0
  11. package/src/bin/setup.d.ts +1 -2
  12. package/src/bin/setup.js +38 -46
  13. package/src/bin/setup.js.map +1 -1
  14. package/src/bin/shim-audit-log.js +12 -1
  15. package/src/bin/shim-audit-log.js.map +1 -1
  16. package/src/bin/shim-deny-reason.d.ts +6 -0
  17. package/src/bin/shim-deny-reason.js +82 -0
  18. package/src/bin/shim-deny-reason.js.map +1 -0
  19. package/src/bin/shim.d.ts +0 -1
  20. package/src/bin/shim.js +3 -57
  21. package/src/bin/shim.js.map +1 -1
  22. package/src/bin/upgrade-shim.js +140 -30
  23. package/src/bin/upgrade-shim.js.map +1 -1
  24. package/src/core/decision-log.d.ts +3 -3
  25. package/src/core/decision-log.js +8 -8
  26. package/src/core/decision-log.js.map +1 -1
  27. package/src/core/effective-tree.d.ts +5 -2
  28. package/src/core/effective-tree.js +1 -1
  29. package/src/core/effective-tree.js.map +1 -1
  30. package/src/core/l0-matrix.js +15 -13
  31. package/src/core/l0-matrix.js.map +1 -1
  32. package/src/core/l1-doc.js +29 -68
  33. package/src/core/l1-doc.js.map +1 -1
  34. package/src/core/l1-rows.d.ts +17 -9
  35. package/src/core/l1-rows.js +18 -13
  36. package/src/core/l1-rows.js.map +1 -1
  37. package/src/core/log-stream.d.ts +6 -4
  38. package/src/core/log-stream.js +6 -4
  39. package/src/core/log-stream.js.map +1 -1
  40. package/src/core/log-streams.d.ts +13 -3
  41. package/src/core/log-streams.js +15 -5
  42. package/src/core/log-streams.js.map +1 -1
  43. package/src/core/runner.d.ts +1 -2
  44. package/src/core/runner.js +29 -24
  45. package/src/core/runner.js.map +1 -1
  46. package/src/core/version-sync.d.ts +67 -0
  47. package/src/core/version-sync.js +148 -0
  48. package/src/core/version-sync.js.map +1 -0
  49. package/src/core/webpieces-versions.d.ts +83 -0
  50. package/src/core/webpieces-versions.js +169 -0
  51. package/src/core/webpieces-versions.js.map +1 -0
  52. package/templates/ai-hook.sh +15 -4
  53. package/templates/claude-settings-hook.json +6 -3
  54. package/src/bin/guarantee-root.d.ts +0 -95
  55. package/src/bin/guarantee-root.js +0 -297
  56. package/src/bin/guarantee-root.js.map +0 -1
  57. package/src/core/coordinator-worktree.d.ts +0 -61
  58. package/src/core/coordinator-worktree.js +0 -94
  59. package/src/core/coordinator-worktree.js.map +0 -1
  60. package/templates/guarantee-root.sh +0 -113
@@ -1,38 +1,70 @@
1
1
  /**
2
- * THE INSTALLED HOOK SURFACE — three hooks, and the ONE place their spelling is defined.
2
+ * THE INSTALLED HOOK SURFACE — two hooks, both ABSOLUTE, and the ONE place their spelling is defined.
3
3
  *
4
- * ─── Why three, and why two of them are RELATIVE ───────────────────────────────────────────────────
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
- * The hooks reference says "the hook runs in the `cwd` value from the JSON input", so a RELATIVE
13
- * command resolves against the tool call's own tree. Hence:
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
- * H1 absolute sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/guarantee-root.sh" matcher Bash
16
- * H2 relative sh ".claude/webpieces/ai-hook.sh" wp-ai-guards-hook matcher Write|…|Read
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
- * H1 stays absolute because it is the one hook that must ALWAYS resolve: a relative hook that cannot
20
- * resolve exits 127, and per the same reference any non-2 non-zero exit is a NON-BLOCKING error — i.e.
21
- * a SILENT UNGUARDED ALLOW. H1 refuses any `cd` that would park the shell where H2/H3 cannot launch,
22
- * which is what makes the relative pair admissible at all. See guarantee-root.ts.
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
- * H1 matches `Bash` alone because only Bash can move the shell the same reason stated at the top of
25
- * guarantee-root.ts, and the reason the rendered guarantee-root.sh exits 0 immediately for every other
26
- * tool. Registering it wider would spawn a process per Write/Read to do nothing.
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 the old
30
- * two-absolute-hook form silently reverted a repo to per-PRIMARY governance and disabled H1 the one
31
- * component whose whole job is failing closed. The installed surface is therefore THREE things
32
- * (ai-hook.sh, guarantee-root.sh, the registration), all three are compared against this release, and
33
- * `wp-upgrade-shim` regenerates all three. A cure that fixes one of three is worse than no cure,
34
- * because it reports success.
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 RELATIVE guard-hook command — `sh ".claude/webpieces/ai-hook.sh" <bin>`.
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
- * Relative, NOT `$CLAUDE_PROJECT_DIR/…`: that is the whole point (see the header). Invoked via `sh
66
- * <file>` rather than executed directly so a missing executable bit on the checked-in shim (fresh
67
- * clone, a filesystem that drops the bit, git core.fileMode quirks) can never break the hook with a raw
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
- /** True when this PreToolUse command is one webpieces owns (either .sh file, in any spelling). */
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
- * H1 rides with the GUARDS hook, not the rules hook: H1 judges Bash, and `Bash` is in the guards
90
- * matcher. A file carrying only the rules hook (the supported split install, where a team ships the
91
- * guards and a developer keeps the code-style rules local) gets no H1 and needs none.
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 — the
96
- * old two-absolute-hook form, a missing guarantee-root entry, a stray duplicate, a wrong matcher.
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
- * Bring one settings object to the three-hook form IN PLACE. Returns true when it changed.
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
- * REMOVE-then-ADD, never add-beside: two spellings of one registration is exactly the compatibility
110
- * shim the backwards-compat reviewer rejects, and leaving the `$CLAUDE_PROJECT_DIR/`-prefixed entry
111
- * beside the relative one would run the PRIMARY's binary alongside the tree's own the straddle this
112
- * whole change exists to delete.
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
- /** Rewrite every stale project settings file under `root`; returns the paths actually changed. */
122
- export declare function repairRegistrationAt(root: string): readonly string[];
123
- /** The three names the drift check reports, so a deny can say WHICH of them moved. */
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
  *