@webpieces/ai-hook-rules 0.4.630 → 0.4.632
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/package.json +2 -2
- package/src/adapters/claude-code-response.js +16 -1
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +1 -1
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/l0-allowlist.d.ts +6 -2
- package/src/bin/l0-allowlist.js +60 -16
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +4 -3
- package/src/bin/shim-audit-log.js +40 -12
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +0 -5
- package/src/bin/shim-deny-reason.js +199 -108
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim-drift-fix.d.ts +1 -0
- package/src/bin/shim-drift-fix.js +53 -0
- package/src/bin/shim-drift-fix.js.map +1 -0
- package/src/bin/shim-testkit.d.ts +11 -0
- package/src/bin/shim-testkit.js +15 -0
- package/src/bin/shim-testkit.js.map +1 -1
- package/src/bin/shim.js +58 -15
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +12 -4
- package/src/core/decision-log.js +13 -5
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-fault-codes.d.ts +46 -0
- package/src/core/l0-fault-codes.js +55 -1
- package/src/core/l0-fault-codes.js.map +1 -1
- package/src/core/l0-matrix.d.ts +23 -4
- package/src/core/l0-matrix.js +74 -24
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/runner.js +1 -1
- package/src/core/runner.js.map +1 -1
- package/templates/ai-hook.sh +73 -23
package/templates/ai-hook.sh
CHANGED
|
@@ -14,6 +14,12 @@ shift
|
|
|
14
14
|
# Resolve the tree relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
|
|
15
15
|
# caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
|
|
16
16
|
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
|
17
|
+
# The JSON escapes every deny message below is assembled from (ANSI red, and the newlines that give the
|
|
18
|
+
# deny the same scannable shape formatReport() gives every L1/L2 deny). See ESCAPES_SH's header.
|
|
19
|
+
BS='\' # one literal backslash, so no \u001b / \n escape sits in this source
|
|
20
|
+
ESC="${BS}u001b" # the 6 chars: backslash u 0 0 1 b — Claude Code parses \u001b → ESC
|
|
21
|
+
NL="${BS}n" # the 2 chars: backslash n — parsed as a real newline inside the JSON string
|
|
22
|
+
WP_STILL_ALLOWED="Still allowed while this block is up:${NL} - any Read${NL} - any Write/Edit whose target is webpieces.config.json${NL} - every command on the L0 allowlist, including the Fix Options below${NL} THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human."
|
|
17
23
|
# The BIN is resolved by walking UP from ROOT (as Node does), and BIN_ROOT records which tree supplied
|
|
18
24
|
# it — the version-drift guard below compares THIS tree's pin against THAT tree's installed version.
|
|
19
25
|
BIN_ROOT="$ROOT"
|
|
@@ -113,7 +119,7 @@ WP_INSTALL_CMD="pnpm install"
|
|
|
113
119
|
WP_BORROW_NOTE=""
|
|
114
120
|
if [ "$BIN_ROOT" != "$ROOT" ]; then
|
|
115
121
|
WP_INSTALL_CMD="cd $ROOT && pnpm install"
|
|
116
|
-
WP_BORROW_NOTE="
|
|
122
|
+
WP_BORROW_NOTE="${NL} NOTE: this tree ($ROOT) has NO node_modules of its own, so the guard binary was inherited from $BIN_ROOT by walking up. TWO cures are real and they fix DIFFERENT things: A makes THIS tree work now, B stops the two trees disagreeing. A - run the command above HERE. That is legitimate and it does work; a worktree NEEDS its own node_modules anyway (nx, vitest and the eslint plugin all execute in this tree and load from it). B - get $BIN_ROOT onto the same @webpieces version: put both trees on the same git hash (the pin is tracked) and run ONE 'pnpm install' there. If you are a SUBAGENT you cannot reach that tree, so ESCALATE - ask the coordinator to run 'pnpm install' in the main tree so both trees are on the same @webpieces version. The rule is NOT no-install-here: it is that this tree's @webpieces must EQUAL $BIN_ROOT's, because doing only A leaves two trees on two releases (the trinary-version-skew guard then BLOCKS rather than letting it pass unnoticed). Adding an ordinary third-party dependency here changes none of that - only a differing @webpieces version does. If this tree genuinely needs a DIFFERENT version, use a separate clone rather than a worktree."
|
|
117
123
|
fi
|
|
118
124
|
# Read the tool payload ONCE, up front. The shim no longer exec's the bin (see RUN_BIN_SH), so it must
|
|
119
125
|
# forward stdin to the bin itself — and it needs the payload again on the fail-closed path below.
|
|
@@ -135,12 +141,15 @@ WP_CWD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\(
|
|
|
135
141
|
# channel — a stray byte there would corrupt allow/deny).
|
|
136
142
|
WP_TREE=""
|
|
137
143
|
WP_LOG_DIR=""
|
|
144
|
+
WP_PRIMARY_LOG_DIR=""
|
|
145
|
+
WP_TAB="$(printf '\t')" # one real tab, so the OPTIONAL bin= field can carry its own separator
|
|
138
146
|
wp_resolve_log_dir() {
|
|
139
147
|
_wp_rp="$(git -C "$WP_CWD" rev-parse --git-dir --git-common-dir 2>/dev/null)"
|
|
140
148
|
_wp_gd="$(printf '%s\n' "$_wp_rp" | sed -n 1p)"
|
|
141
149
|
_wp_cd="$(printf '%s\n' "$_wp_rp" | sed -n 2p)"
|
|
142
150
|
if [ -z "$_wp_gd" ] || [ -z "$_wp_cd" ]; then
|
|
143
|
-
WP_TREE=primary; WP_LOG_DIR="$WP_CWD/.webpieces/logs"
|
|
151
|
+
WP_TREE=primary; WP_LOG_DIR="$WP_CWD/.webpieces/logs"
|
|
152
|
+
WP_PRIMARY_LOG_DIR="$WP_LOG_DIR"; return 0
|
|
144
153
|
fi
|
|
145
154
|
# git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS
|
|
146
155
|
# twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.
|
|
@@ -152,9 +161,14 @@ wp_resolve_log_dir() {
|
|
|
152
161
|
case "$_wp_cd" in
|
|
153
162
|
*/.git) [ -d "${_wp_cd%/*}" ] && _wp_primary="${_wp_cd%/*}" ;;
|
|
154
163
|
esac
|
|
164
|
+
# The PRIMARY clone's log dir, resolved on both branches. A deny that has to tell a human WHERE the
|
|
165
|
+
# audit trail is (the inverse-drift escalation in shim.ts) must be able to name both the tree it is
|
|
166
|
+
# standing in and the primary — a subagent has no reach into the second one, so the deny has to quote
|
|
167
|
+
# that path rather than send anyone to go and look.
|
|
168
|
+
WP_PRIMARY_LOG_DIR="$_wp_primary/.webpieces/logs"
|
|
155
169
|
if [ "$_wp_gd" = "$_wp_cd" ]; then
|
|
156
170
|
WP_TREE=primary
|
|
157
|
-
WP_LOG_DIR="$
|
|
171
|
+
WP_LOG_DIR="$WP_PRIMARY_LOG_DIR"
|
|
158
172
|
else
|
|
159
173
|
# git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the
|
|
160
174
|
# directory's basename — two worktrees under different parents may share a directory name.
|
|
@@ -191,14 +205,33 @@ wp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label
|
|
|
191
205
|
# decide whether a tree was governed by its OWN release or a borrowed one:
|
|
192
206
|
# shim= WHICH COPY OF ai-hook.sh RAN — $ROOT, resolved from $0. The file is TRACKED, so every
|
|
193
207
|
# worktree carries the version at ITS commit; settings.json registers it ABSOLUTE, so the copy
|
|
194
|
-
# that runs is the SESSION ROOT's. Logged rather than assumed
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
208
|
+
# that runs is the SESSION ROOT's. Logged rather than assumed, on EVERY line: compared against
|
|
209
|
+
# tree= it is the STRADDLE detector (tree=agent-X shim=<repo> = standing in one tree, judged by
|
|
210
|
+
# another), and that pair varies constantly.
|
|
211
|
+
# bin= WHICH TREE SUPPLIED THE BINARY — $BIN_ROOT, the upward walk's answer.
|
|
212
|
+
#
|
|
213
|
+
# bin= IS PRINTED ONLY WHEN IT DIFFERS FROM shim=, so its mere PRESENCE is the diagnostic ("the binary
|
|
214
|
+
# came from a different tree than the shim") instead of ~50 bytes repeated on every line. Measured
|
|
215
|
+
# across 549 logged lines: it differed on 39, every one a worktree agent's first few calls before it
|
|
216
|
+
# ran pnpm install — after that they matched for the rest of that agent's life. And since the hooks
|
|
217
|
+
# went ABSOLUTE, shim= is always the MAIN tree, so the two can now only differ when the main tree
|
|
218
|
+
# itself has no node_modules (a fresh clone before install). ~7% of lines then, near 0% going forward.
|
|
219
|
+
# A unit test asserts the field appears if and only if the roots differ, so it cannot quietly become
|
|
220
|
+
# unconditional noise again.
|
|
221
|
+
_wp_bin=""
|
|
222
|
+
[ "$BIN_ROOT" != "$ROOT" ] && _wp_bin="bin=$BIN_ROOT$WP_TAB"
|
|
223
|
+
# layer= and row= are the JOIN KEYS, and they are here so the join is REAL rather than promised.
|
|
224
|
+
# Every L0 deny now opens '[<guard>] (layer=L0 fault=<code> row=<n>)' and cites "the same coordinates
|
|
225
|
+
# the audit line carries" — which was true of the JS half (MATRIX_L0_BLOCK, via decision-log) and
|
|
226
|
+
# FALSE of this one, which carried 'fault=' alone. Fixing the message instead of the line would have
|
|
227
|
+
# left 'grep 'layer=L0 row=3'' finding one half of L0 and silently missing the other four faults.
|
|
228
|
+
#
|
|
229
|
+
# 'row=' is NOT a constant: it is the row of the three-row matrix this call actually took, read off
|
|
230
|
+
# the verdict — hand-down, allowlisted, or blocked — exactly as L1 logs 'row=' from L1_ROWS. That is
|
|
231
|
+
# what distinguishes it from the ~50 constant bytes 'bin=' used to spend above.
|
|
232
|
+
_wp_row=1
|
|
233
|
+
case "$2" in ALLOW*) _wp_row=2 ;; DENY*) _wp_row=3 ;; esac
|
|
234
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s%s\t%s\t%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "tree=$WP_TREE" "layer=L0" "row=$_wp_row" "shim=$ROOT" "$_wp_bin" "fault=$1" "$2" "$CMD_LOG" >> "$_wp_f"
|
|
202
235
|
} 2>/dev/null || true
|
|
203
236
|
}
|
|
204
237
|
BROKEN_BIN=""
|
|
@@ -258,7 +291,7 @@ case "$FILE" in
|
|
|
258
291
|
wp_log "$WP_FAULT" ALLOW-CONFIG # the always-allowed recovery target — every guard is configured from it
|
|
259
292
|
exit 0 ;;
|
|
260
293
|
esac
|
|
261
|
-
if printf '%s' "$CMD" | grep -Eq '^(cd[[:space:]]+([A-Za-z0-9._/@~+-]+|'\''[^'\'']+'\'')[[:space:]]*&&[[:space:]]*)?((pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*|rm[[:space:]]+-rf[[:space:]]+(\./)?node_modules/?([[:space:]]*&&[[:space:]]*(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?|git[[:space:]]+
|
|
294
|
+
if printf '%s' "$CMD" | grep -Eq '^(cd[[:space:]]+([A-Za-z0-9._/@~+-]+|'\''[^'\'']+'\'')[[:space:]]*&&[[:space:]]*)?((pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*|rm[[:space:]]+-rf[[:space:]]+(\./)?node_modules/?([[:space:]]*&&[[:space:]]*(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?|git[[:space:]]+fetch([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*|git[[:space:]]+checkout[[:space:]]+main[[:space:]]*&&[[:space:]]*git[[:space:]]+pull[[:space:]]+origin[[:space:]]+main|(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-upgrade-shim|cp[[:space:]]+(\./)?node_modules/@webpieces/ai-hook-rules/templates/ai-hook\.sh[[:space:]]+(\./)?\.claude/webpieces/ai-hook\.sh|(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-prune-unknown-config|(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-install-ai-hooks([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*|(pnpm|npm)[[:space:]]+add([[:space:]]+(-[A-Za-z]|--[A-Za-z][A-Za-z0-9=._/@:-]*))*[[:space:]]+@webpieces/ai-hook-rules(@[A-Za-z0-9._+-]+)?([[:space:]]+(-[A-Za-z]|--[A-Za-z][A-Za-z0-9=._/@:-]*))*|(pwd|git[[:space:]]+(status|log|diff|show|branch|rev-parse)|git[[:space:]]+worktree[[:space:]]+list)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*)([[:space:]]+2>(&1|/dev/null))?([[:space:]]*\|[[:space:]]*(tail|head)([[:space:]]+-(n[[:space:]]+)?[0-9]+)?)?[[:space:]]*$'; then
|
|
262
295
|
wp_log "$WP_FAULT" ALLOW-CURE # record the self-heal we let through (re-enables the guards)
|
|
263
296
|
exit 0 # allow the cure so the assistant can break the deadlock
|
|
264
297
|
fi
|
|
@@ -270,9 +303,11 @@ if [ -n "$BROKEN_BIN" ]; then
|
|
|
270
303
|
STAGING_N="$(ls "$BIN_ROOT/node_modules" 2>/dev/null | grep -Ec '_[0-9a-f]+_[0-9a-f]+$' || true)"
|
|
271
304
|
STAGING_NOTE=""
|
|
272
305
|
if [ "${STAGING_N:-0}" -gt 0 ] 2>/dev/null; then
|
|
273
|
-
STAGING_NOTE="
|
|
306
|
+
STAGING_NOTE="${NL} → also found $STAGING_N orphaned pnpm staging dirs (name_pid_hash) under node_modules - the fingerprint of an install that was killed mid-write." # only when N > 0
|
|
274
307
|
fi
|
|
275
|
-
|
|
308
|
+
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
309
|
+
WP_HEAD="❌ webpieces ai-hooks blocked this call: the webpieces guards are DOWN."
|
|
310
|
+
REASON="$WP_HEAD${NL}${NL}[guard-bin-crashed] (layer=L0 fault=K row=3, 1 violation)${NL} ${BIN_NAME} ($CRASH_MSG)${NL} → it is installed but CRASHED, so your node_modules is corrupt or partially written; the guards cannot run and they must not be silently skipped. Every OTHER tool call is BLOCKED until they can.${STAGING_NOTE}${NL} → matrix row 3: fault=K present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=3 fault=K) and the same row webpieces.guard-matrix.md prints.${NL}${NL}${WP_STILL_ALLOWED}${NL}${NL} Fix Option 1: (preferred) the only cure. A bare 'pnpm install' will NOT fix this, because pnpm sees the correct version on disk and skips the broken package${NL} run EXACTLY: 'rm -rf node_modules && pnpm install'${NL}${NL}Run it EXACTLY as written - the allowlist matches the whole command, so appending anything (even && git status) makes it a different command and it is rejected; that is not the guard blocking its own cure. Only these may be added: a leading cd <dir> && (single-quote a path containing spaces), a trailing 2>&1, and | tail -N."
|
|
276
311
|
elif [ -n "$DRIFT_PKG" ]; then
|
|
277
312
|
# DECIDE THE DIRECTION, do not make the reader do it (2026-08-03). The detection is a plain !=, so it
|
|
278
313
|
# fires BOTH ways, and the message used to carry OPTION 1/2/3 covering every direction at once — 3343
|
|
@@ -307,13 +342,26 @@ elif [ -n "$DRIFT_PKG" ]; then
|
|
|
307
342
|
if (ip == "" && dp != "") print "newer"
|
|
308
343
|
}' 2>/dev/null)"
|
|
309
344
|
if [ "$DRIFT_DIR" = older ]; then
|
|
310
|
-
|
|
345
|
+
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
346
|
+
WP_HEAD="❌ webpieces ai-hooks blocked this call: webpieces version drift."
|
|
347
|
+
REASON="$WP_HEAD${NL}${NL}[version-drift] (layer=L0 fault=D row=3, 1 violation)${NL} package.json pins $DRIFT_PKG@$DRIFT_DECLARED but node_modules has $DRIFT_INSTALLED${NL} → node_modules is OLDER, so the pin is what you want. Every OTHER tool call is BLOCKED until the two agree.${NL} → matrix row 3: fault=D present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=3 fault=D) and the same row webpieces.guard-matrix.md prints.${NL}${NL}${WP_STILL_ALLOWED}${NL}${NL} Fix Option 1: (preferred) the only cure - it makes node_modules match the pin${NL} run EXACTLY: '$WP_INSTALL_CMD'${WP_BORROW_NOTE}${NL}${NL}Run it EXACTLY as written - the allowlist matches the whole command, so appending anything (even && git status) makes it a different command and it is rejected; that is not the guard blocking its own cure. Only these may be added: a leading cd <dir> && (single-quote a path containing spaces), a trailing 2>&1, and | tail -N."
|
|
311
348
|
else
|
|
312
|
-
# NEWER, or undecidable — the same
|
|
349
|
+
# NEWER, or undecidable — the same choices apply either way, so the only thing the ambiguous
|
|
313
350
|
# case changes is the claim about which side is stale.
|
|
314
351
|
DRIFT_NOTE="node_modules is NEWER, so the PIN is the stale side and a bare 'pnpm install' DOWNGRADES you to $DRIFT_DECLARED"
|
|
315
352
|
[ "$DRIFT_DIR" = newer ] || DRIFT_NOTE="these two versions could not be ordered automatically - compare them yourself: if node_modules is the NEWER side then the PIN is the stale side and a bare 'pnpm install' DOWNGRADES you to $DRIFT_DECLARED"
|
|
316
|
-
|
|
353
|
+
WP_BRANCH="$(git -C "$ROOT" branch --show-current 2>/dev/null)"
|
|
354
|
+
[ -n "$WP_LOG_DIR" ] || wp_resolve_log_dir
|
|
355
|
+
if [ "$WP_BRANCH" = main ]; then
|
|
356
|
+
WP_FIX=" Fix Option 1: (preferred) you are on main and want what origin pins - move forward${NL} run EXACTLY: 'git checkout main && git pull origin main', then 'pnpm install'${NL} Fix Option 2: you mean to stay on this code - the downgrade is the point${NL} run EXACTLY: 'pnpm install'"
|
|
357
|
+
else
|
|
358
|
+
WP_LOG_PATHS="$WP_LOG_DIR/L0-shim/"
|
|
359
|
+
[ "$WP_PRIMARY_LOG_DIR" = "$WP_LOG_DIR" ] || WP_LOG_PATHS="${WP_LOG_PATHS}${NL} and, for the primary clone: $WP_PRIMARY_LOG_DIR/L0-shim/"
|
|
360
|
+
WP_FIX=" Fix Option 1: (preferred) off main, align node_modules to YOUR branch pin - usually right${NL} run EXACTLY: 'pnpm install'${NL} Fix Option 2: you actually need the NEWER pin ON THIS BRANCH - there is no cure to run, and this guard will not invent one${NL} Do NOT reach for 'git pull origin main': pulling main into a feature branch destroys the fork point the build gate --base and the PR review diff are computed from, and the guards block it.${NL} You hit a weird case of needing a downgrade. Contact Dean - he needs the audit logs to understand why you are downgrading, so the guard logic can account for it.${NL} L0 audit logs: $WP_LOG_PATHS"
|
|
361
|
+
fi
|
|
362
|
+
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
363
|
+
WP_HEAD="❌ webpieces ai-hooks blocked this call: webpieces version drift."
|
|
364
|
+
REASON="$WP_HEAD${NL}${NL}[version-drift] (layer=L0 fault=D row=3, 1 violation)${NL} package.json pins $DRIFT_PKG@$DRIFT_DECLARED but node_modules has $DRIFT_INSTALLED${NL} → $DRIFT_NOTE. That may be exactly what you want. Every OTHER tool call is BLOCKED until the two agree.${NL} → matrix row 3: fault=D present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=3 fault=D) and the same row webpieces.guard-matrix.md prints.${NL}${NL}${WP_STILL_ALLOWED}${NL}${NL}${WP_FIX}${WP_BORROW_NOTE}${NL}${NL}Run it EXACTLY as written - the allowlist matches the whole command, so appending anything (even && git status) makes it a different command and it is rejected; that is not the guard blocking its own cure. Only these may be added: a leading cd <dir> && (single-quote a path containing spaces), a trailing 2>&1, and | tail -N."
|
|
317
365
|
fi
|
|
318
366
|
else
|
|
319
367
|
# A LINKED WORKTREE is the overwhelmingly common way to land here with a perfectly healthy repo:
|
|
@@ -323,7 +371,7 @@ else
|
|
|
323
371
|
# load-bearing: installing in the primary clone does nothing for this tree.
|
|
324
372
|
WORKTREE_NOTE=""
|
|
325
373
|
if [ -f "$ROOT/.git" ]; then
|
|
326
|
-
WORKTREE_NOTE="
|
|
374
|
+
WORKTREE_NOTE="${NL} → $ROOT is a LINKED WORKTREE - git does not copy node_modules into a new worktree, so this is expected on a fresh one. Run the Fix Option HERE, in this worktree, not in the primary clone."
|
|
327
375
|
fi
|
|
328
376
|
if [ -z "$WP_HOOK_PKG_DECLARED" ]; then
|
|
329
377
|
# FAULT U — the one shape where the X message is not merely unhelpful but actively WRONG. It asserted
|
|
@@ -334,15 +382,17 @@ else
|
|
|
334
382
|
# prescribe the add — which is allowlist entry ADD_HOOK_PKG, so it is reachable while this block is up.
|
|
335
383
|
WP_ADD_CMD="pnpm add -D @webpieces/ai-hook-rules"
|
|
336
384
|
[ -n "$WP_PIN" ] && WP_ADD_CMD="${WP_ADD_CMD}@$WP_PIN"
|
|
337
|
-
|
|
385
|
+
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
386
|
+
WP_HEAD="❌ webpieces ai-hooks blocked this call: the guard package is not declared anywhere."
|
|
387
|
+
REASON="$WP_HEAD${NL}${NL}[guard-pkg-undeclared] (layer=L0 fault=U row=3, 1 violation)${NL} @webpieces/ai-hook-rules is NOT declared in package.json anywhere, and is not installed (${BIN_NAME} not found)${NL} → .claude/settings.json still runs its hooks, so every OTHER tool call is BLOCKED. Do NOT run 'pnpm install': nothing asks for this package, so it is a NO-OP and repeating it converges to this same state.${NL} → matrix row 3: fault=U present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=3 fault=U) and the same row webpieces.guard-matrix.md prints.${NL}${NL}${WP_STILL_ALLOWED}${NL}${NL} Fix Option 1: (preferred) declare it directly, to unblock yourself right now${NL} run EXACTLY: '$WP_ADD_CMD'${NL} Fix Option 2: the durable fix - @webpieces/ai-hook-rules normally arrives with @webpieces/nx-webpieces-rules, the umbrella that bundles the whole toolchain, so upgrade that once you are unblocked.${NL} NOT an option: if you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json instead.${NL}${NL}Run it EXACTLY as written - the allowlist matches the whole command, so appending anything (even && git status) makes it a different command and it is rejected; that is not the guard blocking its own cure. Only these may be added: a leading cd <dir> && (single-quote a path containing spaces), a trailing 2>&1, and | tail -N."
|
|
338
388
|
else
|
|
339
|
-
|
|
389
|
+
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
390
|
+
WP_HEAD="❌ webpieces ai-hooks blocked this call: the webpieces guard bin is not installed."
|
|
391
|
+
REASON="$WP_HEAD${NL}${NL}[guard-bin-missing] (layer=L0 fault=X row=3, 1 violation)${NL} @webpieces/ai-hook-rules is declared in package.json but is not installed (${BIN_NAME} not found)${NL} → the guards cannot run, so every OTHER tool call is BLOCKED until they can.${WORKTREE_NOTE}${NL} → matrix row 3: fault=X present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=3 fault=X) and the same row webpieces.guard-matrix.md prints.${NL}${NL}${WP_STILL_ALLOWED}${NL}${NL} Fix Option 1: (preferred) the only cure - it materializes what package.json already asks for${NL} run EXACTLY: 'pnpm install'${NL} NOT an option: if you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json instead.${NL}${NL}Run it EXACTLY as written - the allowlist matches the whole command, so appending anything (even && git status) makes it a different command and it is rejected; that is not the guard blocking its own cure. Only these may be added: a leading cd <dir> && (single-quote a path containing spaces), a trailing 2>&1, and | tail -N."
|
|
340
392
|
fi
|
|
341
393
|
fi
|
|
342
394
|
if [ "$TOOL" = "Bash" ]; then
|
|
343
|
-
|
|
344
|
-
ESC="${BS}u001b" # the 6 chars: backslash u 0 0 1 b — Claude Code parses \u001b → ESC
|
|
345
|
-
printf '{"systemMessage":"%s🛑 %s%s","hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "${ESC}[31;1m" "$REASON" "${ESC}[0m" "$REASON"
|
|
395
|
+
printf '{"systemMessage":"%s🛑 %s%s%s","hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "${ESC}[31;1m" "$WP_HEAD" "${ESC}[0m" "${REASON#"$WP_HEAD"}" "$REASON"
|
|
346
396
|
else
|
|
347
397
|
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$REASON"
|
|
348
398
|
fi
|