@webpieces/ai-hook-rules 0.4.587 → 0.4.589
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 +25 -7
- package/package.json +2 -2
- package/src/adapters/hook-core.js +12 -3
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/guarantee-root.d.ts +5 -0
- package/src/bin/guarantee-root.js +13 -0
- package/src/bin/guarantee-root.js.map +1 -1
- package/src/bin/hook-registration.d.ts +134 -0
- package/src/bin/hook-registration.js +320 -0
- package/src/bin/hook-registration.js.map +1 -0
- package/src/bin/install-entry.d.ts +15 -0
- package/src/bin/install-entry.js +22 -0
- package/src/bin/install-entry.js.map +1 -1
- package/src/bin/setup.d.ts +1 -15
- package/src/bin/setup.js +84 -77
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim.d.ts +1 -1
- package/src/bin/shim.js +63 -11
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.d.ts +16 -0
- package/src/bin/upgrade-shim.js +96 -15
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/l0-matrix.js +18 -11
- package/src/core/l0-matrix.js.map +1 -1
- package/templates/ai-hook.sh +30 -6
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REGISTRATION_SURFACE = exports.GUARANTEE_ROOT_SURFACE = exports.SHIM_SURFACE = exports.GUARANTEE_ROOT_ENTRY = exports.GUARANTEE_ROOT_COMMAND = exports.GUARANTEE_ROOT_MATCHER = exports.GUARDS_MATCHER = exports.RULES_MATCHER = exports.GUARDS_BIN = exports.RULES_BIN = exports.HookRegistrationEntry = void 0;
|
|
4
|
+
exports.shimCommand = shimCommand;
|
|
5
|
+
exports.guardHookEntry = guardHookEntry;
|
|
6
|
+
exports.isManagedCommand = isManagedCommand;
|
|
7
|
+
exports.managedEntries = managedEntries;
|
|
8
|
+
exports.registeredBins = registeredBins;
|
|
9
|
+
exports.expectedEntries = expectedEntries;
|
|
10
|
+
exports.registrationStale = registrationStale;
|
|
11
|
+
exports.dropManagedEntries = dropManagedEntries;
|
|
12
|
+
exports.addHookEntry = addHookEntry;
|
|
13
|
+
exports.repairRegistration = repairRegistration;
|
|
14
|
+
exports.projectSettingsPaths = projectSettingsPaths;
|
|
15
|
+
exports.readSettings = readSettings;
|
|
16
|
+
exports.writeSettings = writeSettings;
|
|
17
|
+
exports.registrationStaleAt = registrationStaleAt;
|
|
18
|
+
exports.repairRegistrationAt = repairRegistrationAt;
|
|
19
|
+
exports.managedSurfaceDrift = managedSurfaceDrift;
|
|
20
|
+
const tslib_1 = require("tslib");
|
|
21
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
22
|
+
const path = tslib_1.__importStar(require("path"));
|
|
23
|
+
const to_error_1 = require("../core/to-error");
|
|
24
|
+
const shim_1 = require("./shim");
|
|
25
|
+
const guarantee_root_1 = require("./guarantee-root");
|
|
26
|
+
/**
|
|
27
|
+
* THE INSTALLED HOOK SURFACE — three hooks, and the ONE place their spelling is defined.
|
|
28
|
+
*
|
|
29
|
+
* ─── Why three, and why two of them are RELATIVE ───────────────────────────────────────────────────
|
|
30
|
+
* `.claude/settings.json` used to register two hooks, BOTH absolute via `$CLAUDE_PROJECT_DIR`. That
|
|
31
|
+
* variable NEVER moves — proven from four separate worktrees' own logs, every line reading
|
|
32
|
+
* `root=<worktree> projectDir=<primary>` — so every tree was governed by the PRIMARY's shim, the
|
|
33
|
+
* PRIMARY's binary and the PRIMARY's pin, forever. A worktree could never be judged by the release its
|
|
34
|
+
* own branch pins, and measuring one tree while running another's binary is the non-convergent
|
|
35
|
+
* "two-tree straddle" recorded in shim.ts (an agent gave up after four cures).
|
|
36
|
+
*
|
|
37
|
+
* The hooks reference says "the hook runs in the `cwd` value from the JSON input", so a RELATIVE
|
|
38
|
+
* command resolves against the tool call's own tree. Hence:
|
|
39
|
+
*
|
|
40
|
+
* H1 absolute sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/guarantee-root.sh" matcher Bash
|
|
41
|
+
* H2 relative sh ".claude/webpieces/ai-hook.sh" wp-ai-guards-hook matcher Write|…|Read
|
|
42
|
+
* H3 relative sh ".claude/webpieces/ai-hook.sh" wp-ai-rules-hook matcher Write|Edit|MultiEdit
|
|
43
|
+
*
|
|
44
|
+
* H1 stays absolute because it is the one hook that must ALWAYS resolve: a relative hook that cannot
|
|
45
|
+
* resolve exits 127, and per the same reference any non-2 non-zero exit is a NON-BLOCKING error — i.e.
|
|
46
|
+
* a SILENT UNGUARDED ALLOW. H1 refuses any `cd` that would park the shell where H2/H3 cannot launch,
|
|
47
|
+
* which is what makes the relative pair admissible at all. See guarantee-root.ts.
|
|
48
|
+
*
|
|
49
|
+
* H1 matches `Bash` alone because only Bash can move the shell — the same reason stated at the top of
|
|
50
|
+
* guarantee-root.ts, and the reason the rendered guarantee-root.sh exits 0 immediately for every other
|
|
51
|
+
* tool. Registering it wider would spawn a process per Write/Read to do nothing.
|
|
52
|
+
*
|
|
53
|
+
* ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────
|
|
54
|
+
* Nothing used to validate `.claude/settings.json` at all, so a settings file left on the old
|
|
55
|
+
* two-absolute-hook form silently reverted a repo to per-PRIMARY governance and disabled H1 — the one
|
|
56
|
+
* component whose whole job is failing closed. The installed surface is therefore THREE things
|
|
57
|
+
* (ai-hook.sh, guarantee-root.sh, the registration), all three are compared against this release, and
|
|
58
|
+
* `wp-upgrade-shim` regenerates all three. A cure that fixes one of three is worse than no cure,
|
|
59
|
+
* because it reports success.
|
|
60
|
+
*/
|
|
61
|
+
/** One PreToolUse hook entry as webpieces registers it. Data-only → a class, per CLAUDE.md. */
|
|
62
|
+
class HookRegistrationEntry {
|
|
63
|
+
matcher;
|
|
64
|
+
command;
|
|
65
|
+
constructor(matcher, command) {
|
|
66
|
+
this.matcher = matcher;
|
|
67
|
+
this.command = command;
|
|
68
|
+
}
|
|
69
|
+
sameAs(other) {
|
|
70
|
+
return this.matcher === other.matcher && this.command === other.command;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.HookRegistrationEntry = HookRegistrationEntry;
|
|
74
|
+
exports.RULES_BIN = 'wp-ai-rules-hook';
|
|
75
|
+
exports.GUARDS_BIN = 'wp-ai-guards-hook';
|
|
76
|
+
exports.RULES_MATCHER = 'Write|Edit|MultiEdit';
|
|
77
|
+
// Guards match Bash (git/PR guards), Write|Edit|MultiEdit (file-scoped guards) AND Read — Read carries
|
|
78
|
+
// no guard, but the guards hook owns the per-invocation audit log, so matching Read records every file
|
|
79
|
+
// the AI opens (log-and-allow fast path in hook-core.ts; a Read is never blocked).
|
|
80
|
+
exports.GUARDS_MATCHER = 'Write|Edit|MultiEdit|Bash|Read';
|
|
81
|
+
// Only Bash can move the shell, and moving the shell is the only thing H1 judges.
|
|
82
|
+
exports.GUARANTEE_ROOT_MATCHER = 'Bash';
|
|
83
|
+
/**
|
|
84
|
+
* The RELATIVE guard-hook command — `sh ".claude/webpieces/ai-hook.sh" <bin>`.
|
|
85
|
+
*
|
|
86
|
+
* Relative, NOT `$CLAUDE_PROJECT_DIR/…`: that is the whole point (see the header). Invoked via `sh
|
|
87
|
+
* <file>` rather than executed directly so a missing executable bit on the checked-in shim (fresh
|
|
88
|
+
* clone, a filesystem that drops the bit, git core.fileMode quirks) can never break the hook with a raw
|
|
89
|
+
* `Permission denied` on every tool call. Quoted to survive spaces in the path.
|
|
90
|
+
*/
|
|
91
|
+
// webpieces-disable no-function-outside-class -- this module must load on a tree too broken to build a DI container (upgrade-shim.ts depends on that), so it is module-scope like its siblings shim.ts / guarantee-root.ts
|
|
92
|
+
function shimCommand(bin) {
|
|
93
|
+
return `sh "${shim_1.SHIM_MARKER}" ${bin}`;
|
|
94
|
+
}
|
|
95
|
+
/** H1's command. ABSOLUTE on purpose — it must resolve from ANY cwd or it cannot fail closed. */
|
|
96
|
+
exports.GUARANTEE_ROOT_COMMAND = `sh "$CLAUDE_PROJECT_DIR/${guarantee_root_1.GUARANTEE_ROOT_MARKER}"`;
|
|
97
|
+
exports.GUARANTEE_ROOT_ENTRY = new HookRegistrationEntry(exports.GUARANTEE_ROOT_MATCHER, exports.GUARANTEE_ROOT_COMMAND);
|
|
98
|
+
/** The registration entry for one guard bin. */
|
|
99
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
100
|
+
function guardHookEntry(bin) {
|
|
101
|
+
return new HookRegistrationEntry(bin === exports.RULES_BIN ? exports.RULES_MATCHER : exports.GUARDS_MATCHER, shimCommand(bin));
|
|
102
|
+
}
|
|
103
|
+
/** True when this PreToolUse command is one webpieces owns (either .sh file, in any spelling). */
|
|
104
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
105
|
+
function isManagedCommand(command) {
|
|
106
|
+
return command.includes(shim_1.SHIM_MARKER) || command.includes(guarantee_root_1.GUARANTEE_ROOT_MARKER);
|
|
107
|
+
}
|
|
108
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
109
|
+
function preToolUse(settings) {
|
|
110
|
+
return settings.hooks?.PreToolUse ?? [];
|
|
111
|
+
}
|
|
112
|
+
/** Every webpieces-managed entry of one settings file, flattened to matcher + command pairs. */
|
|
113
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
114
|
+
function managedEntries(settings) {
|
|
115
|
+
const found = [];
|
|
116
|
+
for (const entry of preToolUse(settings)) {
|
|
117
|
+
for (const hook of entry.hooks) {
|
|
118
|
+
if (isManagedCommand(hook.command))
|
|
119
|
+
found.push(new HookRegistrationEntry(entry.matcher, hook.command));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return found;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Which guard bins this settings file registers, in installer order. A file registering NEITHER is not
|
|
126
|
+
* a project (relative) install and is therefore never judged — a global/absolute install names the bin
|
|
127
|
+
* path directly and carries no shim marker at all.
|
|
128
|
+
*/
|
|
129
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
130
|
+
function registeredBins(settings) {
|
|
131
|
+
const commands = managedEntries(settings).map((e) => e.command);
|
|
132
|
+
return [exports.GUARDS_BIN, exports.RULES_BIN].filter((bin) => commands.some((c) => c.includes(bin)));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The exact set of entries THIS RELEASE expects in a settings file that registers `bins`.
|
|
136
|
+
*
|
|
137
|
+
* H1 rides with the GUARDS hook, not the rules hook: H1 judges Bash, and `Bash` is in the guards
|
|
138
|
+
* matcher. A file carrying only the rules hook (the supported split install, where a team ships the
|
|
139
|
+
* guards and a developer keeps the code-style rules local) gets no H1 and needs none.
|
|
140
|
+
*/
|
|
141
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
142
|
+
function expectedEntries(bins) {
|
|
143
|
+
const wanted = [];
|
|
144
|
+
if (bins.includes(exports.GUARDS_BIN))
|
|
145
|
+
wanted.push(exports.GUARANTEE_ROOT_ENTRY);
|
|
146
|
+
for (const bin of bins)
|
|
147
|
+
wanted.push(guardHookEntry(bin));
|
|
148
|
+
return wanted;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* True when a settings file registers webpieces hooks in a shape this release does not expect — the
|
|
152
|
+
* old two-absolute-hook form, a missing guarantee-root entry, a stray duplicate, a wrong matcher.
|
|
153
|
+
*
|
|
154
|
+
* Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order
|
|
155
|
+
* carries no meaning and reordering must not read as drift.
|
|
156
|
+
*/
|
|
157
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
158
|
+
function registrationStale(settings) {
|
|
159
|
+
const bins = registeredBins(settings);
|
|
160
|
+
if (bins.length === 0)
|
|
161
|
+
return false;
|
|
162
|
+
const have = managedEntries(settings);
|
|
163
|
+
const want = expectedEntries(bins);
|
|
164
|
+
if (have.length !== want.length)
|
|
165
|
+
return true;
|
|
166
|
+
return want.some((w) => !have.some((h) => h.sameAs(w)));
|
|
167
|
+
}
|
|
168
|
+
/** Drop every webpieces-managed PreToolUse command; returns true if anything was removed. */
|
|
169
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
170
|
+
function dropManagedEntries(settings) {
|
|
171
|
+
const entries = settings.hooks?.PreToolUse;
|
|
172
|
+
if (!entries)
|
|
173
|
+
return false;
|
|
174
|
+
let changed = false;
|
|
175
|
+
const kept = [];
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const hooks = entry.hooks.filter((h) => !isManagedCommand(h.command));
|
|
178
|
+
if (hooks.length !== entry.hooks.length)
|
|
179
|
+
changed = true;
|
|
180
|
+
if (hooks.length > 0)
|
|
181
|
+
kept.push({ matcher: entry.matcher, hooks });
|
|
182
|
+
}
|
|
183
|
+
if (changed)
|
|
184
|
+
settings.hooks.PreToolUse = kept;
|
|
185
|
+
return changed;
|
|
186
|
+
}
|
|
187
|
+
/** Append one PreToolUse entry. */
|
|
188
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
189
|
+
function addHookEntry(settings, entry) {
|
|
190
|
+
if (!settings.hooks)
|
|
191
|
+
settings.hooks = {};
|
|
192
|
+
if (!Array.isArray(settings.hooks.PreToolUse))
|
|
193
|
+
settings.hooks.PreToolUse = [];
|
|
194
|
+
settings.hooks.PreToolUse.push({ matcher: entry.matcher, hooks: [{ type: 'command', command: entry.command }] });
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Bring one settings object to the three-hook form IN PLACE. Returns true when it changed.
|
|
198
|
+
*
|
|
199
|
+
* REMOVE-then-ADD, never add-beside: two spellings of one registration is exactly the compatibility
|
|
200
|
+
* shim the backwards-compat reviewer rejects, and leaving the `$CLAUDE_PROJECT_DIR/`-prefixed entry
|
|
201
|
+
* beside the relative one would run the PRIMARY's binary alongside the tree's own — the straddle this
|
|
202
|
+
* whole change exists to delete.
|
|
203
|
+
*/
|
|
204
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
205
|
+
function repairRegistration(settings) {
|
|
206
|
+
const bins = registeredBins(settings);
|
|
207
|
+
if (bins.length === 0 || !registrationStale(settings))
|
|
208
|
+
return false;
|
|
209
|
+
dropManagedEntries(settings);
|
|
210
|
+
for (const entry of expectedEntries(bins))
|
|
211
|
+
addHookEntry(settings, entry);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
/** The two project settings files the installer can write. */
|
|
215
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
216
|
+
function projectSettingsPaths(projectRoot) {
|
|
217
|
+
return [
|
|
218
|
+
path.join(projectRoot, '.claude', 'settings.json'),
|
|
219
|
+
path.join(projectRoot, '.claude', 'settings.local.json'),
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
223
|
+
function readSettings(settingsPath) {
|
|
224
|
+
if (!fs.existsSync(settingsPath))
|
|
225
|
+
return {};
|
|
226
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
227
|
+
if (raw.trim() === '')
|
|
228
|
+
return {};
|
|
229
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
230
|
+
try {
|
|
231
|
+
return JSON.parse(raw);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
const error = (0, to_error_1.toError)(err);
|
|
235
|
+
throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
239
|
+
function writeSettings(settingsPath, settings) {
|
|
240
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
241
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
242
|
+
}
|
|
243
|
+
/** True when either project settings file under `root` carries a stale registration. */
|
|
244
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
245
|
+
function registrationStaleAt(root) {
|
|
246
|
+
if (root === null)
|
|
247
|
+
return false;
|
|
248
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
249
|
+
try {
|
|
250
|
+
return projectSettingsPaths(root).some((p) => registrationStale(readSettings(p)));
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
const error = (0, to_error_1.toError)(err);
|
|
254
|
+
void error; // best-effort: unreadable/invalid settings counts as "not stale" so it never wedges a call
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Rewrite every stale project settings file under `root`; returns the paths actually changed. */
|
|
259
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
260
|
+
function repairRegistrationAt(root) {
|
|
261
|
+
const changed = [];
|
|
262
|
+
for (const settingsPath of projectSettingsPaths(root)) {
|
|
263
|
+
if (!fs.existsSync(settingsPath))
|
|
264
|
+
continue;
|
|
265
|
+
const settings = readSettings(settingsPath);
|
|
266
|
+
if (!repairRegistration(settings))
|
|
267
|
+
continue;
|
|
268
|
+
writeSettings(settingsPath, settings);
|
|
269
|
+
changed.push(settingsPath);
|
|
270
|
+
}
|
|
271
|
+
return changed;
|
|
272
|
+
}
|
|
273
|
+
/** The three names the drift check reports, so a deny can say WHICH of them moved. */
|
|
274
|
+
exports.SHIM_SURFACE = shim_1.SHIM_MARKER;
|
|
275
|
+
exports.GUARANTEE_ROOT_SURFACE = guarantee_root_1.GUARANTEE_ROOT_MARKER;
|
|
276
|
+
exports.REGISTRATION_SURFACE = '.claude/settings.json hook registration';
|
|
277
|
+
/**
|
|
278
|
+
* WHICH of the three managed surfaces disagree with this release — the input to fault S.
|
|
279
|
+
*
|
|
280
|
+
* All three are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),
|
|
281
|
+
* never from cwd and never from `$CLAUDE_PROJECT_DIR`: the files we compare and the renderers we
|
|
282
|
+
* compare them TO must come from one install, or the check straddles two trees and can never converge.
|
|
283
|
+
*/
|
|
284
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
285
|
+
function managedSurfaceDrift(root) {
|
|
286
|
+
const drifted = [];
|
|
287
|
+
if ((0, shim_1.committedShimStale)(root))
|
|
288
|
+
drifted.push(exports.SHIM_SURFACE);
|
|
289
|
+
if ((0, guarantee_root_1.committedGuaranteeRootStale)(root) || guaranteeRootRegisteredButMissing(root))
|
|
290
|
+
drifted.push(exports.GUARANTEE_ROOT_SURFACE);
|
|
291
|
+
if (registrationStaleAt(root))
|
|
292
|
+
drifted.push(exports.REGISTRATION_SURFACE);
|
|
293
|
+
return drifted;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The L-1 hook is REGISTERED but its file is gone.
|
|
297
|
+
*
|
|
298
|
+
* `committedGuaranteeRootStale()` answers false for a missing file on purpose — a repo that has not
|
|
299
|
+
* adopted L-1 yet is not "stale", it is simply still on the old registration, and that case is already
|
|
300
|
+
* reported by `registrationStaleAt()`. But a settings file that REGISTERS the hook while the file is
|
|
301
|
+
* absent is the worst case of all: the hook cannot launch, exit 127 is a NON-BLOCKING error, and per
|
|
302
|
+
* the hooks reference the tool call proceeds — every `cd` unjudged, with nothing surfaced. The
|
|
303
|
+
* registration is what distinguishes "not adopted" from "adopted and broken", so it is what is asked.
|
|
304
|
+
*/
|
|
305
|
+
// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above
|
|
306
|
+
function guaranteeRootRegisteredButMissing(root) {
|
|
307
|
+
if (root === null)
|
|
308
|
+
return false;
|
|
309
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
310
|
+
try {
|
|
311
|
+
const registered = projectSettingsPaths(root).some((p) => managedEntries(readSettings(p)).some((e) => e.command === exports.GUARANTEE_ROOT_COMMAND));
|
|
312
|
+
return registered && !fs.existsSync((0, guarantee_root_1.guaranteeRootPath)(root));
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const error = (0, to_error_1.toError)(err);
|
|
316
|
+
void error; // best-effort: unreadable settings counts as "not drifted" so it never wedges a call
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=hook-registration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-registration.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/hook-registration.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, committedShimStale } from './shim';\nimport { GUARANTEE_ROOT_MARKER, committedGuaranteeRootStale, guaranteeRootPath } from './guarantee-root';\n\n/**\n * THE INSTALLED HOOK SURFACE — three hooks, and the ONE place their spelling is defined.\n *\n * ─── Why three, and why two of them are RELATIVE ───────────────────────────────────────────────────\n * `.claude/settings.json` used to register two hooks, BOTH absolute via `$CLAUDE_PROJECT_DIR`. That\n * variable NEVER moves — proven from four separate worktrees' own logs, every line reading\n * `root=<worktree> projectDir=<primary>` — so every tree was governed by the PRIMARY's shim, the\n * PRIMARY's binary and the PRIMARY's pin, forever. A worktree could never be judged by the release its\n * own branch pins, and measuring one tree while running another's binary is the non-convergent\n * \"two-tree straddle\" recorded in shim.ts (an agent gave up after four cures).\n *\n * The hooks reference says \"the hook runs in the `cwd` value from the JSON input\", so a RELATIVE\n * command resolves against the tool call's own tree. Hence:\n *\n * H1 absolute sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/guarantee-root.sh\" matcher Bash\n * H2 relative sh \".claude/webpieces/ai-hook.sh\" wp-ai-guards-hook matcher Write|…|Read\n * H3 relative sh \".claude/webpieces/ai-hook.sh\" wp-ai-rules-hook matcher Write|Edit|MultiEdit\n *\n * H1 stays absolute because it is the one hook that must ALWAYS resolve: a relative hook that cannot\n * resolve exits 127, and per the same reference any non-2 non-zero exit is a NON-BLOCKING error — i.e.\n * a SILENT UNGUARDED ALLOW. H1 refuses any `cd` that would park the shell where H2/H3 cannot launch,\n * which is what makes the relative pair admissible at all. See guarantee-root.ts.\n *\n * H1 matches `Bash` alone because only Bash can move the shell — the same reason stated at the top of\n * guarantee-root.ts, and the reason the rendered guarantee-root.sh exits 0 immediately for every other\n * tool. Registering it wider would spawn a process per Write/Read to do nothing.\n *\n * ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────\n * Nothing used to validate `.claude/settings.json` at all, so a settings file left on the old\n * two-absolute-hook form silently reverted a repo to per-PRIMARY governance and disabled H1 — the one\n * component whose whole job is failing closed. The installed surface is therefore THREE things\n * (ai-hook.sh, guarantee-root.sh, the registration), all three are compared against this release, and\n * `wp-upgrade-shim` regenerates all three. A cure that fixes one of three is worse than no cure,\n * because it reports success.\n */\n\n/** One PreToolUse hook entry as webpieces registers it. Data-only → a class, per CLAUDE.md. */\nexport class HookRegistrationEntry {\n constructor(\n readonly matcher: string,\n readonly command: string,\n ) {}\n\n sameAs(other: HookRegistrationEntry): boolean {\n return this.matcher === other.matcher && this.command === other.command;\n }\n}\n\n// webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON\nexport interface HookCommand { type: string; command: string; }\nexport interface HookEntry { matcher: string; hooks: HookCommand[]; }\nexport interface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport const RULES_BIN = 'wp-ai-rules-hook';\nexport const GUARDS_BIN = 'wp-ai-guards-hook';\nexport const RULES_MATCHER = 'Write|Edit|MultiEdit';\n// Guards match Bash (git/PR guards), Write|Edit|MultiEdit (file-scoped guards) AND Read — Read carries\n// no guard, but the guards hook owns the per-invocation audit log, so matching Read records every file\n// the AI opens (log-and-allow fast path in hook-core.ts; a Read is never blocked).\nexport const GUARDS_MATCHER = 'Write|Edit|MultiEdit|Bash|Read';\n// Only Bash can move the shell, and moving the shell is the only thing H1 judges.\nexport const GUARANTEE_ROOT_MATCHER = 'Bash';\n\n/**\n * The RELATIVE guard-hook command — `sh \".claude/webpieces/ai-hook.sh\" <bin>`.\n *\n * Relative, NOT `$CLAUDE_PROJECT_DIR/…`: that is the whole point (see the header). Invoked via `sh\n * <file>` rather than executed directly so a missing executable bit on the checked-in shim (fresh\n * clone, a filesystem that drops the bit, git core.fileMode quirks) can never break the hook with a raw\n * `Permission denied` on every tool call. Quoted to survive spaces in the path.\n */\n// webpieces-disable no-function-outside-class -- this module must load on a tree too broken to build a DI container (upgrade-shim.ts depends on that), so it is module-scope like its siblings shim.ts / guarantee-root.ts\nexport function shimCommand(bin: string): string {\n return `sh \"${SHIM_MARKER}\" ${bin}`;\n}\n\n/** H1's command. ABSOLUTE on purpose — it must resolve from ANY cwd or it cannot fail closed. */\nexport const GUARANTEE_ROOT_COMMAND = `sh \"$CLAUDE_PROJECT_DIR/${GUARANTEE_ROOT_MARKER}\"`;\n\nexport const GUARANTEE_ROOT_ENTRY = new HookRegistrationEntry(GUARANTEE_ROOT_MATCHER, GUARANTEE_ROOT_COMMAND);\n\n/** The registration entry for one guard bin. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function guardHookEntry(bin: string): HookRegistrationEntry {\n return new HookRegistrationEntry(bin === RULES_BIN ? RULES_MATCHER : GUARDS_MATCHER, shimCommand(bin));\n}\n\n/** True when this PreToolUse command is one webpieces owns (either .sh file, in any spelling). */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function isManagedCommand(command: string): boolean {\n return command.includes(SHIM_MARKER) || command.includes(GUARANTEE_ROOT_MARKER);\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction preToolUse(settings: ClaudeSettings): readonly HookEntry[] {\n return settings.hooks?.PreToolUse ?? [];\n}\n\n/** Every webpieces-managed entry of one settings file, flattened to matcher + command pairs. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedEntries(settings: ClaudeSettings): readonly HookRegistrationEntry[] {\n const found: HookRegistrationEntry[] = [];\n for (const entry of preToolUse(settings)) {\n for (const hook of entry.hooks) {\n if (isManagedCommand(hook.command)) found.push(new HookRegistrationEntry(entry.matcher, hook.command));\n }\n }\n return found;\n}\n\n/**\n * Which guard bins this settings file registers, in installer order. A file registering NEITHER is not\n * a project (relative) install and is therefore never judged — a global/absolute install names the bin\n * path directly and carries no shim marker at all.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registeredBins(settings: ClaudeSettings): readonly string[] {\n const commands = managedEntries(settings).map((e: HookRegistrationEntry): string => e.command);\n return [GUARDS_BIN, RULES_BIN].filter((bin: string): boolean => commands.some((c: string): boolean => c.includes(bin)));\n}\n\n/**\n * The exact set of entries THIS RELEASE expects in a settings file that registers `bins`.\n *\n * H1 rides with the GUARDS hook, not the rules hook: H1 judges Bash, and `Bash` is in the guards\n * matcher. A file carrying only the rules hook (the supported split install, where a team ships the\n * guards and a developer keeps the code-style rules local) gets no H1 and needs none.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function expectedEntries(bins: readonly string[]): readonly HookRegistrationEntry[] {\n const wanted: HookRegistrationEntry[] = [];\n if (bins.includes(GUARDS_BIN)) wanted.push(GUARANTEE_ROOT_ENTRY);\n for (const bin of bins) wanted.push(guardHookEntry(bin));\n return wanted;\n}\n\n/**\n * True when a settings file registers webpieces hooks in a shape this release does not expect — the\n * old two-absolute-hook form, a missing guarantee-root entry, a stray duplicate, a wrong matcher.\n *\n * Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order\n * carries no meaning and reordering must not read as drift.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStale(settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n const have = managedEntries(settings);\n const want = expectedEntries(bins);\n if (have.length !== want.length) return true;\n return want.some((w: HookRegistrationEntry): boolean => !have.some((h: HookRegistrationEntry): boolean => h.sameAs(w)));\n}\n\n/** Drop every webpieces-managed PreToolUse command; returns true if anything was removed. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function dropManagedEntries(settings: ClaudeSettings): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand): boolean => !isManagedCommand(h.command));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\n/** Append one PreToolUse entry. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function addHookEntry(settings: ClaudeSettings, entry: HookRegistrationEntry): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher: entry.matcher, hooks: [{ type: 'command', command: entry.command }] });\n}\n\n/**\n * Bring one settings object to the three-hook form IN PLACE. Returns true when it changed.\n *\n * REMOVE-then-ADD, never add-beside: two spellings of one registration is exactly the compatibility\n * shim the backwards-compat reviewer rejects, and leaving the `$CLAUDE_PROJECT_DIR/`-prefixed entry\n * beside the relative one would run the PRIMARY's binary alongside the tree's own — the straddle this\n * whole change exists to delete.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistration(settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0 || !registrationStale(settings)) return false;\n dropManagedEntries(settings);\n for (const entry of expectedEntries(bins)) addHookEntry(settings, entry);\n return true;\n}\n\n/** The two project settings files the installer can write. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function projectSettingsPaths(projectRoot: string): readonly string[] {\n return [\n path.join(projectRoot, '.claude', 'settings.json'),\n path.join(projectRoot, '.claude', 'settings.local.json'),\n ];\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\n/** True when either project settings file under `root` carries a stale registration. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStaleAt(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return projectSettingsPaths(root).some((p: string): boolean => registrationStale(readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/** Rewrite every stale project settings file under `root`; returns the paths actually changed. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistrationAt(root: string): readonly string[] {\n const changed: string[] = [];\n for (const settingsPath of projectSettingsPaths(root)) {\n if (!fs.existsSync(settingsPath)) continue;\n const settings = readSettings(settingsPath);\n if (!repairRegistration(settings)) continue;\n writeSettings(settingsPath, settings);\n changed.push(settingsPath);\n }\n return changed;\n}\n\n/** The three names the drift check reports, so a deny can say WHICH of them moved. */\nexport const SHIM_SURFACE = SHIM_MARKER;\nexport const GUARANTEE_ROOT_SURFACE = GUARANTEE_ROOT_MARKER;\nexport const REGISTRATION_SURFACE = '.claude/settings.json hook registration';\n\n/**\n * WHICH of the three managed surfaces disagree with this release — the input to fault S.\n *\n * All three are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),\n * never from cwd and never from `$CLAUDE_PROJECT_DIR`: the files we compare and the renderers we\n * compare them TO must come from one install, or the check straddles two trees and can never converge.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedSurfaceDrift(root: string | null): readonly string[] {\n const drifted: string[] = [];\n if (committedShimStale(root)) drifted.push(SHIM_SURFACE);\n if (committedGuaranteeRootStale(root) || guaranteeRootRegisteredButMissing(root)) drifted.push(GUARANTEE_ROOT_SURFACE);\n if (registrationStaleAt(root)) drifted.push(REGISTRATION_SURFACE);\n return drifted;\n}\n\n/**\n * The L-1 hook is REGISTERED but its file is gone.\n *\n * `committedGuaranteeRootStale()` answers false for a missing file on purpose — a repo that has not\n * adopted L-1 yet is not \"stale\", it is simply still on the old registration, and that case is already\n * reported by `registrationStaleAt()`. But a settings file that REGISTERS the hook while the file is\n * absent is the worst case of all: the hook cannot launch, exit 127 is a NON-BLOCKING error, and per\n * the hooks reference the tool call proceeds — every `cd` unjudged, with nothing surfaced. The\n * registration is what distinguishes \"not adopted\" from \"adopted and broken\", so it is what is asked.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction guaranteeRootRegisteredButMissing(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const registered = projectSettingsPaths(root).some((p: string): boolean =>\n managedEntries(readSettings(p)).some((e: HookRegistrationEntry): boolean => e.command === GUARANTEE_ROOT_COMMAND));\n return registered && !fs.existsSync(guaranteeRootPath(root));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable settings counts as \"not drifted\" so it never wedges a call\n return false;\n }\n}\n"]}
|
|
@@ -2,3 +2,18 @@
|
|
|
2
2
|
export declare function recoveryNotice(detail: string, shimRefreshed: boolean): string[];
|
|
3
3
|
export declare function isBrokenTreeError(error: Error): boolean;
|
|
4
4
|
export declare function runInstaller(cwd: string): Promise<number>;
|
|
5
|
+
/**
|
|
6
|
+
* THE PROCESS ENTRY POINT — absent from every release up to and including 0.4.588, exactly as it was
|
|
7
|
+
* absent from upgrade-shim.ts. `pnpm exec wp-install-ai-hooks` loaded this module, defined
|
|
8
|
+
* `isBrokenTreeError` and `runInstaller`, and exited 0 without installing anything: the shim was never
|
|
9
|
+
* healed, `./setup` was never required, and the "broken tree" recovery notice this file exists to print
|
|
10
|
+
* could never be reached. Verified against the published 0.4.576 artifact — `node
|
|
11
|
+
* node_modules/@webpieces/ai-hook-rules/src/bin/install-entry.js` produced no output and exit 0.
|
|
12
|
+
*
|
|
13
|
+
* `runMain` from @webpieces/rules-config is deliberately NOT used: this module must load with fs+path
|
|
14
|
+
* only (see the header) so it survives the corrupt node_modules it is meant to recover from, and
|
|
15
|
+
* importing the wrapper would reintroduce the require-time crash. `main()` is the sanctioned single
|
|
16
|
+
* exit site instead, and `bin-process-entry.spec.ts` SPAWNS this file (with the lazily-required
|
|
17
|
+
* `./setup` stubbed, so the launcher is what is under test) so it cannot be dropped again silently.
|
|
18
|
+
*/
|
|
19
|
+
export declare function main(): Promise<void>;
|
package/src/bin/install-entry.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.recoveryNotice = recoveryNotice;
|
|
5
5
|
exports.isBrokenTreeError = isBrokenTreeError;
|
|
6
6
|
exports.runInstaller = runInstaller;
|
|
7
|
+
exports.main = main;
|
|
7
8
|
const shim_1 = require("./shim");
|
|
8
9
|
const to_error_1 = require("../core/to-error");
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -103,4 +104,25 @@ async function runInstaller(cwd) {
|
|
|
103
104
|
return 1;
|
|
104
105
|
}
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* THE PROCESS ENTRY POINT — absent from every release up to and including 0.4.588, exactly as it was
|
|
109
|
+
* absent from upgrade-shim.ts. `pnpm exec wp-install-ai-hooks` loaded this module, defined
|
|
110
|
+
* `isBrokenTreeError` and `runInstaller`, and exited 0 without installing anything: the shim was never
|
|
111
|
+
* healed, `./setup` was never required, and the "broken tree" recovery notice this file exists to print
|
|
112
|
+
* could never be reached. Verified against the published 0.4.576 artifact — `node
|
|
113
|
+
* node_modules/@webpieces/ai-hook-rules/src/bin/install-entry.js` produced no output and exit 0.
|
|
114
|
+
*
|
|
115
|
+
* `runMain` from @webpieces/rules-config is deliberately NOT used: this module must load with fs+path
|
|
116
|
+
* only (see the header) so it survives the corrupt node_modules it is meant to recover from, and
|
|
117
|
+
* importing the wrapper would reintroduce the require-time crash. `main()` is the sanctioned single
|
|
118
|
+
* exit site instead, and `bin-process-entry.spec.ts` SPAWNS this file (with the lazily-required
|
|
119
|
+
* `./setup` stubbed, so the launcher is what is under test) so it cannot be dropped again silently.
|
|
120
|
+
*/
|
|
121
|
+
// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.
|
|
122
|
+
async function main() {
|
|
123
|
+
process.exit(await runInstaller(process.cwd()));
|
|
124
|
+
}
|
|
125
|
+
if (require.main === module) {
|
|
126
|
+
void main();
|
|
127
|
+
}
|
|
106
128
|
//# sourceMappingURL=install-entry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-entry.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/install-entry.ts"],"names":[],"mappings":";;;AAkCA,wCAuBC;AAiBD,8CAEC;AAKD,oCAsBC;AAtGD,iCAA8D;AAC9D,+CAA2C;AAE3C,8EAA8E;AAC9E,kFAAkF;AAClF,EAAE;AACF,yFAAyF;AACzF,oGAAoG;AACpG,sGAAsG;AACtG,qGAAqG;AACrG,oGAAoG;AACpG,sGAAsG;AACtG,kGAAkG;AAClG,2FAA2F;AAC3F,EAAE;AACF,mGAAmG;AACnG,4FAA4F;AAC5F,EAAE;AACF,kGAAkG;AAClG,mGAAmG;AACnG,oGAAoG;AACpG,qGAAqG;AACrG,8FAA8F;AAC9F,8EAA8E;AAE9E,iGAAiG;AACjG,8CAA8C;AAC9C,MAAM,GAAG,GAAG,SAAS,CAAC;AACtB,MAAM,KAAK,GAAG,MAAM,CAAC;AAErB,sGAAsG;AACtG,+EAA+E;AAC/E,iOAAiO;AACjO,SAAgB,cAAc,CAAC,MAAc,EAAE,aAAsB;IACjE,MAAM,KAAK,GAAG;QACV,GAAG,GAAG,0EAA0E,KAAK,EAAE;QACvF,EAAE;QACF,KAAK,MAAM,EAAE;QACb,EAAE;QACF,8FAA8F;QAC9F,6FAA6F;QAC7F,mEAAmE;QACnE,EAAE;QACF,GAAG,GAAG,kCAAkC,KAAK,EAAE;QAC/C,GAAG,GAAG,OAAO,mBAAY,GAAG,KAAK,EAAE;QACnC,EAAE;KACL,CAAC;IACF,IAAI,aAAa,EAAE,CAAC;QAChB,8FAA8F;QAC9F,sFAAsF;QACtF,KAAK,CAAC,IAAI,CAAC,uFAAuF,CAAC,CAAC;QACpG,KAAK,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;IAChG,CAAC;SAAM,CAAC;QACJ,KAAK,CAAC,IAAI,CAAC,mFAAmF,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,0FAA0F;AAC1F,MAAM,gBAAiB,SAAQ,KAAK;IACvB,IAAI,CAAU;CAC1B;AAED,qGAAqG;AACrG,MAAM,WAAW;IACb,IAAI,CAAuB;CAC9B;AAED,uGAAuG;AACvG,qGAAqG;AACrG,uGAAuG;AACvG,oGAAoG;AACpG,iOAAiO;AACjO,SAAgB,iBAAiB,CAAC,KAAY;IAC1C,OAAQ,KAA0B,CAAC,IAAI,KAAK,kBAAkB,CAAC;AACnE,CAAC;AAED,iGAAiG;AACjG,kCAAkC;AAClC,iOAAiO;AAC1N,KAAK,UAAU,YAAY,CAAC,GAAW;IAC1C,6FAA6F;IAC7F,8FAA8F;IAC9F,MAAM,aAAa,GAAG,IAAA,mBAAY,EAAC,GAAG,CAAC,KAAK,IAAI,CAAC;IACjD,IAAA,eAAQ,EAAC,GAAG,CAAC,CAAC;IAEd,kFAAkF;IAClF,gGAAgG;IAChG,kGAAkG;IAClG,8NAA8N;IAC9N,8DAA8D;IAC9D,IAAI,CAAC;QACD,iEAAiE;QACjE,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAgB,CAAC;QAChD,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO,CAAC,CAAC;IACb,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAC,CAAG,mDAAmD;QACjG,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC;YAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpG,OAAO,CAAC,CAAC;IACb,CAAC;AACL,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { healShim, findShimRoot, RECOVERY_CMD } from './shim';\nimport { toError } from '../core/to-error';\n\n// ---------------------------------------------------------------------------\n// The `wp-install-ai-hooks` entry point, deliberately kept FREE of heavy imports.\n//\n// THE BUG THIS FIXES — the installer was a victim of the very corruption it must repair.\n// setup.ts top-level-imports @webpieces/rules-config, which imports minimatch. When node_modules is\n// corrupt — a package half-written by an install that was killed mid-copy — node dies at REQUIRE time\n// with MODULE_NOT_FOUND, before a single line of the installer runs. So `pnpm wp-install-ai-hooks` —\n// the one command that can rewrite the fail-closed shim — died with a raw 30-line node loader trace\n// and repaired nothing. Worse, `pnpm install` cannot heal that corruption either (pnpm sees the right\n// version on disk, considers the package installed, and skips it), so the repo was wedged: guards\n// down, installer unable to reinstall them, and the only signal an unreadable stack trace.\n//\n// The seam: ./shim imports NOTHING but fs and path. Writing the shim never needed the rule engine.\n// So do the repair FIRST with the dependency-free module, and only then reach for setup.ts:\n//\n// 1. healShim(cwd) — refresh the committed shim. Works on a corrupt tree. This re-arms the\n// fail-closed gate, so guards-being-down becomes LOUD instead of silent.\n// 2. require('./setup') — the full install (config seeding, settings.json wiring). Needs the rule\n// engine, so it loads LAZILY: a corrupt tree can no longer preempt step 1.\n// 3. MODULE_NOT_FOUND — print the one command that actually repairs it, not a node stack.\n// ---------------------------------------------------------------------------\n\n// ANSI red — the recovery command is the whole point of this message, so it must not scroll past\n// unnoticed the way the old loader trace did.\nconst RED = '\u001b[31;1m';\nconst RESET = '\u001b[0m';\n\n// The human-facing recovery notice, printed to stderr INSTEAD of a raw node loader trace. Mirrors the\n// shim's deny reason so the terminal and the AI tell the human the same story.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.\nexport function recoveryNotice(detail: string, shimRefreshed: boolean): string[] {\n const lines = [\n `${RED}🛑 @webpieces: cannot run the installer — your node_modules is corrupt.${RESET}`,\n '',\n ` ${detail}`,\n '',\n ' A package is only partially written on disk (usually an install that was killed mid-copy).',\n ' A plain `pnpm install` will NOT fix it: pnpm sees the correct version in the package.json',\n ' already on disk, considers the package installed, and skips it.',\n '',\n `${RED} Run exactly this, then retry:${RESET}`,\n `${RED} ${RECOVERY_CMD}${RESET}`,\n '',\n ];\n if (shimRefreshed) {\n // The important half of the job still got done: the fail-closed gate is current, so the AI is\n // BLOCKED (citing this same command) rather than silently editing behind dead guards.\n lines.push(' The AI guard shim WAS refreshed, so tool calls are now BLOCKED until you repair the');\n lines.push(' tree. That is intentional — it is what stops the AI from working unguarded.');\n } else {\n lines.push(' No committed shim was found to refresh. Re-run this installer after the repair.');\n }\n return lines;\n}\n\n// Node stamps a `code` onto require() failures, which the base Error type does not carry.\nclass NodeRequireError extends Error {\n readonly code?: string;\n}\n\n// The lazily-required ./setup module. Named (not an inline literal) so the require cast stays typed.\nclass SetupModule {\n main!: () => Promise<void>;\n}\n\n// A require() failure from a corrupt / partially-written node_modules. Node reports a missing relative\n// specifier ('./assert-valid-pattern.js' — a package's own file absent from disk) and a missing bare\n// specifier ('@webpieces/rules-config' — package not installed at all) with the SAME code. Both mean a\n// broken tree from the installer's point of view, and both have the same cure, so both map to true.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.\nexport function isBrokenTreeError(error: Error): boolean {\n return (error as NodeRequireError).code === 'MODULE_NOT_FOUND';\n}\n\n// Returns the process exit code (0 = ok). Kept as a function (not top-level code) so it is unit-\n// testable without spawning node.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.\nexport async function runInstaller(cwd: string): Promise<number> {\n // STEP 1 — re-arm the fail-closed gate FIRST, via the dependency-free module. healShim never\n // throws and only rewrites a shim that ALREADY exists, so a global install is left untouched.\n const shimRefreshed = findShimRoot(cwd) !== null;\n healShim(cwd);\n\n // STEP 2 — the real install, loaded LAZILY ON PURPOSE. A static import would drag\n // @webpieces/rules-config → minimatch in at module-load time and a corrupt tree would kill this\n // file before step 1 ever ran — which is precisely the failure being fixed here. Do not hoist it.\n // webpieces-disable no-unmanaged-exceptions -- this IS the top-level chokepoint: the bin entry. It exists to turn a raw MODULE_NOT_FOUND loader trace into an actionable recovery message; a real bug is re-thrown untouched.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const setup = require('./setup') as SetupModule;\n await setup.main();\n return 0;\n } catch (err: unknown) {\n const error = toError(err);\n if (!isBrokenTreeError(error)) throw error; // a real bug — never hide it behind a nice message\n for (const line of recoveryNotice(error.message.split('\\n')[0], shimRefreshed)) console.error(line);\n return 1;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"install-entry.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/install-entry.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { healShim, findShimRoot, RECOVERY_CMD } from './shim';\nimport { toError } from '../core/to-error';\n\n// ---------------------------------------------------------------------------\n// The `wp-install-ai-hooks` entry point, deliberately kept FREE of heavy imports.\n//\n// THE BUG THIS FIXES — the installer was a victim of the very corruption it must repair.\n// setup.ts top-level-imports @webpieces/rules-config, which imports minimatch. When node_modules is\n// corrupt — a package half-written by an install that was killed mid-copy — node dies at REQUIRE time\n// with MODULE_NOT_FOUND, before a single line of the installer runs. So `pnpm wp-install-ai-hooks` —\n// the one command that can rewrite the fail-closed shim — died with a raw 30-line node loader trace\n// and repaired nothing. Worse, `pnpm install` cannot heal that corruption either (pnpm sees the right\n// version on disk, considers the package installed, and skips it), so the repo was wedged: guards\n// down, installer unable to reinstall them, and the only signal an unreadable stack trace.\n//\n// The seam: ./shim imports NOTHING but fs and path. Writing the shim never needed the rule engine.\n// So do the repair FIRST with the dependency-free module, and only then reach for setup.ts:\n//\n// 1. healShim(cwd) — refresh the committed shim. Works on a corrupt tree. This re-arms the\n// fail-closed gate, so guards-being-down becomes LOUD instead of silent.\n// 2. require('./setup') — the full install (config seeding, settings.json wiring). Needs the rule\n// engine, so it loads LAZILY: a corrupt tree can no longer preempt step 1.\n// 3. MODULE_NOT_FOUND — print the one command that actually repairs it, not a node stack.\n// ---------------------------------------------------------------------------\n\n// ANSI red — the recovery command is the whole point of this message, so it must not scroll past\n// unnoticed the way the old loader trace did.\nconst RED = '\u001b[31;1m';\nconst RESET = '\u001b[0m';\n\n// The human-facing recovery notice, printed to stderr INSTEAD of a raw node loader trace. Mirrors the\n// shim's deny reason so the terminal and the AI tell the human the same story.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.\nexport function recoveryNotice(detail: string, shimRefreshed: boolean): string[] {\n const lines = [\n `${RED}🛑 @webpieces: cannot run the installer — your node_modules is corrupt.${RESET}`,\n '',\n ` ${detail}`,\n '',\n ' A package is only partially written on disk (usually an install that was killed mid-copy).',\n ' A plain `pnpm install` will NOT fix it: pnpm sees the correct version in the package.json',\n ' already on disk, considers the package installed, and skips it.',\n '',\n `${RED} Run exactly this, then retry:${RESET}`,\n `${RED} ${RECOVERY_CMD}${RESET}`,\n '',\n ];\n if (shimRefreshed) {\n // The important half of the job still got done: the fail-closed gate is current, so the AI is\n // BLOCKED (citing this same command) rather than silently editing behind dead guards.\n lines.push(' The AI guard shim WAS refreshed, so tool calls are now BLOCKED until you repair the');\n lines.push(' tree. That is intentional — it is what stops the AI from working unguarded.');\n } else {\n lines.push(' No committed shim was found to refresh. Re-run this installer after the repair.');\n }\n return lines;\n}\n\n// Node stamps a `code` onto require() failures, which the base Error type does not carry.\nclass NodeRequireError extends Error {\n readonly code?: string;\n}\n\n// The lazily-required ./setup module. Named (not an inline literal) so the require cast stays typed.\nclass SetupModule {\n main!: () => Promise<void>;\n}\n\n// A require() failure from a corrupt / partially-written node_modules. Node reports a missing relative\n// specifier ('./assert-valid-pattern.js' — a package's own file absent from disk) and a missing bare\n// specifier ('@webpieces/rules-config' — package not installed at all) with the SAME code. Both mean a\n// broken tree from the installer's point of view, and both have the same cure, so both map to true.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.\nexport function isBrokenTreeError(error: Error): boolean {\n return (error as NodeRequireError).code === 'MODULE_NOT_FOUND';\n}\n\n// Returns the process exit code (0 = ok). Kept as a function (not top-level code) so it is unit-\n// testable without spawning node.\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.\nexport async function runInstaller(cwd: string): Promise<number> {\n // STEP 1 — re-arm the fail-closed gate FIRST, via the dependency-free module. healShim never\n // throws and only rewrites a shim that ALREADY exists, so a global install is left untouched.\n const shimRefreshed = findShimRoot(cwd) !== null;\n healShim(cwd);\n\n // STEP 2 — the real install, loaded LAZILY ON PURPOSE. A static import would drag\n // @webpieces/rules-config → minimatch in at module-load time and a corrupt tree would kill this\n // file before step 1 ever ran — which is precisely the failure being fixed here. Do not hoist it.\n // webpieces-disable no-unmanaged-exceptions -- this IS the top-level chokepoint: the bin entry. It exists to turn a raw MODULE_NOT_FOUND loader trace into an actionable recovery message; a real bug is re-thrown untouched.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const setup = require('./setup') as SetupModule;\n await setup.main();\n return 0;\n } catch (err: unknown) {\n const error = toError(err);\n if (!isBrokenTreeError(error)) throw error; // a real bug — never hide it behind a nice message\n for (const line of recoveryNotice(error.message.split('\\n')[0], shimRefreshed)) console.error(line);\n return 1;\n }\n}\n\n/**\n * THE PROCESS ENTRY POINT — absent from every release up to and including 0.4.588, exactly as it was\n * absent from upgrade-shim.ts. `pnpm exec wp-install-ai-hooks` loaded this module, defined\n * `isBrokenTreeError` and `runInstaller`, and exited 0 without installing anything: the shim was never\n * healed, `./setup` was never required, and the \"broken tree\" recovery notice this file exists to print\n * could never be reached. Verified against the published 0.4.576 artifact — `node\n * node_modules/@webpieces/ai-hook-rules/src/bin/install-entry.js` produced no output and exit 0.\n *\n * `runMain` from @webpieces/rules-config is deliberately NOT used: this module must load with fs+path\n * only (see the header) so it survives the corrupt node_modules it is meant to recover from, and\n * importing the wrapper would reintroduce the require-time crash. `main()` is the sanctioned single\n * exit site instead, and `bin-process-entry.spec.ts` SPAWNS this file (with the lazily-required\n * `./setup` stubbed, so the launcher is what is under test) so it cannot be dropped again silently.\n */\n// webpieces-disable no-function-outside-class -- bin entry point: this module MUST load with only fs+path (see header). A DI-managed class would pull the container in and reintroduce the exact require-time crash being fixed.\nexport async function main(): Promise<void> {\n process.exit(await runInstaller(process.cwd()));\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
|
package/src/bin/setup.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderShim } from './shim';
|
|
2
|
+
import { ClaudeSettings } from './hook-registration';
|
|
2
3
|
export { renderShim };
|
|
3
4
|
declare class HookSpec {
|
|
4
5
|
readonly key: string;
|
|
@@ -35,21 +36,6 @@ interface MigrateResult {
|
|
|
35
36
|
changes: string[];
|
|
36
37
|
}
|
|
37
38
|
export declare function migrate(existing: Json): MigrateResult;
|
|
38
|
-
interface HookCommand {
|
|
39
|
-
type: string;
|
|
40
|
-
command: string;
|
|
41
|
-
}
|
|
42
|
-
interface HookEntry {
|
|
43
|
-
matcher: string;
|
|
44
|
-
hooks: HookCommand[];
|
|
45
|
-
}
|
|
46
|
-
interface ClaudeSettings {
|
|
47
|
-
hooks?: {
|
|
48
|
-
PreToolUse?: HookEntry[];
|
|
49
|
-
};
|
|
50
|
-
[key: string]: unknown;
|
|
51
|
-
}
|
|
52
|
-
export declare function readSettings(settingsPath: string): ClaudeSettings;
|
|
53
39
|
export declare function hasHook(settings: ClaudeSettings, bin: string): boolean;
|
|
54
40
|
export declare function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void;
|
|
55
41
|
export declare function resolveTargetChoice(name: string): string | null;
|