@webpieces/ai-hook-rules 0.4.675 → 0.4.677
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/bin/l0-allowlist.d.ts +22 -0
- package/src/bin/l0-allowlist.js +58 -1
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/shim-audit-log.js +1 -0
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-drift-fix.d.ts +1 -1
- package/src/bin/shim-drift-fix.js +33 -28
- package/src/bin/shim-drift-fix.js.map +1 -1
- package/src/bin/shim-version-drift.d.ts +1 -0
- package/src/bin/shim-version-drift.js +164 -0
- package/src/bin/shim-version-drift.js.map +1 -0
- package/src/bin/shim.js +19 -97
- package/src/bin/shim.js.map +1 -1
- package/src/core/l1-rows.js +2 -2
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/runner.js +7 -7
- package/src/core/runner.js.map +1 -1
- package/src/core/version-sync.d.ts +79 -0
- package/src/core/version-sync.js +236 -29
- package/src/core/version-sync.js.map +1 -1
- package/templates/ai-hook.sh +90 -18
package/templates/ai-hook.sh
CHANGED
|
@@ -19,7 +19,7 @@ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
|
|
19
19
|
BS='\' # one literal backslash, so no \u001b / \n escape sits in this source
|
|
20
20
|
ESC="${BS}u001b" # the 6 chars: backslash u 0 0 1 b — Claude Code parses \u001b → ESC
|
|
21
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."
|
|
22
|
+
WP_STILL_ALLOWED="Still allowed while this block is up:${NL} - any Read${NL} - any Write/Edit whose target is webpieces.config.json, pnpm-workspace.yaml or package.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."
|
|
23
23
|
# The BIN is resolved by walking UP from ROOT (as Node does), and BIN_ROOT records which tree supplied
|
|
24
24
|
# it — the version-drift guard below compares THIS tree's pin against THAT tree's installed version.
|
|
25
25
|
BIN_ROOT="$ROOT"
|
|
@@ -66,6 +66,53 @@ if [ -f "$ROOT/package.json" ]; then
|
|
|
66
66
|
# pass over pnpm-lock.yaml emits "<catalog> <@webpieces/pkg> <version>" lines for the sh lookup below;
|
|
67
67
|
# \047 is a single quote (so this awk program carries none and stays safely single-quotable in sh).
|
|
68
68
|
WP_CATALOGS=""
|
|
69
|
+
WP_WS_CATALOGS=""
|
|
70
|
+
# THE PIN LIVES IN pnpm-workspace.yaml, and the LOCK is only the fallback (2026-08-20).
|
|
71
|
+
#
|
|
72
|
+
# L0 used to learn the pin from pnpm-lock.yaml's `catalogs:` alone, while L1's WebpiecesVersions.readPin
|
|
73
|
+
# reads pnpm-workspace.yaml. Two notions of "the pin", and the gap is exactly where the cure lands: an
|
|
74
|
+
# agent told to raise this tree's pin edits pnpm-workspace.yaml, re-runs, and L0 still reports the OLD
|
|
75
|
+
# number — because only `pnpm install` rewrites the lock — so it concludes the edit did nothing and
|
|
76
|
+
# reaches for something worse. Reading the workspace manifest FIRST makes the edit visible immediately.
|
|
77
|
+
#
|
|
78
|
+
# It resolves the same two YAML shapes readPin does, and that is not optional: a repo pinning the whole
|
|
79
|
+
# @webpieces family in lockstep writes the version ONCE as `&wp 0.4.669` and aliases the rest as `*wp`,
|
|
80
|
+
# so an anchor-blind read silently nulls the leg on precisely the repos that pin most carefully. Both
|
|
81
|
+
# `catalog:` (the default catalog) and `catalogs:` (named ones) are walked. A value that is not a plain
|
|
82
|
+
# digit-version (a range) is NOT emitted, so a loose pinner falls through to the lock rather than being
|
|
83
|
+
# compared against an incomparable spec.
|
|
84
|
+
if grep -Eq '"@webpieces/[^"]*"[[:space:]]*:[[:space:]]*"catalog:' "$ROOT/package.json" 2>/dev/null && [ -f "$ROOT/pnpm-workspace.yaml" ]; then
|
|
85
|
+
WP_WS_CATALOGS="$(awk '
|
|
86
|
+
{ n=0; while (substr($0,n+1,1)==" ") n++; c=substr($0,n+1) }
|
|
87
|
+
c=="" || substr(c,1,1)=="#" { next }
|
|
88
|
+
{
|
|
89
|
+
ai=index(c,":")
|
|
90
|
+
if (ai>0) {
|
|
91
|
+
av=substr(c,ai+1); sub(/^[ \t]+/,"",av)
|
|
92
|
+
if (substr(av,1,1)=="&") {
|
|
93
|
+
an=substr(av,2); sub(/[ \t].*/,"",an)
|
|
94
|
+
sub(/^&[^ \t]+[ \t]*/,"",av)
|
|
95
|
+
sub(/[ \t]+#.*/,"",av); gsub(/["\047]/,"",av); sub(/[ \t].*/,"",av)
|
|
96
|
+
if (an!="" && av!="") anch[an]=av
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
n==0 { mode=(c ~ /^catalog: *$/)?1:((c ~ /^catalogs: *$/)?2:0); cat=(mode==1)?"default":""; next }
|
|
101
|
+
mode==0 { next }
|
|
102
|
+
mode==2 && c ~ /^[^:]+: *$/ { cat=c; sub(/: *$/,"",cat); gsub(/["\047 ]/,"",cat); next }
|
|
103
|
+
{
|
|
104
|
+
ki=index(c,":")
|
|
105
|
+
if (ki<=0) next
|
|
106
|
+
k=substr(c,1,ki-1); gsub(/["\047 ]/,"",k)
|
|
107
|
+
if (substr(k,1,11)!="@webpieces/") next
|
|
108
|
+
v=substr(c,ki+1); sub(/^[ \t]+/,"",v); sub(/^&[^ \t]+[ \t]*/,"",v)
|
|
109
|
+
sub(/[ \t]+#.*/,"",v); gsub(/["\047]/,"",v); sub(/[ \t].*/,"",v)
|
|
110
|
+
if (v=="") next
|
|
111
|
+
nn++; key[nn]=cat " " k; ali[nn]=(substr(v,1,1)=="*")?substr(v,2):""; val[nn]=v
|
|
112
|
+
}
|
|
113
|
+
END { for (i=1;i<=nn;i++) { vv=(ali[i]=="")?val[i]:anch[ali[i]]; if (vv ~ /^[0-9]/) print key[i] " " vv } }
|
|
114
|
+
' "$ROOT/pnpm-workspace.yaml" 2>/dev/null)"
|
|
115
|
+
fi
|
|
69
116
|
if grep -Eq '"@webpieces/[^"]*"[[:space:]]*:[[:space:]]*"catalog:' "$ROOT/package.json" 2>/dev/null && [ -f "$ROOT/pnpm-lock.yaml" ]; then
|
|
70
117
|
WP_CATALOGS="$(awk '
|
|
71
118
|
{ n=0; while (substr($0,n+1,1)==" ") n++; c=substr($0,n+1) }
|
|
@@ -90,7 +137,8 @@ if [ -f "$ROOT/package.json" ]; then
|
|
|
90
137
|
case "$WP_DECL" in
|
|
91
138
|
catalog:*)
|
|
92
139
|
WP_CAT="${WP_DECL#catalog:}"; [ -n "$WP_CAT" ] || WP_CAT="default"
|
|
93
|
-
WP_DECL="$(printf '%s\n' "$
|
|
140
|
+
WP_DECL="$(printf '%s\n' "$WP_WS_CATALOGS" | awk -v c="$WP_CAT" -v p="@webpieces/$WP_NAME" '$1==c && $2==p {print $3; exit}')"
|
|
141
|
+
[ -n "$WP_DECL" ] || WP_DECL="$(printf '%s\n' "$WP_CATALOGS" | awk -v c="$WP_CAT" -v p="@webpieces/$WP_NAME" '$1==c && $2==p {print $3; exit}')"
|
|
94
142
|
[ -n "$WP_DECL" ] || continue ;;
|
|
95
143
|
[0-9]*) : ;;
|
|
96
144
|
*) continue ;;
|
|
@@ -111,15 +159,29 @@ if [ -f "$ROOT/package.json" ]; then
|
|
|
111
159
|
$(sed -n 's/.*"@webpieces\/\([A-Za-z0-9._-]*\)"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1 \2/p' "$ROOT/package.json")
|
|
112
160
|
WPEOF
|
|
113
161
|
fi
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
162
|
+
# A BORROWED BIN IS NOT SINGLE-TREE DRIFT, SO L0 MUST NOT ANSWER IT (2026-08-20).
|
|
163
|
+
#
|
|
164
|
+
# RESOLVE_BIN_SH walks UP for the bin, so in a linked worktree with no node_modules of its own BIN_ROOT
|
|
165
|
+
# is the MAIN tree. The scan above then compares $ROOT's DECLARED pin against $BIN_ROOT's INSTALLED
|
|
166
|
+
# version — a CROSS-TREE comparison it was reporting as fault D, single-tree drift. Everything
|
|
167
|
+
# downstream of that mislabel was wrong:
|
|
168
|
+
# • the cure. 'pnpm install' cannot "align node_modules" in a tree that has none; it MANUFACTURES one,
|
|
169
|
+
# at this tree's stale pin. That is precisely the state L1 row 8 (trinary-version-skew) blocks, so
|
|
170
|
+
# the L0 cure created the next block. One measured agent was walked from a pin disagreement, through
|
|
171
|
+
# 'pnpm install' offered as "(preferred) ... usually right", to a DOWNGRADED engine and a total block.
|
|
172
|
+
# • the analysis. Row 8 already reads all FOUR versions (both trees' pins and both installs), already
|
|
173
|
+
# knows which direction to move, already detects a deliberate pin bump, and already carries the
|
|
174
|
+
# escalate-and-STOP protocol. It could never run: ai-hook.sh hard-exits on any sh-side fault, so D
|
|
175
|
+
# PREEMPTED the guard that had the right answer.
|
|
176
|
+
# So: when the bin came from another tree, raise NO fault here and let the binary run. D stays exactly as
|
|
177
|
+
# it was when ROOT == BIN_ROOT — that IS single-tree drift, and row 8 cannot see it (VersionSyncGuard
|
|
178
|
+
# only applies to a linked worktree compared against a DIFFERENT main tree). Coverage does not gap: if
|
|
179
|
+
# the two pins agree but the two installs differ, row 8's quartet still has two distinct members and it
|
|
180
|
+
# still fires. X / U / K are untouched — a MISSING or CRASHED bin is about the bin, not about a version.
|
|
120
181
|
if [ "$BIN_ROOT" != "$ROOT" ]; then
|
|
121
|
-
|
|
122
|
-
|
|
182
|
+
DRIFT_PKG=""
|
|
183
|
+
DRIFT_DECLARED=""
|
|
184
|
+
DRIFT_INSTALLED=""
|
|
123
185
|
fi
|
|
124
186
|
# Read the tool payload ONCE, up front. The shim no longer exec's the bin (see RUN_BIN_SH), so it must
|
|
125
187
|
# forward stdin to the bin itself — and it needs the payload again on the fail-closed path below.
|
|
@@ -290,6 +352,16 @@ case "$FILE" in
|
|
|
290
352
|
*/webpieces.config.json|webpieces.config.json)
|
|
291
353
|
wp_log "$WP_FAULT" ALLOW-CONFIG # the always-allowed recovery target — every guard is configured from it
|
|
292
354
|
exit 0 ;;
|
|
355
|
+
*/pnpm-workspace.yaml|pnpm-workspace.yaml|*/package.json|package.json)
|
|
356
|
+
# A manifest AT THE ROOT OF A GOVERNED TREE, which is the only place the version pin lives. The test
|
|
357
|
+
# is the sibling webpieces.config.json — TRACKED, so the main clone has one and every worktree has its
|
|
358
|
+
# own — and NOT $ROOT, which names whichever tree supplied this shim and would deny the other's.
|
|
359
|
+
# Basename alone would be far worse here than in the JS half: this arm is TERMINAL (exit 0, the bin
|
|
360
|
+
# never runs), so every packages/**/package.json would be editable with nothing judging it.
|
|
361
|
+
if [ -f "$(dirname -- "$FILE")/webpieces.config.json" ]; then
|
|
362
|
+
wp_log "$WP_FAULT" ALLOW-MANIFEST # raising the pin must be typable from inside the block
|
|
363
|
+
exit 0
|
|
364
|
+
fi ;;
|
|
293
365
|
esac
|
|
294
366
|
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
|
|
295
367
|
wp_log "$WP_FAULT" ALLOW-CURE # record the self-heal we let through (re-enables the guards)
|
|
@@ -344,24 +416,24 @@ elif [ -n "$DRIFT_PKG" ]; then
|
|
|
344
416
|
if [ "$DRIFT_DIR" = older ]; then
|
|
345
417
|
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
346
418
|
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: '
|
|
419
|
+
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: '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."
|
|
348
420
|
else
|
|
349
421
|
# NEWER, or undecidable — the same choices apply either way, so the only thing the ambiguous
|
|
350
422
|
# case changes is the claim about which side is stale.
|
|
351
423
|
DRIFT_NOTE="node_modules is NEWER, so the PIN is the stale side and a bare 'pnpm install' DOWNGRADES you to $DRIFT_DECLARED"
|
|
352
424
|
[ "$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"
|
|
353
425
|
WP_BRANCH="$(git -C "$ROOT" branch --show-current 2>/dev/null)"
|
|
354
|
-
|
|
355
|
-
if [ "$WP_BRANCH"
|
|
356
|
-
WP_FIX=" Fix Option 1: (preferred)
|
|
426
|
+
WP_PIN_EDIT="edit $ROOT/pnpm-workspace.yaml - the catalog line for $DRIFT_PKG, or that dependency in $ROOT/package.json if this repo pins directly - and set it to $DRIFT_INSTALLED, then run 'pnpm install'"
|
|
427
|
+
if [ -z "$WP_BRANCH" ]; then
|
|
428
|
+
WP_FIX=" Fix Option 1: (preferred) HEAD is DETACHED here, so a pin edit would belong to no branch - get onto main instead, whose pin is already at or ahead of what is installed, so the drift clears with no edit at all${NL} run EXACTLY: 'git checkout main && git pull origin main', then 'pnpm install'${NL} Fix Option 2: you mean to stay on this exact commit - the downgrade to $DRIFT_DECLARED is the point${NL} run EXACTLY: 'pnpm install'"
|
|
429
|
+
elif [ "$WP_BRANCH" = main ]; then
|
|
430
|
+
WP_FIX=" Fix Option 1: (preferred) go FORWARD - keep what is installed and raise the pin to match it${NL} ${WP_PIN_EDIT}${NL} That edit is ALLOWED while this block is up, and the install then only rewrites the lock - nothing is downgraded, because the pin now names what is already on disk.${NL} Fix Option 2: you are on main and want what origin pins instead${NL} run EXACTLY: 'git checkout main && git pull origin main', then 'pnpm install'"
|
|
357
431
|
else
|
|
358
|
-
|
|
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"
|
|
432
|
+
WP_FIX=" Fix Option 1: (preferred) go FORWARD - keep what is installed and raise THIS branch's pin to match it${NL} ${WP_PIN_EDIT}${NL} That edit is ALLOWED while this block is up, and the install then only rewrites the lock - nothing is downgraded, because the pin now names what is already on disk.${NL} Fix Option 2: you mean to align node_modules to YOUR branch pin - that is a DOWNGRADE to $DRIFT_DECLARED, so pick it only if you meant to${NL} run EXACTLY: 'pnpm install'${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."
|
|
361
433
|
fi
|
|
362
434
|
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
363
435
|
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}${
|
|
436
|
+
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}${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."
|
|
365
437
|
fi
|
|
366
438
|
else
|
|
367
439
|
# A LINKED WORKTREE is the overwhelmingly common way to land here with a perfectly healthy repo:
|