@webpieces/ai-hook-rules 0.4.676 → 0.4.678

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.
@@ -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
- // WHY THE FIX OPTIONS ARE COMPUTED RATHER THAN LISTED (2026-08-10, audit finding C6).
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
- // This message used to offer a bare `git pull origin main` on every branch, and the L0 allowlist
18
- // terminally ALLOWED it so on a FEATURE branch the guard told the agent to merge main into the branch
19
- // and then waved the command past redirect-how-to-merge-main, the guard whose whole job is stopping
20
- // exactly that. The fork point it destroys is what the 3-point merge, `nx affected --base=` and the PR
21
- // review diff are computed from, so nothing fails at the time; it surfaces later as a build that covered
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
- // ON MAIN the forward move is `git checkout main && git pull origin main` it ends ON main, merges
26
- // nothing into anything, is a no-op checkout when you are already there, and it is the ONE pull spelling
27
- // still on the L0 allowlist (see CHECKOUT_MAIN_PULL_BODY_ERE, which also records why it is a narrow
28
- // literal).
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 which falls to the
53
- // conservative, non-main half, exactly as an unknown branch should.
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
- [ -n "$WP_LOG_DIR" ] || wp_resolve_log_dir
60
- if [ "$WP_BRANCH" = main ]; then
61
- WP_FIX=" Fix Option 1: (preferred) you are on main and want what origin pins - move forward\${NL} run EXACTLY: '${l0_allowlist_1.CHECKOUT_MAIN_PULL_CMD}', then 'pnpm install'\${NL} Fix Option 2: you mean to stay on this code - the downgrade is the point\${NL} run EXACTLY: 'pnpm install'"
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
- WP_LOG_PATHS="$WP_LOG_DIR/${log_streams_1.L0_SHIM_STREAM}/"
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,qDAAqD;AAErD,iDAAwD;AAExD,8EAA8E;AAC9E,sGAAsG;AACtG,kFAAkF;AAClF,EAAE;AACF,sGAAsG;AACtG,uGAAuG;AACvG,uGAAuG;AACvG,sCAAsC;AACtC,EAAE;AACF,sFAAsF;AACtF,EAAE;AACF,iGAAiG;AACjG,wGAAwG;AACxG,oGAAoG;AACpG,uGAAuG;AACvG,yGAAyG;AACzG,wGAAwG;AACxG,oFAAoF;AACpF,EAAE;AACF,oGAAoG;AACpG,yGAAyG;AACzG,oGAAoG;AACpG,YAAY;AACZ,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,wGAAwG;AACxG,mGAAmG;AACnG,wGAAwG;AACxG,uGAAuG;AACvG,yGAAyG;AACzG,EAAE;AACF,qGAAqG;AACrG,oGAAoG;AACpG,oEAAoE;AACpE,EAAE;AACF,uGAAuG;AACvG,gGAAgG;AAChG,8EAA8E;AACjE,QAAA,oBAAoB,GAAG;;;4HAGwF,qCAAsB;;kCAEhH,4BAAc;+IAC+F,4BAAc;;OAEtJ,CAAC","sourcesContent":["import { L0_SHIM_STREAM } from '../core/log-streams';\n\nimport { CHECKOUT_MAIN_PULL_CMD } 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// WHY THE FIX OPTIONS ARE COMPUTED RATHER THAN LISTED (2026-08-10, audit finding C6).\n//\n// This message used to offer a bare `git pull origin main` on every branch, and the L0 allowlist\n// terminally ALLOWED it so on a FEATURE branch the guard told the agent to merge main into the branch\n// and then waved the command past redirect-how-to-merge-main, the guard whose whole job is stopping\n// exactly that. The fork point it destroys is what the 3-point merge, `nx affected --base=` and the PR\n// review diff are computed from, so nothing fails at the time; it surfaces later as a build that covered\n// the wrong scope and a PR diff describing work nobody did. A menu whose first option is blocked on the\n// branch you are standing on is worse than no menu, so the branch is asked instead.\n//\n// ON MAIN the forward move is `git checkout main && git pull origin main` — it ends ON main, merges\n// nothing into anything, is a no-op checkout when you are already there, and it is the ONE pull spelling\n// still on the L0 allowlist (see CHECKOUT_MAIN_PULL_BODY_ERE, which also records why it is a narrow\n// 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// ON A FEATURE BRANCH there is no honest forward move to name, and this deliberately does not invent\n// one. `pnpm install` aligns node_modules to YOUR branch pin and is the usual right answer, but it is a\n// DOWNGRADE and every drift event logged in this repo to date has been the OTHER direction, so a\n// feature branch that genuinely needs the NEWER pin is a shape the guard logic has never seen. Option 2\n// therefore says that plainly and prints the L0 audit-log paths, because those logs are the evidence a\n// real cure would have to be designed from, and an escalation nobody can act on is not an option at all.\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 which falls to the\n// conservative, non-main half, exactly as an unknown branch should.\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// ---------------------------------------------------------------------------\nexport const DRIFT_INVERSE_FIX_SH = `WP_BRANCH=\"$(git -C \"$ROOT\" branch --show-current 2>/dev/null)\"\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n if [ \"$WP_BRANCH\" = main ]; then\n WP_FIX=\" Fix Option 1: (preferred) you are on main and want what origin pins - move forward\\${NL} run EXACTLY: '${CHECKOUT_MAIN_PULL_CMD}', then 'pnpm install'\\${NL} Fix Option 2: you mean to stay on this code - the downgrade is the point\\${NL} run EXACTLY: 'pnpm install'\"\n else\n WP_LOG_PATHS=\"$WP_LOG_DIR/${L0_SHIM_STREAM}/\"\n [ \"$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_STREAM}/\"\n 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\"\n fi`;\n"]}
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 the walk is paired with a check DECLARED comes from `$ROOT/package.json`
96
- // (the tree being judged) and INSTALLED comes from `$BIN_ROOT/node_modules` (the binary actually
97
- // running). Equal no fault, keep reusing the inherited bin, which is the common case and stays free.
98
- // Different → fault D, cured by an install in THIS tree, which materialises its own node_modules.
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: '\$WP_INSTALL_CMD'\${WP_BORROW_NOTE}\${NL}\${NL}${exports.NO_CHAINING_RULE}"
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}\${WP_BORROW_NOTE}\${NL}\${NL}${exports.NO_CHAINING_RULE}"
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)"