@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.
@@ -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' "$WP_CATALOGS" | awk -v c="$WP_CAT" -v p="@webpieces/$WP_NAME" '$1==c && $2==p {print $3; exit}')"
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
- # THE CURE FOR A BORROWED node_modules RUNS IN THIS TREE, NOT WHEREVER THE BIN CAME FROM. A bare
115
- # 'pnpm install' typed while the shell sits in the primary clone installs into the primary, changes
116
- # nothing in the worktree being judged, and re-fires the identical fault the four-cure straddle
117
- # recorded above committedShimStale(). When the bin was inherited, prescribe the cd and say why.
118
- WP_INSTALL_CMD="pnpm install"
119
- WP_BORROW_NOTE=""
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
- WP_INSTALL_CMD="cd $ROOT && pnpm install"
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, B is the half you cannot do: it needs both trees on the same git hash, and CROSS-TREE GIT is refused to you here (a local 'cd' + install does run - it is git -C another tree that is blocked). So ESCALATE B: TELL THE MAIN AGENT in the MAIN git worktree ($BIN_ROOT) to run 'git pull && pnpm install' there so both trees are on the same @webpieces version, and to tell you when it is complete so you can continue. If you escalate instead of doing A, that forwarding IS the end of your turn: STOP WORKING NOW, make NO further tool calls and do NOT retry - RETRYING IS THE BUG, because every retry re-fires this identical deny and buries the ask. WAIT for that confirmation, then resume. 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."
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: '$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."
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
- [ -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'"
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
- 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"
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}${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."
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: