@webpieces/ai-hook-rules 0.4.676 → 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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DRIFT_INVERSE_FIX_SH = void 0;
|
|
4
|
-
const log_streams_1 = require("../core/log-streams");
|
|
5
4
|
const l0_allowlist_1 = require("./l0-allowlist");
|
|
6
5
|
// ---------------------------------------------------------------------------
|
|
7
6
|
// Shell fragment: the Fix Options for the INVERSE half of L0 fault D — node_modules is NEWER than the
|
|
@@ -12,20 +11,27 @@ const l0_allowlist_1 = require("./l0-allowlist");
|
|
|
12
11
|
// acyclic, and it stays as dependency-free as the rest of the shim — this text has to render on a tree
|
|
13
12
|
// too broken to load the rule engine.
|
|
14
13
|
//
|
|
15
|
-
//
|
|
14
|
+
// EVERY ARM'S FIRST OPTION NOW GOES FORWARD (2026-08-20). This message used to lead, on a feature
|
|
15
|
+
// branch, with `pnpm install` as "(preferred) ... usually right" — an option that is by construction a
|
|
16
|
+
// DOWNGRADE, since this whole branch of the message only renders when node_modules is the NEWER side.
|
|
17
|
+
// One measured agent took that advice, downgraded its engine two releases, and the older engine then
|
|
18
|
+
// rejected a config key it had never heard of and blocked every Bash call. The forward move — keep what
|
|
19
|
+
// is installed, raise the pin to match it — was never offered at all, on any arm.
|
|
16
20
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// the wrong scope and a PR diff describing work nobody did. A menu whose first option is blocked on the
|
|
23
|
-
// branch you are standing on is worse than no menu, so the branch is asked instead.
|
|
21
|
+
// THE FORWARD MOVE IS A FILE EDIT, and that is exactly why it could not be offered before: editing
|
|
22
|
+
// pnpm-workspace.yaml was not on the L0 allowlist, so the guard would have prescribed a call it then
|
|
23
|
+
// denied. It IS on the list now (see l0-allowlist's manifest entry and isAllowed), so Option 1 is a
|
|
24
|
+
// cure the reader can actually perform from inside this block. The `pnpm install` that follows it
|
|
25
|
+
// rewrites the lock and downgrades nothing, because the pin now names what is already on disk.
|
|
24
26
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
27
|
+
// THIS FRAGMENT IS SINGLE-TREE ONLY. Fault D no longer fires when the guard bin was inherited from
|
|
28
|
+
// another tree (see VERSION_DRIFT_GUARD_SH) — that case belongs to L1 row 8, which can see all four
|
|
29
|
+
// versions. So every option here acts on ONE tree, and no cross-tree escalation is needed or offered.
|
|
30
|
+
//
|
|
31
|
+
// ON MAIN the alternative forward move is `git checkout main && git pull origin main` — it ends ON main,
|
|
32
|
+
// merges nothing into anything, is a no-op checkout when you are already there, and it is the ONE pull
|
|
33
|
+
// spelling still on the L0 allowlist (see CHECKOUT_MAIN_PULL_BODY_ERE, which also records why it is a
|
|
34
|
+
// narrow literal).
|
|
29
35
|
//
|
|
30
36
|
// RAW GIT ON PURPOSE, and this is now the DIFFERENCE from the workflow guards rather than a match with
|
|
31
37
|
// them. stale-main-bash-guard, merged-branch-message, TreeRecovery and the L2 rows all prescribe
|
|
@@ -41,27 +47,26 @@ const l0_allowlist_1 = require("./l0-allowlist");
|
|
|
41
47
|
// committed-shim self-guard, so a comment-only change would fire that guard on every consumer's tree for
|
|
42
48
|
// no behavioural reason — the exact churn the version stamp was removed to stop.
|
|
43
49
|
//
|
|
44
|
-
// ON A FEATURE BRANCH there is no honest forward move to name, and this deliberately does not invent
|
|
45
|
-
// one. `pnpm install` aligns node_modules to YOUR branch pin and is the usual right answer, but it is a
|
|
46
|
-
// DOWNGRADE — and every drift event logged in this repo to date has been the OTHER direction, so a
|
|
47
|
-
// feature branch that genuinely needs the NEWER pin is a shape the guard logic has never seen. Option 2
|
|
48
|
-
// therefore says that plainly and prints the L0 audit-log paths, because those logs are the evidence a
|
|
49
|
-
// real cure would have to be designed from, and an escalation nobody can act on is not an option at all.
|
|
50
|
-
//
|
|
51
50
|
// `git branch --show-current`, NOT `git rev-parse --abbrev-ref HEAD`: it answers on an UNBORN branch
|
|
52
|
-
// (`rev-parse` fatals there), and it prints an EMPTY string on a detached HEAD
|
|
53
|
-
//
|
|
51
|
+
// (`rev-parse` fatals there), and it prints an EMPTY string on a detached HEAD. That empty answer used
|
|
52
|
+
// to fall into the feature-branch half, which was wrong twice over: there is no branch for Option 1's
|
|
53
|
+
// pin edit to belong to, and the branch-specific warning about fork points is meaningless. It is its own
|
|
54
|
+
// arm now — the one arm whose preferred cure is the checkout, precisely because it has no branch to edit.
|
|
54
55
|
//
|
|
55
56
|
// CONSTRAINT, same as every other deny fragment: no `"` and no backslash may reach the rendered text —
|
|
56
57
|
// it is interpolated into a `REASON="…"` shell assignment and then printf'd into a JSON string.
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
59
|
+
const PIN_EDIT_SH = `edit $ROOT/${l0_allowlist_1.WORKSPACE_MANIFEST} - the catalog line for $DRIFT_PKG, or that dependency in `
|
|
60
|
+
+ `$ROOT/${l0_allowlist_1.PACKAGE_MANIFEST} if this repo pins directly - and set it to $DRIFT_INSTALLED, then run 'pnpm install'`;
|
|
61
|
+
const FORWARD_NOTE_SH = `That edit is ALLOWED while this block is up, and the install then only rewrites the lock - `
|
|
62
|
+
+ `nothing is downgraded, because the pin now names what is already on disk.`;
|
|
58
63
|
exports.DRIFT_INVERSE_FIX_SH = `WP_BRANCH="$(git -C "$ROOT" branch --show-current 2>/dev/null)"
|
|
59
|
-
|
|
60
|
-
if [ "$WP_BRANCH"
|
|
61
|
-
WP_FIX=" Fix Option 1: (preferred)
|
|
64
|
+
WP_PIN_EDIT="${PIN_EDIT_SH}"
|
|
65
|
+
if [ -z "$WP_BRANCH" ]; then
|
|
66
|
+
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: '${l0_allowlist_1.CHECKOUT_MAIN_PULL_CMD}', 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'"
|
|
67
|
+
elif [ "$WP_BRANCH" = main ]; then
|
|
68
|
+
WP_FIX=" Fix Option 1: (preferred) go FORWARD - keep what is installed and raise the pin to match it\${NL} \${WP_PIN_EDIT}\${NL} ${FORWARD_NOTE_SH}\${NL} Fix Option 2: you are on main and want what origin pins instead\${NL} run EXACTLY: '${l0_allowlist_1.CHECKOUT_MAIN_PULL_CMD}', then 'pnpm install'"
|
|
62
69
|
else
|
|
63
|
-
|
|
64
|
-
[ "$WP_PRIMARY_LOG_DIR" = "$WP_LOG_DIR" ] || WP_LOG_PATHS="\${WP_LOG_PATHS}\${NL} and, for the primary clone: $WP_PRIMARY_LOG_DIR/${log_streams_1.L0_SHIM_STREAM}/"
|
|
65
|
-
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"
|
|
70
|
+
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} ${FORWARD_NOTE_SH}\${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."
|
|
66
71
|
fi`;
|
|
67
72
|
//# sourceMappingURL=shim-drift-fix.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shim-drift-fix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-drift-fix.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"shim-drift-fix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-drift-fix.ts"],"names":[],"mappings":";;;AAAA,iDAA8F;AAE9F,8EAA8E;AAC9E,sGAAsG;AACtG,kFAAkF;AAClF,EAAE;AACF,sGAAsG;AACtG,uGAAuG;AACvG,uGAAuG;AACvG,sCAAsC;AACtC,EAAE;AACF,kGAAkG;AAClG,uGAAuG;AACvG,sGAAsG;AACtG,qGAAqG;AACrG,wGAAwG;AACxG,kFAAkF;AAClF,EAAE;AACF,mGAAmG;AACnG,qGAAqG;AACrG,oGAAoG;AACpG,kGAAkG;AAClG,+FAA+F;AAC/F,EAAE;AACF,mGAAmG;AACnG,oGAAoG;AACpG,sGAAsG;AACtG,EAAE;AACF,yGAAyG;AACzG,uGAAuG;AACvG,sGAAsG;AACtG,mBAAmB;AACnB,EAAE;AACF,uGAAuG;AACvG,iGAAiG;AACjG,oGAAoG;AACpG,yGAAyG;AACzG,sGAAsG;AACtG,oGAAoG;AACpG,wGAAwG;AACxG,wGAAwG;AACxG,EAAE;AACF,uGAAuG;AACvG,mGAAmG;AACnG,yGAAyG;AACzG,iFAAiF;AACjF,EAAE;AACF,qGAAqG;AACrG,uGAAuG;AACvG,sGAAsG;AACtG,yGAAyG;AACzG,0GAA0G;AAC1G,EAAE;AACF,uGAAuG;AACvG,gGAAgG;AAChG,8EAA8E;AAC9E,MAAM,WAAW,GACb,cAAc,iCAAkB,4DAA4D;MAC1F,SAAS,+BAAgB,uFAAuF,CAAC;AAEvH,MAAM,eAAe,GACjB,6FAA6F;MAC3F,2EAA2E,CAAC;AAErE,QAAA,oBAAoB,GAAG;mBACjB,WAAW;;0PAE4N,qCAAsB;;gJAEhI,eAAe,kGAAkG,qCAAsB;;0JAE7H,eAAe;OAClK,CAAC","sourcesContent":["import { CHECKOUT_MAIN_PULL_CMD, WORKSPACE_MANIFEST, PACKAGE_MANIFEST } from './l0-allowlist';\n\n// ---------------------------------------------------------------------------\n// Shell fragment: the Fix Options for the INVERSE half of L0 fault D — node_modules is NEWER than the\n// pin (or the two could not be ordered), so a bare `pnpm install` is a DOWNGRADE.\n//\n// Its own module for the same reason shim-audit-log.ts is: shim.ts renders the whole shim body and is\n// at its file-size cap. It imports FROM ./l0-allowlist and is never imported BY it, so the graph stays\n// acyclic, and it stays as dependency-free as the rest of the shim — this text has to render on a tree\n// too broken to load the rule engine.\n//\n// EVERY ARM'S FIRST OPTION NOW GOES FORWARD (2026-08-20). This message used to lead, on a feature\n// branch, with `pnpm install` as \"(preferred) ... usually right\" — an option that is by construction a\n// DOWNGRADE, since this whole branch of the message only renders when node_modules is the NEWER side.\n// One measured agent took that advice, downgraded its engine two releases, and the older engine then\n// rejected a config key it had never heard of and blocked every Bash call. The forward move — keep what\n// is installed, raise the pin to match it — was never offered at all, on any arm.\n//\n// THE FORWARD MOVE IS A FILE EDIT, and that is exactly why it could not be offered before: editing\n// pnpm-workspace.yaml was not on the L0 allowlist, so the guard would have prescribed a call it then\n// denied. It IS on the list now (see l0-allowlist's manifest entry and isAllowed), so Option 1 is a\n// cure the reader can actually perform from inside this block. The `pnpm install` that follows it\n// rewrites the lock and downgrades nothing, because the pin now names what is already on disk.\n//\n// THIS FRAGMENT IS SINGLE-TREE ONLY. Fault D no longer fires when the guard bin was inherited from\n// another tree (see VERSION_DRIFT_GUARD_SH) — that case belongs to L1 row 8, which can see all four\n// versions. So every option here acts on ONE tree, and no cross-tree escalation is needed or offered.\n//\n// ON MAIN the alternative forward move is `git checkout main && git pull origin main` — it ends ON main,\n// merges nothing into anything, is a no-op checkout when you are already there, and it is the ONE pull\n// spelling still on the L0 allowlist (see CHECKOUT_MAIN_PULL_BODY_ERE, which also records why it is a\n// narrow literal).\n//\n// RAW GIT ON PURPOSE, and this is now the DIFFERENCE from the workflow guards rather than a match with\n// them. stale-main-bash-guard, merged-branch-message, TreeRecovery and the L2 rows all prescribe\n// `pnpm wp-checkout-clean-main` — the same pairing with `wp-cleanup` and the orphan-directory sweep\n// welded on, so the sweep actually runs. THIS message must NOT follow them: the fault it is reporting IS\n// that `node_modules` disagrees with the pin, which makes `node_modules` the untrustworthy thing, and\n// every `pnpm wp-*` bin resolves through it. An L0 cure may never be a command that has to load the\n// package it is repairing. Two layers, two spellings, and that is not the two-spellings shim — they are\n// cures for two different states, one of which has a working package manager and one of which does not.\n//\n// The rationale is recorded HERE, in the source, and deliberately NOT as a comment inside the rendered\n// shell: the committed `.claude/webpieces/ai-hook.sh` is byte-compared against renderShim() by the\n// committed-shim self-guard, so a comment-only change would fire that guard on every consumer's tree for\n// no behavioural reason — the exact churn the version stamp was removed to stop.\n//\n// `git branch --show-current`, NOT `git rev-parse --abbrev-ref HEAD`: it answers on an UNBORN branch\n// (`rev-parse` fatals there), and it prints an EMPTY string on a detached HEAD. That empty answer used\n// to fall into the feature-branch half, which was wrong twice over: there is no branch for Option 1's\n// pin edit to belong to, and the branch-specific warning about fork points is meaningless. It is its own\n// arm now — the one arm whose preferred cure is the checkout, precisely because it has no branch to edit.\n//\n// CONSTRAINT, same as every other deny fragment: no `\"` and no backslash may reach the rendered text —\n// it is interpolated into a `REASON=\"…\"` shell assignment and then printf'd into a JSON string.\n// ---------------------------------------------------------------------------\nconst PIN_EDIT_SH =\n `edit $ROOT/${WORKSPACE_MANIFEST} - the catalog line for $DRIFT_PKG, or that dependency in `\n + `$ROOT/${PACKAGE_MANIFEST} if this repo pins directly - and set it to $DRIFT_INSTALLED, then run 'pnpm install'`;\n\nconst FORWARD_NOTE_SH =\n `That edit is ALLOWED while this block is up, and the install then only rewrites the lock - `\n + `nothing is downgraded, because the pin now names what is already on disk.`;\n\nexport const DRIFT_INVERSE_FIX_SH = `WP_BRANCH=\"$(git -C \"$ROOT\" branch --show-current 2>/dev/null)\"\n WP_PIN_EDIT=\"${PIN_EDIT_SH}\"\n if [ -z \"$WP_BRANCH\" ]; then\n 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: '${CHECKOUT_MAIN_PULL_CMD}', 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'\"\n elif [ \"$WP_BRANCH\" = main ]; then\n WP_FIX=\" Fix Option 1: (preferred) go FORWARD - keep what is installed and raise the pin to match it\\${NL} \\${WP_PIN_EDIT}\\${NL} ${FORWARD_NOTE_SH}\\${NL} Fix Option 2: you are on main and want what origin pins instead\\${NL} run EXACTLY: '${CHECKOUT_MAIN_PULL_CMD}', then 'pnpm install'\"\n else\n 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} ${FORWARD_NOTE_SH}\\${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.\"\n fi`;\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION_DRIFT_GUARD_SH = "# --- webpieces version-drift guard (pure sh \u2014 runs even when the installed guard bin is stale) -----\n# The committed shim is version-agnostic, so it keeps working right after a git pull, BEFORE the\n# matching pnpm install. That is exactly when node_modules can be STALE: an OLDER @webpieces than\n# package.json now pins, whose outdated validator rejects the NEWER webpieces.config.json with baffling\n# \"unknown rule\" errors. Detect that drift HERE (before exec'ing the possibly-stale bin): compare every\n# EXACT-pinned @webpieces/* version in the root package.json against the version actually installed in\n# node_modules; the first mismatch wins. Range specs (^ ~ workspace:*) are skipped, so they never\n# false-positive; best-effort \u2014 a version we cannot read is skipped. On drift we fall through to the\n# SAME fail-closed path as a missing bin (allow only pnpm install, deny the rest).\n#\n# pnpm CATALOGS: a dep pinned via \"catalog:\" / \"catalog:<name>\" carries NO digit-version in package.json,\n# so the old scraper matched nothing and the guard was BLIND to it \u2014 DRIFT_PKG stayed empty and the\n# stale bin ran (the 2026-07 \"0.3.369 vs 0.4.405\" incident). Resolve those specs through the top-level\n# `catalogs:` block of pnpm-lock.yaml (catalog -> pkg -> resolved version) before comparing.\n#\n# THE SAME PASS ANSWERS FAULT U (2026-08-05). Scraping root package.json is also the only way to learn\n# whether @webpieces/ai-hook-rules is DECLARED at all, and that is the difference between \"not installed\n# yet\" (X, cured by pnpm install) and \"nothing asks for it\" (U, where pnpm install is a guaranteed\n# no-op). WP_PIN carries the first EXACT @webpieces pin found, so U's deny can prescribe the version the\n# rest of the repo is already on rather than an unpinned add. Both are set BEFORE the range/catalog\n# `continue`s, so a repo pinning the package by range still counts as having declared it.\nDRIFT_PKG=\"\"\nDRIFT_DECLARED=\"\"\nDRIFT_INSTALLED=\"\"\nWP_HOOK_PKG_DECLARED=\"\"\nWP_PIN=\"\"\nif [ -f \"$ROOT/package.json\" ]; then\n # Only when a @webpieces dep actually uses a \"catalog:\" spec do we scan the (possibly huge) lockfile \u2014\n # a cheap grep keeps the common, catalog-free repo from paying that cost on every tool call. One awk\n # pass over pnpm-lock.yaml emits \"<catalog> <@webpieces/pkg> <version>\" lines for the sh lookup below;\n # \\047 is a single quote (so this awk program carries none and stays safely single-quotable in sh).\n WP_CATALOGS=\"\"\n WP_WS_CATALOGS=\"\"\n # THE PIN LIVES IN pnpm-workspace.yaml, and the LOCK is only the fallback (2026-08-20).\n #\n # L0 used to learn the pin from pnpm-lock.yaml's `catalogs:` alone, while L1's WebpiecesVersions.readPin\n # reads pnpm-workspace.yaml. Two notions of \"the pin\", and the gap is exactly where the cure lands: an\n # agent told to raise this tree's pin edits pnpm-workspace.yaml, re-runs, and L0 still reports the OLD\n # number \u2014 because only `pnpm install` rewrites the lock \u2014 so it concludes the edit did nothing and\n # reaches for something worse. Reading the workspace manifest FIRST makes the edit visible immediately.\n #\n # It resolves the same two YAML shapes readPin does, and that is not optional: a repo pinning the whole\n # @webpieces family in lockstep writes the version ONCE as `&wp 0.4.669` and aliases the rest as `*wp`,\n # so an anchor-blind read silently nulls the leg on precisely the repos that pin most carefully. Both\n # `catalog:` (the default catalog) and `catalogs:` (named ones) are walked. A value that is not a plain\n # digit-version (a range) is NOT emitted, so a loose pinner falls through to the lock rather than being\n # compared against an incomparable spec.\n if grep -Eq '\"@webpieces/[^\"]*\"[[:space:]]*:[[:space:]]*\"catalog:' \"$ROOT/package.json\" 2>/dev/null && [ -f \"$ROOT/pnpm-workspace.yaml\" ]; then\n WP_WS_CATALOGS=\"$(awk '\n { n=0; while (substr($0,n+1,1)==\" \") n++; c=substr($0,n+1) }\n c==\"\" || substr(c,1,1)==\"#\" { next }\n {\n ai=index(c,\":\")\n if (ai>0) {\n av=substr(c,ai+1); sub(/^[ \\t]+/,\"\",av)\n if (substr(av,1,1)==\"&\") {\n an=substr(av,2); sub(/[ \\t].*/,\"\",an)\n sub(/^&[^ \\t]+[ \\t]*/,\"\",av)\n sub(/[ \\t]+#.*/,\"\",av); gsub(/[\"\\047]/,\"\",av); sub(/[ \\t].*/,\"\",av)\n if (an!=\"\" && av!=\"\") anch[an]=av\n }\n }\n }\n n==0 { mode=(c ~ /^catalog: *$/)?1:((c ~ /^catalogs: *$/)?2:0); cat=(mode==1)?\"default\":\"\"; next }\n mode==0 { next }\n mode==2 && c ~ /^[^:]+: *$/ { cat=c; sub(/: *$/,\"\",cat); gsub(/[\"\\047 ]/,\"\",cat); next }\n {\n ki=index(c,\":\")\n if (ki<=0) next\n k=substr(c,1,ki-1); gsub(/[\"\\047 ]/,\"\",k)\n if (substr(k,1,11)!=\"@webpieces/\") next\n v=substr(c,ki+1); sub(/^[ \\t]+/,\"\",v); sub(/^&[^ \\t]+[ \\t]*/,\"\",v)\n sub(/[ \\t]+#.*/,\"\",v); gsub(/[\"\\047]/,\"\",v); sub(/[ \\t].*/,\"\",v)\n if (v==\"\") next\n nn++; key[nn]=cat \" \" k; ali[nn]=(substr(v,1,1)==\"*\")?substr(v,2):\"\"; val[nn]=v\n }\n END { for (i=1;i<=nn;i++) { vv=(ali[i]==\"\")?val[i]:anch[ali[i]]; if (vv ~ /^[0-9]/) print key[i] \" \" vv } }\n ' \"$ROOT/pnpm-workspace.yaml\" 2>/dev/null)\"\n fi\n if grep -Eq '\"@webpieces/[^\"]*\"[[:space:]]*:[[:space:]]*\"catalog:' \"$ROOT/package.json\" 2>/dev/null && [ -f \"$ROOT/pnpm-lock.yaml\" ]; then\n WP_CATALOGS=\"$(awk '\n { n=0; while (substr($0,n+1,1)==\" \") n++; c=substr($0,n+1) }\n c==\"\" { next }\n n==0 { incat=(c ~ /^catalogs: *$/)?1:0; cat=\"\"; pkg=\"\"; next }\n incat==0 { next }\n n==2 { cat=c; sub(/:.*/,\"\",cat); pkg=\"\"; next }\n n==4 { pkg=c; sub(/: *$/,\"\",pkg); gsub(/[\"\\047]/,\"\",pkg); next }\n n==6 && substr(pkg,1,11)==\"@webpieces/\" && c ~ /^version:/ {\n v=c; sub(/^version: */,\"\",v); gsub(/[\"\\047 ]/,\"\",v);\n if (cat!=\"\" && v!=\"\") print cat \" \" pkg \" \" v\n }\n ' \"$ROOT/pnpm-lock.yaml\" 2>/dev/null)\"\n fi\n while IFS=' ' read -r WP_NAME WP_DECL; do\n [ -n \"$WP_NAME\" ] || continue\n # Fault U's input: the package is DECLARED (in any spec shape, in any dependency block of the root\n # manifest). Recorded before every `continue` below, so a range or catalog spec still counts.\n [ \"$WP_NAME\" = \"ai-hook-rules\" ] && WP_HOOK_PKG_DECLARED=1\n # Resolve the declared spec to an EXACT version, or skip it: ranges (^ ~ workspace:*) never drift,\n # and a catalog spec we cannot resolve is best-effort skipped rather than guessed.\n case \"$WP_DECL\" in\n catalog:*)\n WP_CAT=\"${WP_DECL#catalog:}\"; [ -n \"$WP_CAT\" ] || WP_CAT=\"default\"\n 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}')\"\n [ -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}')\"\n [ -n \"$WP_DECL\" ] || continue ;;\n [0-9]*) : ;;\n *) continue ;;\n esac\n # The release the rest of this repo is on \u2014 what fault U's cure should pin to.\n [ -n \"$WP_PIN\" ] || WP_PIN=\"$WP_DECL\"\n WP_MANIFEST=\"$BIN_ROOT/node_modules/@webpieces/$WP_NAME/package.json\"\n [ -f \"$WP_MANIFEST\" ] || continue\n WP_INST=\"$(sed -n 's/.*\"version\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' \"$WP_MANIFEST\" | head -n1)\"\n [ -n \"$WP_INST\" ] || continue\n if [ \"$WP_DECL\" != \"$WP_INST\" ]; then\n DRIFT_PKG=\"@webpieces/$WP_NAME\"\n DRIFT_DECLARED=\"$WP_DECL\"\n DRIFT_INSTALLED=\"$WP_INST\"\n break\n fi\n done <<WPEOF\n$(sed -n 's/.*\"@webpieces\\/\\([A-Za-z0-9._-]*\\)\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1 \\2/p' \"$ROOT/package.json\")\nWPEOF\nfi\n# A BORROWED BIN IS NOT SINGLE-TREE DRIFT, SO L0 MUST NOT ANSWER IT (2026-08-20).\n#\n# RESOLVE_BIN_SH walks UP for the bin, so in a linked worktree with no node_modules of its own BIN_ROOT\n# is the MAIN tree. The scan above then compares $ROOT's DECLARED pin against $BIN_ROOT's INSTALLED\n# version \u2014 a CROSS-TREE comparison it was reporting as fault D, single-tree drift. Everything\n# downstream of that mislabel was wrong:\n# \u2022 the cure. 'pnpm install' cannot \"align node_modules\" in a tree that has none; it MANUFACTURES one,\n# at this tree's stale pin. That is precisely the state L1 row 8 (trinary-version-skew) blocks, so\n# the L0 cure created the next block. One measured agent was walked from a pin disagreement, through\n# 'pnpm install' offered as \"(preferred) ... usually right\", to a DOWNGRADED engine and a total block.\n# \u2022 the analysis. Row 8 already reads all FOUR versions (both trees' pins and both installs), already\n# knows which direction to move, already detects a deliberate pin bump, and already carries the\n# escalate-and-STOP protocol. It could never run: ai-hook.sh hard-exits on any sh-side fault, so D\n# PREEMPTED the guard that had the right answer.\n# So: when the bin came from another tree, raise NO fault here and let the binary run. D stays exactly as\n# it was when ROOT == BIN_ROOT \u2014 that IS single-tree drift, and row 8 cannot see it (VersionSyncGuard\n# only applies to a linked worktree compared against a DIFFERENT main tree). Coverage does not gap: if\n# the two pins agree but the two installs differ, row 8's quartet still has two distinct members and it\n# still fires. X / U / K are untouched \u2014 a MISSING or CRASHED bin is about the bin, not about a version.\nif [ \"$BIN_ROOT\" != \"$ROOT\" ]; then\n DRIFT_PKG=\"\"\n DRIFT_DECLARED=\"\"\n DRIFT_INSTALLED=\"\"\nfi";
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VERSION_DRIFT_GUARD_SH = void 0;
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Shell fragment: L0's VERSION-DRIFT guard (fault D), in pure sh.
|
|
6
|
+
//
|
|
7
|
+
// Its own module for the same reason shim-audit-log.ts and shim-drift-fix.ts are: shim.ts renders the
|
|
8
|
+
// whole shim body and is at its file-size cap. It is spliced back into renderShim() verbatim,
|
|
9
|
+
// byte-for-byte, and it imports nothing — this has to run on a tree too broken to load the rule engine.
|
|
10
|
+
//
|
|
11
|
+
// The rationale for every branch is IN the fragment, as shell comments, and stays there: unlike
|
|
12
|
+
// shim-drift-fix.ts's Fix Options, this text is the guard's own reasoning about a tree it cannot
|
|
13
|
+
// otherwise explain, and a reader debugging a live block is reading the rendered .sh, not this file.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
exports.VERSION_DRIFT_GUARD_SH = `# --- webpieces version-drift guard (pure sh — runs even when the installed guard bin is stale) -----
|
|
16
|
+
# The committed shim is version-agnostic, so it keeps working right after a git pull, BEFORE the
|
|
17
|
+
# matching pnpm install. That is exactly when node_modules can be STALE: an OLDER @webpieces than
|
|
18
|
+
# package.json now pins, whose outdated validator rejects the NEWER webpieces.config.json with baffling
|
|
19
|
+
# "unknown rule" errors. Detect that drift HERE (before exec'ing the possibly-stale bin): compare every
|
|
20
|
+
# EXACT-pinned @webpieces/* version in the root package.json against the version actually installed in
|
|
21
|
+
# node_modules; the first mismatch wins. Range specs (^ ~ workspace:*) are skipped, so they never
|
|
22
|
+
# false-positive; best-effort — a version we cannot read is skipped. On drift we fall through to the
|
|
23
|
+
# SAME fail-closed path as a missing bin (allow only pnpm install, deny the rest).
|
|
24
|
+
#
|
|
25
|
+
# pnpm CATALOGS: a dep pinned via "catalog:" / "catalog:<name>" carries NO digit-version in package.json,
|
|
26
|
+
# so the old scraper matched nothing and the guard was BLIND to it — DRIFT_PKG stayed empty and the
|
|
27
|
+
# stale bin ran (the 2026-07 "0.3.369 vs 0.4.405" incident). Resolve those specs through the top-level
|
|
28
|
+
# \`catalogs:\` block of pnpm-lock.yaml (catalog -> pkg -> resolved version) before comparing.
|
|
29
|
+
#
|
|
30
|
+
# THE SAME PASS ANSWERS FAULT U (2026-08-05). Scraping root package.json is also the only way to learn
|
|
31
|
+
# whether @webpieces/ai-hook-rules is DECLARED at all, and that is the difference between "not installed
|
|
32
|
+
# yet" (X, cured by pnpm install) and "nothing asks for it" (U, where pnpm install is a guaranteed
|
|
33
|
+
# no-op). WP_PIN carries the first EXACT @webpieces pin found, so U's deny can prescribe the version the
|
|
34
|
+
# rest of the repo is already on rather than an unpinned add. Both are set BEFORE the range/catalog
|
|
35
|
+
# \`continue\`s, so a repo pinning the package by range still counts as having declared it.
|
|
36
|
+
DRIFT_PKG=""
|
|
37
|
+
DRIFT_DECLARED=""
|
|
38
|
+
DRIFT_INSTALLED=""
|
|
39
|
+
WP_HOOK_PKG_DECLARED=""
|
|
40
|
+
WP_PIN=""
|
|
41
|
+
if [ -f "$ROOT/package.json" ]; then
|
|
42
|
+
# Only when a @webpieces dep actually uses a "catalog:" spec do we scan the (possibly huge) lockfile —
|
|
43
|
+
# a cheap grep keeps the common, catalog-free repo from paying that cost on every tool call. One awk
|
|
44
|
+
# pass over pnpm-lock.yaml emits "<catalog> <@webpieces/pkg> <version>" lines for the sh lookup below;
|
|
45
|
+
# \\047 is a single quote (so this awk program carries none and stays safely single-quotable in sh).
|
|
46
|
+
WP_CATALOGS=""
|
|
47
|
+
WP_WS_CATALOGS=""
|
|
48
|
+
# THE PIN LIVES IN pnpm-workspace.yaml, and the LOCK is only the fallback (2026-08-20).
|
|
49
|
+
#
|
|
50
|
+
# L0 used to learn the pin from pnpm-lock.yaml's \`catalogs:\` alone, while L1's WebpiecesVersions.readPin
|
|
51
|
+
# reads pnpm-workspace.yaml. Two notions of "the pin", and the gap is exactly where the cure lands: an
|
|
52
|
+
# agent told to raise this tree's pin edits pnpm-workspace.yaml, re-runs, and L0 still reports the OLD
|
|
53
|
+
# number — because only \`pnpm install\` rewrites the lock — so it concludes the edit did nothing and
|
|
54
|
+
# reaches for something worse. Reading the workspace manifest FIRST makes the edit visible immediately.
|
|
55
|
+
#
|
|
56
|
+
# It resolves the same two YAML shapes readPin does, and that is not optional: a repo pinning the whole
|
|
57
|
+
# @webpieces family in lockstep writes the version ONCE as \`&wp 0.4.669\` and aliases the rest as \`*wp\`,
|
|
58
|
+
# so an anchor-blind read silently nulls the leg on precisely the repos that pin most carefully. Both
|
|
59
|
+
# \`catalog:\` (the default catalog) and \`catalogs:\` (named ones) are walked. A value that is not a plain
|
|
60
|
+
# digit-version (a range) is NOT emitted, so a loose pinner falls through to the lock rather than being
|
|
61
|
+
# compared against an incomparable spec.
|
|
62
|
+
if grep -Eq '"@webpieces/[^"]*"[[:space:]]*:[[:space:]]*"catalog:' "$ROOT/package.json" 2>/dev/null && [ -f "$ROOT/pnpm-workspace.yaml" ]; then
|
|
63
|
+
WP_WS_CATALOGS="$(awk '
|
|
64
|
+
{ n=0; while (substr($0,n+1,1)==" ") n++; c=substr($0,n+1) }
|
|
65
|
+
c=="" || substr(c,1,1)=="#" { next }
|
|
66
|
+
{
|
|
67
|
+
ai=index(c,":")
|
|
68
|
+
if (ai>0) {
|
|
69
|
+
av=substr(c,ai+1); sub(/^[ \\t]+/,"",av)
|
|
70
|
+
if (substr(av,1,1)=="&") {
|
|
71
|
+
an=substr(av,2); sub(/[ \\t].*/,"",an)
|
|
72
|
+
sub(/^&[^ \\t]+[ \\t]*/,"",av)
|
|
73
|
+
sub(/[ \\t]+#.*/,"",av); gsub(/["\\047]/,"",av); sub(/[ \\t].*/,"",av)
|
|
74
|
+
if (an!="" && av!="") anch[an]=av
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
n==0 { mode=(c ~ /^catalog: *$/)?1:((c ~ /^catalogs: *$/)?2:0); cat=(mode==1)?"default":""; next }
|
|
79
|
+
mode==0 { next }
|
|
80
|
+
mode==2 && c ~ /^[^:]+: *$/ { cat=c; sub(/: *$/,"",cat); gsub(/["\\047 ]/,"",cat); next }
|
|
81
|
+
{
|
|
82
|
+
ki=index(c,":")
|
|
83
|
+
if (ki<=0) next
|
|
84
|
+
k=substr(c,1,ki-1); gsub(/["\\047 ]/,"",k)
|
|
85
|
+
if (substr(k,1,11)!="@webpieces/") next
|
|
86
|
+
v=substr(c,ki+1); sub(/^[ \\t]+/,"",v); sub(/^&[^ \\t]+[ \\t]*/,"",v)
|
|
87
|
+
sub(/[ \\t]+#.*/,"",v); gsub(/["\\047]/,"",v); sub(/[ \\t].*/,"",v)
|
|
88
|
+
if (v=="") next
|
|
89
|
+
nn++; key[nn]=cat " " k; ali[nn]=(substr(v,1,1)=="*")?substr(v,2):""; val[nn]=v
|
|
90
|
+
}
|
|
91
|
+
END { for (i=1;i<=nn;i++) { vv=(ali[i]=="")?val[i]:anch[ali[i]]; if (vv ~ /^[0-9]/) print key[i] " " vv } }
|
|
92
|
+
' "$ROOT/pnpm-workspace.yaml" 2>/dev/null)"
|
|
93
|
+
fi
|
|
94
|
+
if grep -Eq '"@webpieces/[^"]*"[[:space:]]*:[[:space:]]*"catalog:' "$ROOT/package.json" 2>/dev/null && [ -f "$ROOT/pnpm-lock.yaml" ]; then
|
|
95
|
+
WP_CATALOGS="$(awk '
|
|
96
|
+
{ n=0; while (substr($0,n+1,1)==" ") n++; c=substr($0,n+1) }
|
|
97
|
+
c=="" { next }
|
|
98
|
+
n==0 { incat=(c ~ /^catalogs: *$/)?1:0; cat=""; pkg=""; next }
|
|
99
|
+
incat==0 { next }
|
|
100
|
+
n==2 { cat=c; sub(/:.*/,"",cat); pkg=""; next }
|
|
101
|
+
n==4 { pkg=c; sub(/: *$/,"",pkg); gsub(/["\\047]/,"",pkg); next }
|
|
102
|
+
n==6 && substr(pkg,1,11)=="@webpieces/" && c ~ /^version:/ {
|
|
103
|
+
v=c; sub(/^version: */,"",v); gsub(/["\\047 ]/,"",v);
|
|
104
|
+
if (cat!="" && v!="") print cat " " pkg " " v
|
|
105
|
+
}
|
|
106
|
+
' "$ROOT/pnpm-lock.yaml" 2>/dev/null)"
|
|
107
|
+
fi
|
|
108
|
+
while IFS=' ' read -r WP_NAME WP_DECL; do
|
|
109
|
+
[ -n "$WP_NAME" ] || continue
|
|
110
|
+
# Fault U's input: the package is DECLARED (in any spec shape, in any dependency block of the root
|
|
111
|
+
# manifest). Recorded before every \`continue\` below, so a range or catalog spec still counts.
|
|
112
|
+
[ "$WP_NAME" = "ai-hook-rules" ] && WP_HOOK_PKG_DECLARED=1
|
|
113
|
+
# Resolve the declared spec to an EXACT version, or skip it: ranges (^ ~ workspace:*) never drift,
|
|
114
|
+
# and a catalog spec we cannot resolve is best-effort skipped rather than guessed.
|
|
115
|
+
case "$WP_DECL" in
|
|
116
|
+
catalog:*)
|
|
117
|
+
WP_CAT="\${WP_DECL#catalog:}"; [ -n "$WP_CAT" ] || WP_CAT="default"
|
|
118
|
+
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}')"
|
|
119
|
+
[ -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}')"
|
|
120
|
+
[ -n "$WP_DECL" ] || continue ;;
|
|
121
|
+
[0-9]*) : ;;
|
|
122
|
+
*) continue ;;
|
|
123
|
+
esac
|
|
124
|
+
# The release the rest of this repo is on — what fault U's cure should pin to.
|
|
125
|
+
[ -n "$WP_PIN" ] || WP_PIN="$WP_DECL"
|
|
126
|
+
WP_MANIFEST="$BIN_ROOT/node_modules/@webpieces/$WP_NAME/package.json"
|
|
127
|
+
[ -f "$WP_MANIFEST" ] || continue
|
|
128
|
+
WP_INST="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$WP_MANIFEST" | head -n1)"
|
|
129
|
+
[ -n "$WP_INST" ] || continue
|
|
130
|
+
if [ "$WP_DECL" != "$WP_INST" ]; then
|
|
131
|
+
DRIFT_PKG="@webpieces/$WP_NAME"
|
|
132
|
+
DRIFT_DECLARED="$WP_DECL"
|
|
133
|
+
DRIFT_INSTALLED="$WP_INST"
|
|
134
|
+
break
|
|
135
|
+
fi
|
|
136
|
+
done <<WPEOF
|
|
137
|
+
$(sed -n 's/.*"@webpieces\\/\\([A-Za-z0-9._-]*\\)"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1 \\2/p' "$ROOT/package.json")
|
|
138
|
+
WPEOF
|
|
139
|
+
fi
|
|
140
|
+
# A BORROWED BIN IS NOT SINGLE-TREE DRIFT, SO L0 MUST NOT ANSWER IT (2026-08-20).
|
|
141
|
+
#
|
|
142
|
+
# RESOLVE_BIN_SH walks UP for the bin, so in a linked worktree with no node_modules of its own BIN_ROOT
|
|
143
|
+
# is the MAIN tree. The scan above then compares $ROOT's DECLARED pin against $BIN_ROOT's INSTALLED
|
|
144
|
+
# version — a CROSS-TREE comparison it was reporting as fault D, single-tree drift. Everything
|
|
145
|
+
# downstream of that mislabel was wrong:
|
|
146
|
+
# • the cure. 'pnpm install' cannot "align node_modules" in a tree that has none; it MANUFACTURES one,
|
|
147
|
+
# at this tree's stale pin. That is precisely the state L1 row 8 (trinary-version-skew) blocks, so
|
|
148
|
+
# the L0 cure created the next block. One measured agent was walked from a pin disagreement, through
|
|
149
|
+
# 'pnpm install' offered as "(preferred) ... usually right", to a DOWNGRADED engine and a total block.
|
|
150
|
+
# • the analysis. Row 8 already reads all FOUR versions (both trees' pins and both installs), already
|
|
151
|
+
# knows which direction to move, already detects a deliberate pin bump, and already carries the
|
|
152
|
+
# escalate-and-STOP protocol. It could never run: ai-hook.sh hard-exits on any sh-side fault, so D
|
|
153
|
+
# PREEMPTED the guard that had the right answer.
|
|
154
|
+
# So: when the bin came from another tree, raise NO fault here and let the binary run. D stays exactly as
|
|
155
|
+
# it was when ROOT == BIN_ROOT — that IS single-tree drift, and row 8 cannot see it (VersionSyncGuard
|
|
156
|
+
# only applies to a linked worktree compared against a DIFFERENT main tree). Coverage does not gap: if
|
|
157
|
+
# the two pins agree but the two installs differ, row 8's quartet still has two distinct members and it
|
|
158
|
+
# still fires. X / U / K are untouched — a MISSING or CRASHED bin is about the bin, not about a version.
|
|
159
|
+
if [ "$BIN_ROOT" != "$ROOT" ]; then
|
|
160
|
+
DRIFT_PKG=""
|
|
161
|
+
DRIFT_DECLARED=""
|
|
162
|
+
DRIFT_INSTALLED=""
|
|
163
|
+
fi`;
|
|
164
|
+
//# sourceMappingURL=shim-version-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shim-version-drift.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-version-drift.ts"],"names":[],"mappings":";;;AAAA,8EAA8E;AAC9E,kEAAkE;AAClE,EAAE;AACF,sGAAsG;AACtG,8FAA8F;AAC9F,wGAAwG;AACxG,EAAE;AACF,gGAAgG;AAChG,iGAAiG;AACjG,qGAAqG;AACrG,8EAA8E;AACjE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoJnC,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// Shell fragment: L0's VERSION-DRIFT guard (fault D), in pure sh.\n//\n// Its own module for the same reason shim-audit-log.ts and shim-drift-fix.ts are: shim.ts renders the\n// whole shim body and is at its file-size cap. It is spliced back into renderShim() verbatim,\n// byte-for-byte, and it imports nothing — this has to run on a tree too broken to load the rule engine.\n//\n// The rationale for every branch is IN the fragment, as shell comments, and stays there: unlike\n// shim-drift-fix.ts's Fix Options, this text is the guard's own reasoning about a tree it cannot\n// otherwise explain, and a reader debugging a live block is reading the rendered .sh, not this file.\n// ---------------------------------------------------------------------------\nexport const VERSION_DRIFT_GUARD_SH = `# --- webpieces version-drift guard (pure sh — runs even when the installed guard bin is stale) -----\n# The committed shim is version-agnostic, so it keeps working right after a git pull, BEFORE the\n# matching pnpm install. That is exactly when node_modules can be STALE: an OLDER @webpieces than\n# package.json now pins, whose outdated validator rejects the NEWER webpieces.config.json with baffling\n# \"unknown rule\" errors. Detect that drift HERE (before exec'ing the possibly-stale bin): compare every\n# EXACT-pinned @webpieces/* version in the root package.json against the version actually installed in\n# node_modules; the first mismatch wins. Range specs (^ ~ workspace:*) are skipped, so they never\n# false-positive; best-effort — a version we cannot read is skipped. On drift we fall through to the\n# SAME fail-closed path as a missing bin (allow only pnpm install, deny the rest).\n#\n# pnpm CATALOGS: a dep pinned via \"catalog:\" / \"catalog:<name>\" carries NO digit-version in package.json,\n# so the old scraper matched nothing and the guard was BLIND to it — DRIFT_PKG stayed empty and the\n# stale bin ran (the 2026-07 \"0.3.369 vs 0.4.405\" incident). Resolve those specs through the top-level\n# \\`catalogs:\\` block of pnpm-lock.yaml (catalog -> pkg -> resolved version) before comparing.\n#\n# THE SAME PASS ANSWERS FAULT U (2026-08-05). Scraping root package.json is also the only way to learn\n# whether @webpieces/ai-hook-rules is DECLARED at all, and that is the difference between \"not installed\n# yet\" (X, cured by pnpm install) and \"nothing asks for it\" (U, where pnpm install is a guaranteed\n# no-op). WP_PIN carries the first EXACT @webpieces pin found, so U's deny can prescribe the version the\n# rest of the repo is already on rather than an unpinned add. Both are set BEFORE the range/catalog\n# \\`continue\\`s, so a repo pinning the package by range still counts as having declared it.\nDRIFT_PKG=\"\"\nDRIFT_DECLARED=\"\"\nDRIFT_INSTALLED=\"\"\nWP_HOOK_PKG_DECLARED=\"\"\nWP_PIN=\"\"\nif [ -f \"$ROOT/package.json\" ]; then\n # Only when a @webpieces dep actually uses a \"catalog:\" spec do we scan the (possibly huge) lockfile —\n # a cheap grep keeps the common, catalog-free repo from paying that cost on every tool call. One awk\n # pass over pnpm-lock.yaml emits \"<catalog> <@webpieces/pkg> <version>\" lines for the sh lookup below;\n # \\\\047 is a single quote (so this awk program carries none and stays safely single-quotable in sh).\n WP_CATALOGS=\"\"\n WP_WS_CATALOGS=\"\"\n # THE PIN LIVES IN pnpm-workspace.yaml, and the LOCK is only the fallback (2026-08-20).\n #\n # L0 used to learn the pin from pnpm-lock.yaml's \\`catalogs:\\` alone, while L1's WebpiecesVersions.readPin\n # reads pnpm-workspace.yaml. Two notions of \"the pin\", and the gap is exactly where the cure lands: an\n # agent told to raise this tree's pin edits pnpm-workspace.yaml, re-runs, and L0 still reports the OLD\n # number — because only \\`pnpm install\\` rewrites the lock — so it concludes the edit did nothing and\n # reaches for something worse. Reading the workspace manifest FIRST makes the edit visible immediately.\n #\n # It resolves the same two YAML shapes readPin does, and that is not optional: a repo pinning the whole\n # @webpieces family in lockstep writes the version ONCE as \\`&wp 0.4.669\\` and aliases the rest as \\`*wp\\`,\n # so an anchor-blind read silently nulls the leg on precisely the repos that pin most carefully. Both\n # \\`catalog:\\` (the default catalog) and \\`catalogs:\\` (named ones) are walked. A value that is not a plain\n # digit-version (a range) is NOT emitted, so a loose pinner falls through to the lock rather than being\n # compared against an incomparable spec.\n if grep -Eq '\"@webpieces/[^\"]*\"[[:space:]]*:[[:space:]]*\"catalog:' \"$ROOT/package.json\" 2>/dev/null && [ -f \"$ROOT/pnpm-workspace.yaml\" ]; then\n WP_WS_CATALOGS=\"$(awk '\n { n=0; while (substr($0,n+1,1)==\" \") n++; c=substr($0,n+1) }\n c==\"\" || substr(c,1,1)==\"#\" { next }\n {\n ai=index(c,\":\")\n if (ai>0) {\n av=substr(c,ai+1); sub(/^[ \\\\t]+/,\"\",av)\n if (substr(av,1,1)==\"&\") {\n an=substr(av,2); sub(/[ \\\\t].*/,\"\",an)\n sub(/^&[^ \\\\t]+[ \\\\t]*/,\"\",av)\n sub(/[ \\\\t]+#.*/,\"\",av); gsub(/[\"\\\\047]/,\"\",av); sub(/[ \\\\t].*/,\"\",av)\n if (an!=\"\" && av!=\"\") anch[an]=av\n }\n }\n }\n n==0 { mode=(c ~ /^catalog: *$/)?1:((c ~ /^catalogs: *$/)?2:0); cat=(mode==1)?\"default\":\"\"; next }\n mode==0 { next }\n mode==2 && c ~ /^[^:]+: *$/ { cat=c; sub(/: *$/,\"\",cat); gsub(/[\"\\\\047 ]/,\"\",cat); next }\n {\n ki=index(c,\":\")\n if (ki<=0) next\n k=substr(c,1,ki-1); gsub(/[\"\\\\047 ]/,\"\",k)\n if (substr(k,1,11)!=\"@webpieces/\") next\n v=substr(c,ki+1); sub(/^[ \\\\t]+/,\"\",v); sub(/^&[^ \\\\t]+[ \\\\t]*/,\"\",v)\n sub(/[ \\\\t]+#.*/,\"\",v); gsub(/[\"\\\\047]/,\"\",v); sub(/[ \\\\t].*/,\"\",v)\n if (v==\"\") next\n nn++; key[nn]=cat \" \" k; ali[nn]=(substr(v,1,1)==\"*\")?substr(v,2):\"\"; val[nn]=v\n }\n END { for (i=1;i<=nn;i++) { vv=(ali[i]==\"\")?val[i]:anch[ali[i]]; if (vv ~ /^[0-9]/) print key[i] \" \" vv } }\n ' \"$ROOT/pnpm-workspace.yaml\" 2>/dev/null)\"\n fi\n if grep -Eq '\"@webpieces/[^\"]*\"[[:space:]]*:[[:space:]]*\"catalog:' \"$ROOT/package.json\" 2>/dev/null && [ -f \"$ROOT/pnpm-lock.yaml\" ]; then\n WP_CATALOGS=\"$(awk '\n { n=0; while (substr($0,n+1,1)==\" \") n++; c=substr($0,n+1) }\n c==\"\" { next }\n n==0 { incat=(c ~ /^catalogs: *$/)?1:0; cat=\"\"; pkg=\"\"; next }\n incat==0 { next }\n n==2 { cat=c; sub(/:.*/,\"\",cat); pkg=\"\"; next }\n n==4 { pkg=c; sub(/: *$/,\"\",pkg); gsub(/[\"\\\\047]/,\"\",pkg); next }\n n==6 && substr(pkg,1,11)==\"@webpieces/\" && c ~ /^version:/ {\n v=c; sub(/^version: */,\"\",v); gsub(/[\"\\\\047 ]/,\"\",v);\n if (cat!=\"\" && v!=\"\") print cat \" \" pkg \" \" v\n }\n ' \"$ROOT/pnpm-lock.yaml\" 2>/dev/null)\"\n fi\n while IFS=' ' read -r WP_NAME WP_DECL; do\n [ -n \"$WP_NAME\" ] || continue\n # Fault U's input: the package is DECLARED (in any spec shape, in any dependency block of the root\n # manifest). Recorded before every \\`continue\\` below, so a range or catalog spec still counts.\n [ \"$WP_NAME\" = \"ai-hook-rules\" ] && WP_HOOK_PKG_DECLARED=1\n # Resolve the declared spec to an EXACT version, or skip it: ranges (^ ~ workspace:*) never drift,\n # and a catalog spec we cannot resolve is best-effort skipped rather than guessed.\n case \"$WP_DECL\" in\n catalog:*)\n WP_CAT=\"\\${WP_DECL#catalog:}\"; [ -n \"$WP_CAT\" ] || WP_CAT=\"default\"\n 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}')\"\n [ -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}')\"\n [ -n \"$WP_DECL\" ] || continue ;;\n [0-9]*) : ;;\n *) continue ;;\n esac\n # The release the rest of this repo is on — what fault U's cure should pin to.\n [ -n \"$WP_PIN\" ] || WP_PIN=\"$WP_DECL\"\n WP_MANIFEST=\"$BIN_ROOT/node_modules/@webpieces/$WP_NAME/package.json\"\n [ -f \"$WP_MANIFEST\" ] || continue\n WP_INST=\"$(sed -n 's/.*\"version\"[[:space:]]*:[[:space:]]*\"\\\\([^\"]*\\\\)\".*/\\\\1/p' \"$WP_MANIFEST\" | head -n1)\"\n [ -n \"$WP_INST\" ] || continue\n if [ \"$WP_DECL\" != \"$WP_INST\" ]; then\n DRIFT_PKG=\"@webpieces/$WP_NAME\"\n DRIFT_DECLARED=\"$WP_DECL\"\n DRIFT_INSTALLED=\"$WP_INST\"\n break\n fi\n done <<WPEOF\n$(sed -n 's/.*\"@webpieces\\\\/\\\\([A-Za-z0-9._-]*\\\\)\"[[:space:]]*:[[:space:]]*\"\\\\([^\"]*\\\\)\".*/\\\\1 \\\\2/p' \"$ROOT/package.json\")\nWPEOF\nfi\n# A BORROWED BIN IS NOT SINGLE-TREE DRIFT, SO L0 MUST NOT ANSWER IT (2026-08-20).\n#\n# RESOLVE_BIN_SH walks UP for the bin, so in a linked worktree with no node_modules of its own BIN_ROOT\n# is the MAIN tree. The scan above then compares $ROOT's DECLARED pin against $BIN_ROOT's INSTALLED\n# version — a CROSS-TREE comparison it was reporting as fault D, single-tree drift. Everything\n# downstream of that mislabel was wrong:\n# • the cure. 'pnpm install' cannot \"align node_modules\" in a tree that has none; it MANUFACTURES one,\n# at this tree's stale pin. That is precisely the state L1 row 8 (trinary-version-skew) blocks, so\n# the L0 cure created the next block. One measured agent was walked from a pin disagreement, through\n# 'pnpm install' offered as \"(preferred) ... usually right\", to a DOWNGRADED engine and a total block.\n# • the analysis. Row 8 already reads all FOUR versions (both trees' pins and both installs), already\n# knows which direction to move, already detects a deliberate pin bump, and already carries the\n# escalate-and-STOP protocol. It could never run: ai-hook.sh hard-exits on any sh-side fault, so D\n# PREEMPTED the guard that had the right answer.\n# So: when the bin came from another tree, raise NO fault here and let the binary run. D stays exactly as\n# it was when ROOT == BIN_ROOT — that IS single-tree drift, and row 8 cannot see it (VersionSyncGuard\n# only applies to a linked worktree compared against a DIFFERENT main tree). Coverage does not gap: if\n# the two pins agree but the two installs differ, row 8's quartet still has two distinct members and it\n# still fires. X / U / K are untouched — a MISSING or CRASHED bin is about the bin, not about a version.\nif [ \"$BIN_ROOT\" != \"$ROOT\" ]; then\n DRIFT_PKG=\"\"\n DRIFT_DECLARED=\"\"\n DRIFT_INSTALLED=\"\"\nfi`;\n"]}
|
package/src/bin/shim.js
CHANGED
|
@@ -18,6 +18,7 @@ const to_error_1 = require("../core/to-error");
|
|
|
18
18
|
const l0_allowlist_1 = require("./l0-allowlist");
|
|
19
19
|
const shim_audit_log_1 = require("./shim-audit-log");
|
|
20
20
|
const shim_drift_fix_1 = require("./shim-drift-fix");
|
|
21
|
+
const shim_version_drift_1 = require("./shim-version-drift");
|
|
21
22
|
// The allowlist moved to ./l0-allowlist (this module was over the file-size limit); re-exported here so
|
|
22
23
|
// every existing `from './shim'` import keeps working and there is still ONE name to import L0 by.
|
|
23
24
|
tslib_1.__exportStar(require("./l0-allowlist"), exports);
|
|
@@ -92,10 +93,10 @@ exports.NO_CHAINING_RULE = 'Run it EXACTLY as written - the allowlist matches th
|
|
|
92
93
|
//
|
|
93
94
|
// BIN_ROOT is not a curiosity: walking up ALONE re-creates the version straddle documented above
|
|
94
95
|
// committedShimStale(), where the shim of one tree is paired with the binary of another and the cure
|
|
95
|
-
// can never converge. So
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
96
|
+
// can never converge. So BIN_ROOT is RECORDED, and the drift scan below uses it to decide WHOSE
|
|
97
|
+
// question this is: when BIN_ROOT == ROOT the pin and the install belong to one tree and fault D
|
|
98
|
+
// answers; when they differ the disagreement is CROSS-TREE and L0 stays silent so L1 row 8
|
|
99
|
+
// (VersionSyncGuard), which reads all four versions, answers instead. See VERSION_DRIFT_GUARD_SH.
|
|
99
100
|
const RESOLVE_BIN_SH = `BIN_ROOT="\$ROOT"
|
|
100
101
|
BIN="\$ROOT/node_modules/.bin/\$BIN_NAME"
|
|
101
102
|
WP_WALK="\$ROOT"
|
|
@@ -132,96 +133,7 @@ fi`;
|
|
|
132
133
|
const ESCAPES_SH = `BS='\\' # one literal backslash, so no \\u001b / \\n escape sits in this source
|
|
133
134
|
ESC="\${BS}u001b" # the 6 chars: backslash u 0 0 1 b — Claude Code parses \\u001b → ESC
|
|
134
135
|
NL="\${BS}n" # the 2 chars: backslash n — parsed as a real newline inside the JSON string
|
|
135
|
-
WP_STILL_ALLOWED="Still allowed while this block is up:\${NL} - any Read\${NL} - any Write/Edit whose target is ${rules_config_1.CONFIG_FILENAME}\${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."`;
|
|
136
|
-
// Shell fragment: the version-drift guard (see its own block comment). Extracted to a module const so
|
|
137
|
-
// renderShim() stays within the method-line budget; it is spliced back in verbatim, byte-for-byte.
|
|
138
|
-
const VERSION_DRIFT_GUARD_SH = `# --- webpieces version-drift guard (pure sh — runs even when the installed guard bin is stale) -----
|
|
139
|
-
# The committed shim is version-agnostic, so it keeps working right after a git pull, BEFORE the
|
|
140
|
-
# matching pnpm install. That is exactly when node_modules can be STALE: an OLDER @webpieces than
|
|
141
|
-
# package.json now pins, whose outdated validator rejects the NEWER webpieces.config.json with baffling
|
|
142
|
-
# "unknown rule" errors. Detect that drift HERE (before exec'ing the possibly-stale bin): compare every
|
|
143
|
-
# EXACT-pinned @webpieces/* version in the root package.json against the version actually installed in
|
|
144
|
-
# node_modules; the first mismatch wins. Range specs (^ ~ workspace:*) are skipped, so they never
|
|
145
|
-
# false-positive; best-effort — a version we cannot read is skipped. On drift we fall through to the
|
|
146
|
-
# SAME fail-closed path as a missing bin (allow only pnpm install, deny the rest).
|
|
147
|
-
#
|
|
148
|
-
# pnpm CATALOGS: a dep pinned via "catalog:" / "catalog:<name>" carries NO digit-version in package.json,
|
|
149
|
-
# so the old scraper matched nothing and the guard was BLIND to it — DRIFT_PKG stayed empty and the
|
|
150
|
-
# stale bin ran (the 2026-07 "0.3.369 vs 0.4.405" incident). Resolve those specs through the top-level
|
|
151
|
-
# \`catalogs:\` block of pnpm-lock.yaml (catalog -> pkg -> resolved version) before comparing.
|
|
152
|
-
#
|
|
153
|
-
# THE SAME PASS ANSWERS FAULT U (2026-08-05). Scraping root package.json is also the only way to learn
|
|
154
|
-
# whether @webpieces/ai-hook-rules is DECLARED at all, and that is the difference between "not installed
|
|
155
|
-
# yet" (X, cured by pnpm install) and "nothing asks for it" (U, where pnpm install is a guaranteed
|
|
156
|
-
# no-op). WP_PIN carries the first EXACT @webpieces pin found, so U's deny can prescribe the version the
|
|
157
|
-
# rest of the repo is already on rather than an unpinned add. Both are set BEFORE the range/catalog
|
|
158
|
-
# \`continue\`s, so a repo pinning the package by range still counts as having declared it.
|
|
159
|
-
DRIFT_PKG=""
|
|
160
|
-
DRIFT_DECLARED=""
|
|
161
|
-
DRIFT_INSTALLED=""
|
|
162
|
-
WP_HOOK_PKG_DECLARED=""
|
|
163
|
-
WP_PIN=""
|
|
164
|
-
if [ -f "$ROOT/package.json" ]; then
|
|
165
|
-
# Only when a @webpieces dep actually uses a "catalog:" spec do we scan the (possibly huge) lockfile —
|
|
166
|
-
# a cheap grep keeps the common, catalog-free repo from paying that cost on every tool call. One awk
|
|
167
|
-
# pass over pnpm-lock.yaml emits "<catalog> <@webpieces/pkg> <version>" lines for the sh lookup below;
|
|
168
|
-
# \\047 is a single quote (so this awk program carries none and stays safely single-quotable in sh).
|
|
169
|
-
WP_CATALOGS=""
|
|
170
|
-
if grep -Eq '"@webpieces/[^"]*"[[:space:]]*:[[:space:]]*"catalog:' "$ROOT/package.json" 2>/dev/null && [ -f "$ROOT/pnpm-lock.yaml" ]; then
|
|
171
|
-
WP_CATALOGS="$(awk '
|
|
172
|
-
{ n=0; while (substr($0,n+1,1)==" ") n++; c=substr($0,n+1) }
|
|
173
|
-
c=="" { next }
|
|
174
|
-
n==0 { incat=(c ~ /^catalogs: *$/)?1:0; cat=""; pkg=""; next }
|
|
175
|
-
incat==0 { next }
|
|
176
|
-
n==2 { cat=c; sub(/:.*/,"",cat); pkg=""; next }
|
|
177
|
-
n==4 { pkg=c; sub(/: *$/,"",pkg); gsub(/["\\047]/,"",pkg); next }
|
|
178
|
-
n==6 && substr(pkg,1,11)=="@webpieces/" && c ~ /^version:/ {
|
|
179
|
-
v=c; sub(/^version: */,"",v); gsub(/["\\047 ]/,"",v);
|
|
180
|
-
if (cat!="" && v!="") print cat " " pkg " " v
|
|
181
|
-
}
|
|
182
|
-
' "$ROOT/pnpm-lock.yaml" 2>/dev/null)"
|
|
183
|
-
fi
|
|
184
|
-
while IFS=' ' read -r WP_NAME WP_DECL; do
|
|
185
|
-
[ -n "$WP_NAME" ] || continue
|
|
186
|
-
# Fault U's input: the package is DECLARED (in any spec shape, in any dependency block of the root
|
|
187
|
-
# manifest). Recorded before every \`continue\` below, so a range or catalog spec still counts.
|
|
188
|
-
[ "$WP_NAME" = "ai-hook-rules" ] && WP_HOOK_PKG_DECLARED=1
|
|
189
|
-
# Resolve the declared spec to an EXACT version, or skip it: ranges (^ ~ workspace:*) never drift,
|
|
190
|
-
# and a catalog spec we cannot resolve is best-effort skipped rather than guessed.
|
|
191
|
-
case "$WP_DECL" in
|
|
192
|
-
catalog:*)
|
|
193
|
-
WP_CAT="\${WP_DECL#catalog:}"; [ -n "$WP_CAT" ] || WP_CAT="default"
|
|
194
|
-
WP_DECL="$(printf '%s\\n' "$WP_CATALOGS" | awk -v c="$WP_CAT" -v p="@webpieces/$WP_NAME" '$1==c && $2==p {print $3; exit}')"
|
|
195
|
-
[ -n "$WP_DECL" ] || continue ;;
|
|
196
|
-
[0-9]*) : ;;
|
|
197
|
-
*) continue ;;
|
|
198
|
-
esac
|
|
199
|
-
# The release the rest of this repo is on — what fault U's cure should pin to.
|
|
200
|
-
[ -n "$WP_PIN" ] || WP_PIN="$WP_DECL"
|
|
201
|
-
WP_MANIFEST="$BIN_ROOT/node_modules/@webpieces/$WP_NAME/package.json"
|
|
202
|
-
[ -f "$WP_MANIFEST" ] || continue
|
|
203
|
-
WP_INST="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$WP_MANIFEST" | head -n1)"
|
|
204
|
-
[ -n "$WP_INST" ] || continue
|
|
205
|
-
if [ "$WP_DECL" != "$WP_INST" ]; then
|
|
206
|
-
DRIFT_PKG="@webpieces/$WP_NAME"
|
|
207
|
-
DRIFT_DECLARED="$WP_DECL"
|
|
208
|
-
DRIFT_INSTALLED="$WP_INST"
|
|
209
|
-
break
|
|
210
|
-
fi
|
|
211
|
-
done <<WPEOF
|
|
212
|
-
$(sed -n 's/.*"@webpieces\\/\\([A-Za-z0-9._-]*\\)"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1 \\2/p' "$ROOT/package.json")
|
|
213
|
-
WPEOF
|
|
214
|
-
fi
|
|
215
|
-
# THE CURE FOR A BORROWED node_modules RUNS IN THIS TREE, NOT WHEREVER THE BIN CAME FROM. A bare
|
|
216
|
-
# 'pnpm install' typed while the shell sits in the primary clone installs into the primary, changes
|
|
217
|
-
# nothing in the worktree being judged, and re-fires the identical fault — the four-cure straddle
|
|
218
|
-
# recorded above committedShimStale(). When the bin was inherited, prescribe the cd and say why.
|
|
219
|
-
WP_INSTALL_CMD="pnpm install"
|
|
220
|
-
WP_BORROW_NOTE=""
|
|
221
|
-
if [ "$BIN_ROOT" != "$ROOT" ]; then
|
|
222
|
-
WP_INSTALL_CMD="cd $ROOT && pnpm install"
|
|
223
|
-
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."
|
|
224
|
-
fi`;
|
|
136
|
+
WP_STILL_ALLOWED="Still allowed while this block is up:\${NL} - any Read\${NL} - any Write/Edit whose target is ${rules_config_1.CONFIG_FILENAME}, ${l0_allowlist_1.WORKSPACE_MANIFEST} or ${l0_allowlist_1.PACKAGE_MANIFEST}\${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."`;
|
|
225
137
|
// Shell fragment: run the installed guard bin and INSPECT its outcome, instead of exec'ing it.
|
|
226
138
|
//
|
|
227
139
|
// THE BUG THIS FIXES (guards silently fail-OPEN): the shim used to `exec "$BIN"`. exec REPLACES this
|
|
@@ -332,6 +244,16 @@ case "\$FILE" in
|
|
|
332
244
|
*/${rules_config_1.CONFIG_FILENAME}|${rules_config_1.CONFIG_FILENAME})
|
|
333
245
|
wp_log "\$WP_FAULT" ALLOW-CONFIG # the always-allowed recovery target — every guard is configured from it
|
|
334
246
|
exit 0 ;;
|
|
247
|
+
*/${l0_allowlist_1.WORKSPACE_MANIFEST}|${l0_allowlist_1.WORKSPACE_MANIFEST}|*/${l0_allowlist_1.PACKAGE_MANIFEST}|${l0_allowlist_1.PACKAGE_MANIFEST})
|
|
248
|
+
# A manifest AT THE ROOT OF A GOVERNED TREE, which is the only place the version pin lives. The test
|
|
249
|
+
# is the sibling ${rules_config_1.CONFIG_FILENAME} — TRACKED, so the main clone has one and every worktree has its
|
|
250
|
+
# own — and NOT \$ROOT, which names whichever tree supplied this shim and would deny the other's.
|
|
251
|
+
# Basename alone would be far worse here than in the JS half: this arm is TERMINAL (exit 0, the bin
|
|
252
|
+
# never runs), so every packages/**/package.json would be editable with nothing judging it.
|
|
253
|
+
if [ -f "\$(dirname -- "\$FILE")/${rules_config_1.CONFIG_FILENAME}" ]; then
|
|
254
|
+
wp_log "\$WP_FAULT" ALLOW-MANIFEST # raising the pin must be typable from inside the block
|
|
255
|
+
exit 0
|
|
256
|
+
fi ;;
|
|
335
257
|
esac
|
|
336
258
|
if printf '%s' "\$CMD" | grep -Eq '${l0_allowlist_1.L0_ALLOW_ERE_SH}'; then
|
|
337
259
|
wp_log "\$WP_FAULT" ALLOW-CURE # record the self-heal we let through (re-enables the guards)
|
|
@@ -416,7 +338,7 @@ elif [ -n "\$DRIFT_PKG" ]; then
|
|
|
416
338
|
if [ "\$DRIFT_DIR" = older ]; then
|
|
417
339
|
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
418
340
|
WP_HEAD="❌ webpieces ai-hooks blocked this call: webpieces version drift."
|
|
419
|
-
REASON="\$WP_HEAD\${NL}\${NL}${(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_DRIFT, '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} → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_DRIFT)}\${NL}\${NL}\${WP_STILL_ALLOWED}\${NL}\${NL} Fix Option 1: (preferred) the only cure - it makes node_modules match the pin\${NL} run EXACTLY: '
|
|
341
|
+
REASON="\$WP_HEAD\${NL}\${NL}${(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_DRIFT, '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} → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_DRIFT)}\${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}${exports.NO_CHAINING_RULE}"
|
|
420
342
|
else
|
|
421
343
|
# NEWER, or undecidable — the same choices apply either way, so the only thing the ambiguous
|
|
422
344
|
# case changes is the claim about which side is stale.
|
|
@@ -425,7 +347,7 @@ elif [ -n "\$DRIFT_PKG" ]; then
|
|
|
425
347
|
${shim_drift_fix_1.DRIFT_INVERSE_FIX_SH}
|
|
426
348
|
# The HEADLINE, kept in its own variable so DENY_EMIT_SH can paint ONLY it red (see there).
|
|
427
349
|
WP_HEAD="❌ webpieces ai-hooks blocked this call: webpieces version drift."
|
|
428
|
-
REASON="\$WP_HEAD\${NL}\${NL}${(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_DRIFT, '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} → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_DRIFT)}\${NL}\${NL}\${WP_STILL_ALLOWED}\${NL}\${NL}\${WP_FIX}\${
|
|
350
|
+
REASON="\$WP_HEAD\${NL}\${NL}${(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_DRIFT, '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} → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_DRIFT)}\${NL}\${NL}\${WP_STILL_ALLOWED}\${NL}\${NL}\${WP_FIX}\${NL}\${NL}${exports.NO_CHAINING_RULE}"
|
|
429
351
|
fi
|
|
430
352
|
else
|
|
431
353
|
# A LINKED WORKTREE is the overwhelmingly common way to land here with a perfectly healthy repo:
|
|
@@ -478,7 +400,7 @@ ${ESCAPES_SH}
|
|
|
478
400
|
# The BIN is resolved by walking UP from ROOT (as Node does), and BIN_ROOT records which tree supplied
|
|
479
401
|
# it — the version-drift guard below compares THIS tree's pin against THAT tree's installed version.
|
|
480
402
|
${RESOLVE_BIN_SH}
|
|
481
|
-
${VERSION_DRIFT_GUARD_SH}
|
|
403
|
+
${shim_version_drift_1.VERSION_DRIFT_GUARD_SH}
|
|
482
404
|
# Read the tool payload ONCE, up front. The shim no longer exec's the bin (see RUN_BIN_SH), so it must
|
|
483
405
|
# forward stdin to the bin itself — and it needs the payload again on the fail-closed path below.
|
|
484
406
|
PAYLOAD="$(cat)"
|