@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.
@@ -4,6 +4,7 @@ exports.VersionSyncGuard = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const path = tslib_1.__importStar(require("path"));
7
+ const l0_allowlist_1 = require("../bin/l0-allowlist");
7
8
  const read_only_inspection_1 = require("./read-only-inspection");
8
9
  const webpieces_versions_1 = require("./webpieces-versions");
9
10
  /**
@@ -35,9 +36,15 @@ const webpieces_versions_1 = require("./webpieces-versions");
35
36
  * 1. WORK IN THE MAIN TREE — a main-tree-targeted command cannot classify as `worktree`, so it never
36
37
  * reaches this guard at all. No allowlist entry can break it because none is involved.
37
38
  * 2. EDIT THE MANIFESTS — `pnpm-workspace.yaml` / `package.json` edits are carved out in the runner
38
- * the same way `webpieces.config.json` already is, so the cure is typable from inside the block.
39
+ * (`runInternal`, beside the `webpieces.config.json` pass) and on the L0 allowlist
40
+ * (`MANIFEST_FILENAMES` / `isAllowed`), so the cure is typable from inside the block.
39
41
  * Reads and read-only inspection are never blocked either, so an agent can always look before it fixes.
40
42
  *
43
+ * THAT SECOND ESCAPE WAS FICTION UNTIL 2026-08-20, and this docblock asserted it anyway. Only
44
+ * `CONFIG_FILENAME` was ever carved out, so the one cure the report told a blocked agent to perform —
45
+ * raise this tree's pin — was itself blocked. It is real now, and the `main-ahead` branch of `fixLines`
46
+ * is what spends it. A future edit that narrows either carve-out has to change this text too.
47
+ *
41
48
  * ─── The MAIN tree is `tree.mainRoot`, never `tree.governedRoot` ───────────────────────────────────
42
49
  * The two differ for exactly the reader this guard is for. `governedRoot` is walked up from the payload
43
50
  * cwd to the nearest `webpieces.config.json`, and that file is TRACKED — a linked worktree has its own.
@@ -47,8 +54,19 @@ const webpieces_versions_1 = require("./webpieces-versions");
47
54
  * it is the same answer from every checkout. Measured 2026-08-10: a worktree on 0.4.624 with its own
48
55
  * install, a main clone on 0.4.616, and not one word from this guard.
49
56
  */
50
- /** The one file a pin lives in named here so the "did this branch bump it" check cannot drift from it. */
51
- const WORKSPACE_MANIFEST = 'pnpm-workspace.yaml';
57
+ /** One version reduced to what an order compare needs. Data-only a class, per CLAUDE.md. */
58
+ class VersionParts {
59
+ core;
60
+ pre;
61
+ constructor(
62
+ /** The numeric core, most-significant first. */
63
+ core,
64
+ /** The pre-release suffix INCLUDING its leading `-`, or '' for a plain release. */
65
+ pre) {
66
+ this.core = core;
67
+ this.pre = pre;
68
+ }
69
+ }
52
70
  class VersionSyncGuard {
53
71
  inspection = new read_only_inspection_1.ReadOnlyInspectionScan();
54
72
  versions = new webpieces_versions_1.WebpiecesVersions();
@@ -122,7 +140,12 @@ class VersionSyncGuard {
122
140
  // if each one argues its case. State the skew, show every version WITH its file, give the git cure
123
141
  // first, then the two structural escapes, then what is still allowed.
124
142
  report(tree, quartet) {
125
- const bump = this.isDeliberateBump(tree, quartet);
143
+ const skew = this.classify(tree, quartet);
144
+ // The two SELF-SERVE cases print NO escalation, and that omission is the deliverable rather than
145
+ // a saving: an escalation block on a cure the reader can perform HERE teaches it to stop and wait
146
+ // for a main agent who has nothing to do. The other three genuinely need the main tree to move.
147
+ const selfServe = skew === 'main-ahead' || skew === 'worktree-stale';
148
+ const escalation = selfServe ? [] : ['', ...this.escalationLines(tree, quartet, skew)];
126
149
  return [
127
150
  `❌ @webpieces version SKEW — this worktree and the main tree disagree, so work here is blocked.`,
128
151
  '',
@@ -131,9 +154,8 @@ class VersionSyncGuard {
131
154
  ` Whichever tree's hooks are live, one of these two releases lints, validates and builds`,
132
155
  ` this worktree — and it may be the one this manifest does not ask for.`,
133
156
  '',
134
- ...this.fixLines(tree, quartet, bump),
135
- '',
136
- ...this.escalationLines(tree, quartet, bump),
157
+ ...this.fixLines(tree, quartet, skew),
158
+ ...escalation,
137
159
  '',
138
160
  ` STILL ALLOWED HERE: every Read, read-only inspection, \`pnpm install\`, \`git pull\`/\`fetch\`,`,
139
161
  ` and edits to pnpm-workspace.yaml / package.json / webpieces.config.json.`,
@@ -141,6 +163,69 @@ class VersionSyncGuard {
141
163
  ` session's own governor.`,
142
164
  ].join('\n');
143
165
  }
166
+ /**
167
+ * WHICH of the five states this is — asked ONCE, so the FIX block and the escalation block can never
168
+ * describe two different diagnoses of the same skew.
169
+ *
170
+ * Order is not arbitrary. `main-inconsistent` is asked FIRST because it is a fault in the governing
171
+ * tree itself: comparing this worktree against a tree that disagrees with itself picks a direction
172
+ * out of two numbers that are not yet one answer. `bump` next, because a deliberate raise is the one
173
+ * shape where the direction is real but every ordinary cure is harmful.
174
+ */
175
+ classify(tree, quartet) {
176
+ const mainPin = quartet.main.pinned;
177
+ const mainInstalled = quartet.main.installed;
178
+ if (mainPin !== null && mainInstalled !== null && mainPin !== mainInstalled)
179
+ return 'main-inconsistent';
180
+ if (this.isDeliberateBump(tree, quartet))
181
+ return 'bump';
182
+ const wtPin = quartet.worktree.pinned;
183
+ if (mainInstalled !== null && wtPin === mainInstalled && quartet.worktree.installed !== null)
184
+ return 'worktree-stale';
185
+ return this.compare(mainInstalled, wtPin) > 0 ? 'main-ahead' : 'main-behind';
186
+ }
187
+ /**
188
+ * SEMVER ORDER of two versions: 1 when `a` is newer, -1 when older, 0 when equal OR undecidable.
189
+ *
190
+ * 0 is the FAIL-SAFE answer and every caller must read it as "no opinion": an unreadable leg, a
191
+ * dist-tag, two different pre-releases, or build metadata (which carries no precedence) all land
192
+ * there, and `classify` then falls to `main-behind`, the branch that ASKS rather than acts. Guessing
193
+ * a direction is how a downgrade gets prescribed, and this repo has already paid for that once.
194
+ *
195
+ * The same rules the shim's awk compare uses, so L0 and L1 cannot order one pair two ways: build
196
+ * metadata stripped, numeric cores compared component-wise, and a pre-release sorting BELOW its
197
+ * release.
198
+ */
199
+ compare(a, b) {
200
+ if (a === null || b === null || a === b)
201
+ return 0;
202
+ const aParts = this.parts(a);
203
+ const bParts = this.parts(b);
204
+ if (aParts === null || bParts === null)
205
+ return 0;
206
+ const width = Math.max(aParts.core.length, bParts.core.length);
207
+ for (let i = 0; i < width; i++) {
208
+ const av = aParts.core[i] ?? 0;
209
+ const bv = bParts.core[i] ?? 0;
210
+ if (av !== bv)
211
+ return av < bv ? -1 : 1;
212
+ }
213
+ if (aParts.pre === bParts.pre)
214
+ return 0;
215
+ if (aParts.pre !== '' && bParts.pre === '')
216
+ return -1;
217
+ if (aParts.pre === '' && bParts.pre !== '')
218
+ return 1;
219
+ return 0;
220
+ }
221
+ /** One version split into its numeric core and its pre-release suffix, or null if it is not numeric. */
222
+ parts(version) {
223
+ const noBuild = version.replace(/\+.*$/, '');
224
+ const core = noBuild.replace(/-.*$/, '');
225
+ if (!/^[0-9]+(\.[0-9]+)*$/.test(core))
226
+ return null;
227
+ return new VersionParts(core.split('.').map((n) => Number(n)), noBuild.slice(core.length));
228
+ }
144
229
  /**
145
230
  * The cure list, which is NOT the same list in both directions — but which, in BOTH directions, is a
146
231
  * list of things the reader ASKS FOR rather than runs.
@@ -170,8 +255,24 @@ class VersionSyncGuard {
170
255
  * competing with it is exactly the wall-of-text regression the L0 message diet exists to prevent.
171
256
  * Labelling carries the "not yours to run" fact instead, which is what the caps header does.
172
257
  */
173
- fixLines(tree, quartet, bump) {
174
- if (bump) {
258
+ fixLines(tree, quartet, skew) {
259
+ if (skew === 'main-inconsistent')
260
+ return this.mainInconsistentFix(tree, quartet);
261
+ if (skew === 'main-ahead')
262
+ return this.mainAheadFix(tree, quartet);
263
+ if (skew === 'worktree-stale') {
264
+ // Every PIN agrees, and so does main's install. The ONLY disagreeing leg is this tree's own
265
+ // node_modules — so this is the one state where a bare `pnpm install` HERE is not a guess,
266
+ // is not a downgrade, and needs nobody's permission. Saying so plainly matters because the
267
+ // other cases all warn AGAINST reaching for it.
268
+ return [
269
+ ` FIX — THIS ONE IS YOURS, AND IT IS ONE COMMAND. Every pin already agrees; only this`,
270
+ ` tree's own node_modules lags at ${this.show(quartet.worktree.installed).trim()}.`,
271
+ ` 1. Run \`pnpm install\` HERE, in ${tree.root}.`,
272
+ ` Nothing needs to move in the main tree and there is nobody to ask.`,
273
+ ];
274
+ }
275
+ if (skew === 'bump') {
175
276
  return [
176
277
  ` THIS BRANCH BUMPED THE PIN ON PURPOSE (${this.show(quartet.main.pinned).trim()} → ${this.show(quartet.worktree.pinned).trim()}), so the usual cures do NOT apply:`,
177
278
  ` • \`pnpm install\` cannot help in EITHER tree — an install materializes a pin, never moves one.`,
@@ -183,7 +284,7 @@ class VersionSyncGuard {
183
284
  ` 1. Tell main agent: this task has to be redone in the MAIN tree ${tree.mainRoot}`,
184
285
  ` — a version bump cannot be done in a worktree at all.`,
185
286
  ` 2. Tell main agent: OR raise the MAIN tree's pin to ${this.show(quartet.worktree.pinned).trim()} in`,
186
- ` ${tree.mainRoot}/${WORKSPACE_MANIFEST} and run \`pnpm install\` there.`,
287
+ ` ${tree.mainRoot}/${l0_allowlist_1.WORKSPACE_MANIFEST} and run \`pnpm install\` there.`,
187
288
  ` 3. Tell main agent: to tell you when that is complete.`,
188
289
  ` THEN AND ONLY THEN will this worktree — and every other subagent — work again.`,
189
290
  ];
@@ -204,6 +305,87 @@ class VersionSyncGuard {
204
305
  ` installing here is fine; what it may not have is a DIFFERENT @webpieces version.`,
205
306
  ];
206
307
  }
308
+ /**
309
+ * CASE A — the MAIN tree disagrees with ITSELF: its `node_modules` is on one version while its own
310
+ * `pnpm-workspace.yaml` pins another. Nothing in this worktree is wrong, and nothing this worktree
311
+ * does can help.
312
+ *
313
+ * The cure is deliberately the SMALL one. The generic branch prints `git checkout main && git pull`
314
+ * first, and that is over-prescribed here: both halves of the disagreement are already in that tree,
315
+ * so an install materializes the pin it already has and the skew is gone. Printing the pull as well
316
+ * invites a main agent to move main's commit for a fault that is not about main's commit at all.
317
+ */
318
+ mainInconsistentFix(tree, quartet) {
319
+ return [
320
+ ` THE MAIN TREE IS INTERNALLY INCONSISTENT — its node_modules is on ${this.show(quartet.main.installed).trim()} but its own`,
321
+ ` pin says ${this.show(quartet.main.pinned).trim()}. That is not this worktree's fault and not this worktree's to fix.`,
322
+ ` FIX — YOU CANNOT DO THIS FROM HERE. Cross-tree git is REFUSED to a subagent:`,
323
+ ` 1. Tell main agent: run \`pnpm install\` in ${tree.mainRoot} — no pull is needed, both`,
324
+ ` halves are already in that tree.`,
325
+ ` 2. Tell main agent: to tell you when that is complete.`,
326
+ ` THEN AND ONLY THEN will this worktree — and every other subagent — work again.`,
327
+ ];
328
+ }
329
+ /**
330
+ * CASE B — the MAIN tree already RUNS a newer release than this tree's pin asks for. This is the
331
+ * common case, and it is the one the old message got exactly backwards.
332
+ *
333
+ * It is the ONLY case a worktree fixes ITSELF. Nothing needs to move in the main tree — the version
334
+ * the guards will judge this tree by is already installed there — so the entire fix is to raise this
335
+ * tree's own pin to match, which is a one-line edit to a TRACKED file this tree owns. Printing an
336
+ * escalation here (as every earlier revision did) tells an agent to stop and wait for a main agent
337
+ * who has nothing to do, which is how a five-minute edit became a stalled turn.
338
+ *
339
+ * The edit is typable from inside the block because pnpm-workspace.yaml is on the L0 allowlist and
340
+ * carved out in the runner's edit path. That carve-out and this text ship together on purpose: a
341
+ * message prescribing a blocked call is the failure shape this repo has been burned by most often.
342
+ *
343
+ * ON A DETACHED HEAD the edit has no branch to belong to, so it is not offered — an edit that
344
+ * survives nothing is worse than no edit. Get onto a branch, then read this message again.
345
+ */
346
+ mainAheadFix(tree, quartet) {
347
+ const target = this.show(quartet.main.installed).trim();
348
+ if (this.isDetachedHead(tree.root)) {
349
+ return [
350
+ ` FIX — GET ONTO A BRANCH FIRST. \`git branch --show-current\` is empty in ${tree.root}, so`,
351
+ ` HEAD is DETACHED and the one-line pin edit below would belong to no branch at all.`,
352
+ ` 1. Check out a branch here, then re-run this command.`,
353
+ ` 2. The fix is then yours alone: set ${l0_allowlist_1.WORKSPACE_MANIFEST}'s catalog pin to ${target}.`,
354
+ ` Nothing needs to move in the main tree — it already runs ${target}.`,
355
+ ];
356
+ }
357
+ const install = quartet.worktree.installed !== null
358
+ ? [` 2. Then run \`pnpm install\` HERE — this tree has its own node_modules, so it must be`,
359
+ ` materialized at ${target} too.`]
360
+ : [` 2. Nothing else. This tree has no node_modules of its own, so there is nothing to install.`];
361
+ return [
362
+ ` FIX — THIS ONE IS YOURS, AND YOU CAN DO IT RIGHT HERE. The main tree is AHEAD: it already`,
363
+ ` runs ${target}, so nothing has to move there and there is nobody to ask.`,
364
+ ` 1. Edit ${tree.root}/${l0_allowlist_1.WORKSPACE_MANIFEST} — the catalog line for`,
365
+ ` ${webpieces_versions_1.UMBRELLA_PACKAGE} — and set it to ${target}. That edit is ALLOWED`,
366
+ ` right now, from inside this block.`,
367
+ ...install,
368
+ ` Do NOT run a bare \`pnpm install\` first: this tree's pin is the STALE side, so installing`,
369
+ ` before the edit materializes the OLD release and this guard fires again.`,
370
+ ];
371
+ }
372
+ /**
373
+ * Is HEAD DETACHED in this tree — i.e. is there no branch for a pin edit to belong to?
374
+ *
375
+ * `branch --show-current`, NOT `rev-parse --abbrev-ref HEAD`: it answers on an UNBORN branch (which
376
+ * every freshly-created worktree is until its first commit, and where `rev-parse` fatals) and prints
377
+ * EMPTY on a detached HEAD, which is exactly the distinction case B needs.
378
+ *
379
+ * A git FAILURE is NOT detached, and the difference is load-bearing: `--show-current` prints nothing
380
+ * in both situations, so keying off the output alone would tell anyone whose tree git cannot read
381
+ * (no repo, a broken index, git absent) that their HEAD is detached — a confident diagnosis of a
382
+ * state nobody measured. Only an exit-0-with-empty-output is detached; anything else falls back to
383
+ * the ordinary branch wording, whose worst case is prescribing an edit that turns out to be moot.
384
+ */
385
+ isDetachedHead(root) {
386
+ const result = (0, child_process_1.spawnSync)('git', ['-C', root, 'branch', '--show-current'], { encoding: 'utf8' });
387
+ return result.status === 0 && (result.stdout ?? '').trim() === '';
388
+ }
207
389
  /**
208
390
  * THE SUBAGENT CANNOT REACH THE MAIN TREE, so the message it is handed has to be the message it
209
391
  * FORWARDS. This used to be one sentence — "report to your coordinator that one of you must move to
@@ -237,27 +419,12 @@ class VersionSyncGuard {
237
419
  * further tool calls / RETRYING IS THE BUG / WAIT) — shouting the whole report would just restore
238
420
  * the wall of text the L0 message diet exists to prevent.
239
421
  */
240
- escalationLines(tree, quartet, bump) {
241
- const ask = bump
242
- ? [
243
- ` > A \`pnpm install\` in main will NOT fix this — main's PIN has to move. Pick one:`,
244
- ` > (a) I redo this task in the MAIN tree (a version bump cannot be done in a worktree), or`,
245
- ` > (b) you TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to`,
246
- ` > raise main's catalog pin to ${this.show(quartet.worktree.pinned).trim()} and \`pnpm install\` there, and to tell me when it is complete`,
247
- ` > so I can continue here. I cannot reach that tree from here.`,
248
- ]
249
- : [
250
- ` > Please TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to run`,
251
- ` > \`git checkout main && git pull && pnpm install\` there — it must NAME main, since a bare`,
252
- ` > pull moves whatever branch that tree is on — so both trees are on the same release, and`,
253
- ` > to tell me when it is complete so I can continue working, and what`,
254
- ` > \`ls node_modules/@webpieces\` shows there. I cannot reach that tree from here.`,
255
- ];
422
+ escalationLines(tree, quartet, skew) {
256
423
  return [
257
424
  ` SUBAGENT? You cannot fix the main tree from here. Forward this to your coordinator verbatim:`,
258
425
  ` > My worktree ${tree.root} is on @webpieces ${this.show(quartet.worktree.pinned).trim()};`,
259
426
  ` > the main tree ${tree.mainRoot} is on ${this.show(quartet.main.pinned).trim()}.`,
260
- ...ask,
427
+ ...this.askLines(tree, quartet, skew),
261
428
  ` THEN STOP WORKING NOW. Forwarding that message IS the end of your turn: make NO further tool`,
262
429
  ` calls and do NOT retry this one — RETRYING IS THE BUG. Every retry re-fires this identical`,
263
430
  ` deny and buries the ask above; one subagent did it 13 times, 25 across that whole session.`,
@@ -265,6 +432,46 @@ class VersionSyncGuard {
265
432
  ` then resume — nothing between.`,
266
433
  ];
267
434
  }
435
+ /**
436
+ * The forwardable ASK, one per case that needs one. `main-ahead` never reaches here — it has nothing
437
+ * to ask for — so the three shapes below cover every escalating case.
438
+ *
439
+ * The bump ask carries the upgrade sentence VERBATIM as Dean wrote it, because it answers the one
440
+ * question an upgrade agent gets wrong: "webpieces cannot be upgraded from a worktree" is not a
441
+ * missing permission it can route around, it is a property of who governs whom. Its `STOP` is about
442
+ * the ROLE ("you are the wrong agent for this task"); the caps beat at the end of the block is about
443
+ * the TURN ("forwarding ends it"). Two different instructions, and the closer stays last and unique.
444
+ */
445
+ askLines(tree, quartet, skew) {
446
+ if (skew === 'bump') {
447
+ return [
448
+ ` > A \`pnpm install\` in main will NOT fix this — main's PIN has to move. Pick one:`,
449
+ ` > (a) I redo this task in the MAIN tree (a version bump cannot be done in a worktree), or`,
450
+ ` > (b) you TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to`,
451
+ ` > raise main's catalog pin to ${this.show(quartet.worktree.pinned).trim()} and \`pnpm install\` there, and to tell me when it is complete`,
452
+ ` > so I can continue here. I cannot reach that tree from here.`,
453
+ ` If you are a subagent upgrading webpieces, STOP — only main agents in worktrees can do`,
454
+ ` this. Otherwise your main agent must \`git pull\` main: main has an earlier version of`,
455
+ ` webpieces pinned and must upgrade. You MUST tell the main agent to pull main and`,
456
+ ` \`pnpm install\`.`,
457
+ ];
458
+ }
459
+ if (skew === 'main-inconsistent') {
460
+ return [
461
+ ` > Please TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to run`,
462
+ ` > \`pnpm install\` there — its node_modules is on ${this.show(quartet.main.installed).trim()} but its own pin says`,
463
+ ` > ${this.show(quartet.main.pinned).trim()}, so no pull is needed — and to tell me when it is complete so I can`,
464
+ ` > continue working. I cannot reach that tree from here.`,
465
+ ];
466
+ }
467
+ return [
468
+ ` > Please TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to run`,
469
+ ` > \`git checkout main && git pull && pnpm install\` there — it must NAME main, since a bare`,
470
+ ` > pull moves whatever branch that tree is on — so both trees are on the same release, and`,
471
+ ` > to tell me when it is complete so I can continue working, and what`,
472
+ ` > \`ls node_modules/@webpieces\` shows there. I cannot reach that tree from here.`,
473
+ ];
474
+ }
268
475
  /**
269
476
  * Did THIS BRANCH change the pin, as opposed to the two trees having drifted onto different commits?
270
477
  *
@@ -281,8 +488,8 @@ class VersionSyncGuard {
281
488
  return false;
282
489
  if (quartet.main.pinned === quartet.worktree.pinned)
283
490
  return false;
284
- return this.touchesWorkspaceFile(tree.root, ['status', '--porcelain', '--', WORKSPACE_MANIFEST])
285
- || this.touchesWorkspaceFile(tree.root, ['diff', '--name-only', 'origin/main...HEAD', '--', WORKSPACE_MANIFEST]);
491
+ return this.touchesWorkspaceFile(tree.root, ['status', '--porcelain', '--', l0_allowlist_1.WORKSPACE_MANIFEST])
492
+ || this.touchesWorkspaceFile(tree.root, ['diff', '--name-only', 'origin/main...HEAD', '--', l0_allowlist_1.WORKSPACE_MANIFEST]);
286
493
  }
287
494
  touchesWorkspaceFile(root, args) {
288
495
  const result = (0, child_process_1.spawnSync)('git', ['-C', root, ...args], { encoding: 'utf8' });
@@ -1 +1 @@
1
- {"version":3,"file":"version-sync.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/version-sync.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as path from 'path';\n\nimport { EffectiveTree } from './effective-tree';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { UMBRELLA_PACKAGE, VersionQuartet, WebpiecesVersions } from './webpieces-versions';\n\n/**\n * L1 row 8 — a tree may not be worked in while its `@webpieces` version disagrees with the MAIN tree's.\n *\n * ─── WHY THIS EXISTS, and what it replaces ─────────────────────────────────────────────────────────\n * The guard hooks are registered ABSOLUTE (`$CLAUDE_PROJECT_DIR/...`), so the MAIN tree governs every\n * tree. That is not a new imposition — it is what was always happening, because a linked worktree has no\n * `node_modules` and ai-hook.sh's upward walk already executed the main tree's binary. The design now\n * says so out loud, which makes ONE case newly important: a worktree whose branch pins a DIFFERENT\n * release is being linted, validated and built by a release it never asked for.\n *\n * This guard makes that case LOUD instead of silent. It replaces `CoordinatorWorktreeGuard`, and the\n * replacement is strictly better on the axis that matters: the old guard keyed off WHO was asking\n * (coordinator vs subagent), and agent identity was measured untrustworthy — a worktree-isolated agent\n * whose tree is auto-reaped at a turn boundary silently resumes with its cwd on the primary clone\n * (reproduced twice, 2026-08-10). This guard keys off the PATH the command acts on, which cannot lie.\n *\n * ─── IT EXISTS TO STOP A LOOP, not to enforce tidiness ─────────────────────────────────────────────\n * A main/worktree manifest mismatch is exactly the shape that produced the founding incident: an agent\n * is shown a fault measured against one tree, runs the prescribed cure in another, the cure succeeds,\n * nothing the guard measures changes, and the guard re-denies. Five identical no-op `pnpm install`s and\n * a fabricated theory about the harness later, a human had to untangle it. Firing EARLY, with a message\n * that names all the versions and all their files, is the whole point. Any future proposal to soften\n * this to a warning must answer: what stops the five-install loop instead?\n *\n * ─── Never a deadlock ──────────────────────────────────────────────────────────────────────────────\n * Two structurally independent escapes, and neither depends on an allowlist regex staying in step:\n * 1. WORK IN THE MAIN TREE — a main-tree-targeted command cannot classify as `worktree`, so it never\n * reaches this guard at all. No allowlist entry can break it because none is involved.\n * 2. EDIT THE MANIFESTS — `pnpm-workspace.yaml` / `package.json` edits are carved out in the runner\n * the same way `webpieces.config.json` already is, so the cure is typable from inside the block.\n * Reads and read-only inspection are never blocked either, so an agent can always look before it fixes.\n *\n * ─── The MAIN tree is `tree.mainRoot`, never `tree.governedRoot` ───────────────────────────────────\n * The two differ for exactly the reader this guard is for. `governedRoot` is walked up from the payload\n * cwd to the nearest `webpieces.config.json`, and that file is TRACKED — a linked worktree has its own.\n * So for an agent resident in a worktree `governedRoot` IS the worktree, and comparing it against\n * `tree.root` compared the tree with ITSELF: trivially in sync, guard silent. `mainRoot` is git's\n * `<git-common-dir>/..`, i.e. the clone whose `node_modules` actually supplies the judging binary, and\n * it is the same answer from every checkout. Measured 2026-08-10: a worktree on 0.4.624 with its own\n * install, a main clone on 0.4.616, and not one word from this guard.\n */\n/** The one file a pin lives in — named here so the \"did this branch bump it\" check cannot drift from it. */\nconst WORKSPACE_MANIFEST = 'pnpm-workspace.yaml';\n\nexport class VersionSyncGuard {\n private readonly inspection = new ReadOnlyInspectionScan();\n private readonly versions = new WebpiecesVersions();\n\n /**\n * True when this tree is a linked worktree whose webpieces version disagrees with the main tree's.\n * This is the `V` dimension of the L1 matrix; the runner asks it for EVERY Bash call so the answer\n * lands in the audit log even when nothing blocks.\n */\n skewed(tree: EffectiveTree): boolean {\n if (!this.applies(tree)) return false;\n return !this.quartetFor(tree).inSync;\n }\n\n /** The deny report, or null to allow. */\n block(command: string, tree: EffectiveTree): string | null {\n if (!this.applies(tree)) return null;\n if (this.inspection.isReadOnlyInspection(command)) return null;\n if (this.isCureOrLook(command)) return null;\n const quartet = this.quartetFor(tree);\n if (quartet.inSync) return null;\n return this.report(tree, quartet);\n }\n\n /**\n * Commands that must pass EVEN WHILE THIS GUARD IS BLOCKING, because they are how you get unblocked\n * — or how you look at the tree first.\n *\n * `ReadOnlyInspectionScan` deliberately excludes git and gh OUTRIGHT (\"the guards exist to police\n * git, and read-only git is not a line worth drawing while flying blind\"), which is right for the\n * guards that police git but WRONG here: this guard's own prescribed cure is `git pull` in both\n * trees. Without this carve-out the guard would deny the exact command it tells the reader to run —\n * the single failure shape this repo has been burned by most often, and the reason the deny text is\n * allowed to promise \"STILL ALLOWED HERE: ... pnpm install, git pull/fetch\".\n *\n * Deliberately NARROW: fetching, pulling and installing cannot make a skew worse, and every one of\n * them moves the tree toward agreement. Anything that BUILDS, TESTS or COMMITS is still blocked,\n * because those are the operations that would be judged by the wrong release.\n */\n private isCureOrLook(command: string): boolean {\n const words = command.trim().split(/\\s+/);\n const head = words[0] ?? '';\n const sub = words[1] ?? '';\n if (head === 'git' || head === 'gh') {\n // `git -C <dir> <sub>` names its own directory; take the first non-flag word after it.\n const subcommand = sub === '-C' ? (words[3] ?? '') : sub;\n return ['pull', 'fetch', 'status', 'log', 'diff', 'show', 'branch', 'rev-parse', 'worktree'].includes(subcommand);\n }\n return (head === 'pnpm' || head === 'npm') && (sub === 'install' || sub === 'i');\n }\n\n /**\n * Is there a cross-tree comparison to make at all? TWO cheap conditions, no file read behind either:\n *\n * • K is `worktree` — git's `--git-dir ≠ --git-common-dir`, so a repo with no linked worktrees can\n * never reach the manifests. (In the primary clone this is also structural escape #1: \"do the\n * work in the main tree\" needs no allowlist entry to keep working.)\n * • the two roots are DIFFERENT directories — a tree compared with itself is not a skew, it is the\n * single-tree pin-vs-install question the L0 drift guard already owns.\n */\n private applies(tree: EffectiveTree): boolean {\n return tree.kind === 'worktree'\n && path.resolve(tree.mainRoot) !== path.resolve(tree.root);\n }\n\n /** Public so the runner can log all four versions on ALLOW as well as on BLOCK (audit, not just deny). */\n quartetFor(tree: EffectiveTree): VersionQuartet {\n return this.versions.quartet(tree.mainRoot, tree.root);\n }\n\n // Short on purpose — L0 ran a deliberate message diet and these blocks regress into a wall of text\n // if each one argues its case. State the skew, show every version WITH its file, give the git cure\n // first, then the two structural escapes, then what is still allowed.\n private report(tree: EffectiveTree, quartet: VersionQuartet): string {\n const bump = this.isDeliberateBump(tree, quartet);\n return [\n `❌ @webpieces version SKEW — this worktree and the main tree disagree, so work here is blocked.`,\n '',\n ...this.versionLines(tree, quartet),\n '',\n ` Whichever tree's hooks are live, one of these two releases lints, validates and builds`,\n ` this worktree — and it may be the one this manifest does not ask for.`,\n '',\n ...this.fixLines(tree, quartet, bump),\n '',\n ...this.escalationLines(tree, quartet, bump),\n '',\n ` STILL ALLOWED HERE: every Read, read-only inspection, \\`pnpm install\\`, \\`git pull\\`/\\`fetch\\`,`,\n ` and edits to pnpm-workspace.yaml / package.json / webpieces.config.json.`,\n ` Do NOT lower the MAIN tree's pin to match — that downgrades every tree, including this`,\n ` session's own governor.`,\n ].join('\\n');\n }\n\n /**\n * The cure list, which is NOT the same list in both directions — but which, in BOTH directions, is a\n * list of things the reader ASKS FOR rather than runs.\n *\n * The ordinary skew is two trees sitting on different commits of main. The pin is tracked, so putting\n * both trees on the same commit and installing genuinely converges them. That cure is WRONG, and worse\n * than useless, when the branch bumped the pin ON PURPOSE: pulling would revert the deliverable, and\n * an install cannot move a pin in either tree. Printing the git cure first in that case is what sent a\n * real upgrade agent round the loop below.\n *\n * WHAT THIS BLOCK IS NOT ALLOWED TO SAY, in either branch: `git -C <the main tree> pull`. Two defects\n * rode on that one line, and it was printed ABOVE the escalation block, so it was the first thing read.\n * (a) A worktree-isolated SUBAGENT — the overwhelmingly common reader of this deny — CANNOT run\n * cross-tree git at all; the harness refuses it (shim-deny-reason.ts records the same\n * measurement). The one printed cure was the one thing the reader could not perform.\n * (b) A bare `git pull` acts on whatever branch that tree currently has checked out, and the primary\n * clone is normally sitting on a feature branch. It pulls the feature branch, the manifest never\n * moves, the pin never converges, and this guard fires again. The cure has to NAME main:\n * `cd <main> && git checkout main && git pull`.\n * So every step is prefixed `Tell main agent:` — INDIVIDUALLY, not under one shared header. That\n * repetition is deliberate and is the deliverable: a reader who skims exactly one of these lines must\n * still see it is not their own action. Do not factor it out.\n *\n * The FIX block still prints ABOVE the escalation block, on purpose. Moving it below would split the\n * numbered steps from the versions they refer to, and the one place caps are spent on ENDING the turn\n * (STOP WORKING NOW / RETRYING IS THE BUG) has to stay last and stay unique — a second STOP beat\n * competing with it is exactly the wall-of-text regression the L0 message diet exists to prevent.\n * Labelling carries the \"not yours to run\" fact instead, which is what the caps header does.\n */\n private fixLines(tree: EffectiveTree, quartet: VersionQuartet, bump: boolean): readonly string[] {\n if (bump) {\n return [\n ` THIS BRANCH BUMPED THE PIN ON PURPOSE (${this.show(quartet.main.pinned).trim()} → ${this.show(quartet.worktree.pinned).trim()}), so the usual cures do NOT apply:`,\n ` • \\`pnpm install\\` cannot help in EITHER tree — an install materializes a pin, never moves one.`,\n ` • \\`git pull\\` here would revert the bump, which is the whole deliverable.`,\n ` • Wiping this tree's node_modules does NOT help — the two PINS still disagree, and the`,\n ` L0 drift guard blocks in this guard's place.`,\n ` FIX — YOU CANNOT DO THIS FROM HERE. BOTH ways out need the MAIN tree, and cross-tree`,\n ` git is REFUSED to a subagent, so every step below is something you ASK FOR, not run:`,\n ` 1. Tell main agent: this task has to be redone in the MAIN tree ${tree.mainRoot}`,\n ` — a version bump cannot be done in a worktree at all.`,\n ` 2. Tell main agent: OR raise the MAIN tree's pin to ${this.show(quartet.worktree.pinned).trim()} in`,\n ` ${tree.mainRoot}/${WORKSPACE_MANIFEST} and run \\`pnpm install\\` there.`,\n ` 3. Tell main agent: to tell you when that is complete.`,\n ` THEN AND ONLY THEN will this worktree — and every other subagent — work again.`,\n ];\n }\n return [\n ` FIX — YOU CANNOT DO THIS FROM HERE. Cross-tree git is REFUSED to a subagent, and a bare`,\n ` \\`git pull\\` moves whatever branch that tree is on — so the cure must NAME main, in main:`,\n ` 1. Tell main agent: \\`cd ${tree.mainRoot} && git checkout main && git pull\\``,\n ` 2. Tell main agent: then run \\`pnpm install\\` in ${tree.mainRoot}`,\n ` 3. Tell main agent: then report back what \\`ls ${tree.mainRoot}/node_modules/@webpieces\\``,\n ` shows, so we know whether the hook shim needs re-upgrading too.`,\n ` 4. Tell main agent: to tell you when ALL of that is complete.`,\n ` THEN AND ONLY THEN will this worktree — and every other subagent — work again.`,\n ` The two structural escapes, if converging the trees is not what the coordinator wants:`,\n ` • Do the work in the MAIN tree instead — it is never blocked by this guard.`,\n ` • Or use a separate CLONE, not a worktree, if this tree genuinely needs a DIFFERENT`,\n ` version: a clone gets its own governance. A worktree MAY have its own node_modules, so`,\n ` installing here is fine; what it may not have is a DIFFERENT @webpieces version.`,\n ];\n }\n\n /**\n * THE SUBAGENT CANNOT REACH THE MAIN TREE, so the message it is handed has to be the message it\n * FORWARDS. This used to be one sentence — \"report to your coordinator that one of you must move to\n * the other's version\" — with no command, no direction and nothing pasteable, and the result was a\n * subagent that correctly diagnosed the block, correctly escalated, and handed its coordinator a\n * request too vague to act on. Worse, the obvious guess (\"ask the coordinator to run `pnpm install`\n * in main\") is a NO-OP on a bump: it reinstalls main's own pin and nothing moves.\n *\n * So the escalation is rendered as literal text to forward, with the versions and the direction\n * already filled in. A human cannot sit with every agent; the deny has to carry the whole ask.\n *\n * The ask is ROUTED THROUGH THE MAIN AGENT rather than phrased as a command, and that distinction is\n * the whole point of this block. `git -C <mainRoot> pull` reads like something you run from wherever\n * you are standing — so a subagent reads it, tries it, and only then discovers that CROSS-TREE GIT IS\n * REFUSED to a worktree-isolated agent (same measurement shim-deny-reason.ts records: the harness\n * blocks `git -C <other tree>`, and it is git specifically). This skew needs a git pull in main, so\n * the one printed cure was the one thing this session cannot perform. Be precise about that and do\n * NOT overstate it into \"you cannot reach that tree at all\" — a local `cd <main> && pnpm install`\n * measurably DOES run, it simply cannot move main onto a different commit, which is what a skew\n * requires. The actor who can is the MAIN AGENT running in the MAIN git worktree, so the forwarded\n * text asks for exactly that — and asks to be TOLD WHEN IT IS DONE, because \"the work happened\" is\n * the event that unblocks this subagent and it has no way to observe it otherwise.\n *\n * And it has to say STOP, in caps, because forwarding is only half of what the subagent must do. One\n * measured subagent transcript re-fired this identical deny 13 TIMES (25 across the whole session,\n * counting two sibling subagents and two parent sessions): the subagent read it, escalated exactly\n * as asked — and then kept making tool calls, because nothing here said that forwarding ENDS the\n * turn. Every retry cost a round trip and pushed the one message that mattered further up the\n * scrollback. The block is not transient and no command from this tree slips past it, so retrying is\n * never a strategy; it is the bug. The caps are spent ONLY on that beat (STOP WORKING NOW / NO\n * further tool calls / RETRYING IS THE BUG / WAIT) — shouting the whole report would just restore\n * the wall of text the L0 message diet exists to prevent.\n */\n private escalationLines(tree: EffectiveTree, quartet: VersionQuartet, bump: boolean): readonly string[] {\n const ask = bump\n ? [\n ` > A \\`pnpm install\\` in main will NOT fix this — main's PIN has to move. Pick one:`,\n ` > (a) I redo this task in the MAIN tree (a version bump cannot be done in a worktree), or`,\n ` > (b) you TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to`,\n ` > raise main's catalog pin to ${this.show(quartet.worktree.pinned).trim()} and \\`pnpm install\\` there, and to tell me when it is complete`,\n ` > so I can continue here. I cannot reach that tree from here.`,\n ]\n : [\n ` > Please TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to run`,\n ` > \\`git checkout main && git pull && pnpm install\\` there — it must NAME main, since a bare`,\n ` > pull moves whatever branch that tree is on — so both trees are on the same release, and`,\n ` > to tell me when it is complete so I can continue working, and what`,\n ` > \\`ls node_modules/@webpieces\\` shows there. I cannot reach that tree from here.`,\n ];\n return [\n ` SUBAGENT? You cannot fix the main tree from here. Forward this to your coordinator verbatim:`,\n ` > My worktree ${tree.root} is on @webpieces ${this.show(quartet.worktree.pinned).trim()};`,\n ` > the main tree ${tree.mainRoot} is on ${this.show(quartet.main.pinned).trim()}.`,\n ...ask,\n ` THEN STOP WORKING NOW. Forwarding that message IS the end of your turn: make NO further tool`,\n ` calls and do NOT retry this one — RETRYING IS THE BUG. Every retry re-fires this identical`,\n ` deny and buries the ask above; one subagent did it 13 times, 25 across that whole session.`,\n ` Nothing you can do from this tree clears it. WAIT for the main agent to confirm it is done,`,\n ` then resume — nothing between.`,\n ];\n }\n\n /**\n * Did THIS BRANCH change the pin, as opposed to the two trees having drifted onto different commits?\n *\n * Only answerable now that both pin legs actually resolve — before the catalog reader followed YAML\n * anchors they both read null on the repos that pin via an anchor, so every skew looked alike and the\n * report could only ever print the one generic cure.\n *\n * Two git spawns worst case, on the BLOCK path only (this is never reached on an allow), and\n * best-effort: a git failure answers \"not a deliberate bump\", which falls back to the generic cure\n * that was the only text this report had before.\n */\n private isDeliberateBump(tree: EffectiveTree, quartet: VersionQuartet): boolean {\n if (quartet.main.pinned === null || quartet.worktree.pinned === null) return false;\n if (quartet.main.pinned === quartet.worktree.pinned) return false;\n return this.touchesWorkspaceFile(tree.root, ['status', '--porcelain', '--', WORKSPACE_MANIFEST])\n || this.touchesWorkspaceFile(tree.root, ['diff', '--name-only', 'origin/main...HEAD', '--', WORKSPACE_MANIFEST]);\n }\n\n private touchesWorkspaceFile(root: string, args: readonly string[]): boolean {\n const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() !== '';\n }\n\n // Every version WITH the file it came from. An agent that is told \"they disagree\" without being told\n // WHICH FILE to edit re-derives it by grepping, which is exactly the turn-burning this guard exists\n // to prevent. Unreadable legs are printed as `-` rather than omitted, so the reader can tell\n // \"this one is absent\" from \"I forgot to look\".\n private versionLines(tree: EffectiveTree, quartet: VersionQuartet): readonly string[] {\n const lines = [\n ` main pin ${this.show(quartet.main.pinned)} ${tree.mainRoot}/pnpm-workspace.yaml`,\n ` main installed ${this.show(quartet.main.installed)} ${tree.mainRoot}/node_modules/${UMBRELLA_PACKAGE}`,\n ` ^ the binary judging this very call`,\n ` this worktree ${this.show(quartet.worktree.pinned)} ${tree.root}/pnpm-workspace.yaml`,\n ];\n if (quartet.worktree.installed !== null) {\n lines.push(` its installed ${this.show(quartet.worktree.installed)} ${tree.root}/node_modules/${UMBRELLA_PACKAGE}`);\n lines.push(' ^ what nx, vitest and eslint load IN this tree');\n }\n const others = this.versions.otherWorktrees(tree.mainRoot, tree.root);\n if (others.length > 0) {\n lines.push(` NOTE ${others.length} other worktree(s) exist and are governed the same way — if they are`);\n lines.push(' skewed too, their agents are already mis-governed. Consider clones, or');\n lines.push(' serializing the work in the main tree.');\n }\n return lines;\n }\n\n private show(version: string | null): string {\n return (version ?? '-').padEnd(10);\n }\n}\n"]}
1
+ {"version":3,"file":"version-sync.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/version-sync.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as path from 'path';\n\nimport { WORKSPACE_MANIFEST } from '../bin/l0-allowlist';\nimport { EffectiveTree } from './effective-tree';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { UMBRELLA_PACKAGE, VersionQuartet, WebpiecesVersions } from './webpieces-versions';\n\n/**\n * L1 row 8 — a tree may not be worked in while its `@webpieces` version disagrees with the MAIN tree's.\n *\n * ─── WHY THIS EXISTS, and what it replaces ─────────────────────────────────────────────────────────\n * The guard hooks are registered ABSOLUTE (`$CLAUDE_PROJECT_DIR/...`), so the MAIN tree governs every\n * tree. That is not a new imposition — it is what was always happening, because a linked worktree has no\n * `node_modules` and ai-hook.sh's upward walk already executed the main tree's binary. The design now\n * says so out loud, which makes ONE case newly important: a worktree whose branch pins a DIFFERENT\n * release is being linted, validated and built by a release it never asked for.\n *\n * This guard makes that case LOUD instead of silent. It replaces `CoordinatorWorktreeGuard`, and the\n * replacement is strictly better on the axis that matters: the old guard keyed off WHO was asking\n * (coordinator vs subagent), and agent identity was measured untrustworthy — a worktree-isolated agent\n * whose tree is auto-reaped at a turn boundary silently resumes with its cwd on the primary clone\n * (reproduced twice, 2026-08-10). This guard keys off the PATH the command acts on, which cannot lie.\n *\n * ─── IT EXISTS TO STOP A LOOP, not to enforce tidiness ─────────────────────────────────────────────\n * A main/worktree manifest mismatch is exactly the shape that produced the founding incident: an agent\n * is shown a fault measured against one tree, runs the prescribed cure in another, the cure succeeds,\n * nothing the guard measures changes, and the guard re-denies. Five identical no-op `pnpm install`s and\n * a fabricated theory about the harness later, a human had to untangle it. Firing EARLY, with a message\n * that names all the versions and all their files, is the whole point. Any future proposal to soften\n * this to a warning must answer: what stops the five-install loop instead?\n *\n * ─── Never a deadlock ──────────────────────────────────────────────────────────────────────────────\n * Two structurally independent escapes, and neither depends on an allowlist regex staying in step:\n * 1. WORK IN THE MAIN TREE — a main-tree-targeted command cannot classify as `worktree`, so it never\n * reaches this guard at all. No allowlist entry can break it because none is involved.\n * 2. EDIT THE MANIFESTS — `pnpm-workspace.yaml` / `package.json` edits are carved out in the runner\n * (`runInternal`, beside the `webpieces.config.json` pass) and on the L0 allowlist\n * (`MANIFEST_FILENAMES` / `isAllowed`), so the cure is typable from inside the block.\n * Reads and read-only inspection are never blocked either, so an agent can always look before it fixes.\n *\n * THAT SECOND ESCAPE WAS FICTION UNTIL 2026-08-20, and this docblock asserted it anyway. Only\n * `CONFIG_FILENAME` was ever carved out, so the one cure the report told a blocked agent to perform —\n * raise this tree's pin — was itself blocked. It is real now, and the `main-ahead` branch of `fixLines`\n * is what spends it. A future edit that narrows either carve-out has to change this text too.\n *\n * ─── The MAIN tree is `tree.mainRoot`, never `tree.governedRoot` ───────────────────────────────────\n * The two differ for exactly the reader this guard is for. `governedRoot` is walked up from the payload\n * cwd to the nearest `webpieces.config.json`, and that file is TRACKED — a linked worktree has its own.\n * So for an agent resident in a worktree `governedRoot` IS the worktree, and comparing it against\n * `tree.root` compared the tree with ITSELF: trivially in sync, guard silent. `mainRoot` is git's\n * `<git-common-dir>/..`, i.e. the clone whose `node_modules` actually supplies the judging binary, and\n * it is the same answer from every checkout. Measured 2026-08-10: a worktree on 0.4.624 with its own\n * install, a main clone on 0.4.616, and not one word from this guard.\n */\n/** One version reduced to what an order compare needs. Data-only → a class, per CLAUDE.md. */\nclass VersionParts {\n constructor(\n /** The numeric core, most-significant first. */\n readonly core: readonly number[],\n /** The pre-release suffix INCLUDING its leading `-`, or '' for a plain release. */\n readonly pre: string,\n ) {}\n}\n\n/**\n * WHICH of the five states this skew is. The whole point of naming them is that they do NOT share a\n * cure, and printing the wrong one is what walked a real agent backwards through a downgrade:\n *\n * `main-inconsistent` — the MAIN tree disagrees with ITSELF (its pin ≠ its own node_modules). Nothing\n * about this worktree is wrong; only the main agent can fix it, with one install.\n * `bump` — this branch RAISED the pin on purpose. The deliverable is the pin, so every\n * ordinary cure would revert it. Main has to come up to meet it.\n * `main-ahead` — the governing tree already RUNS a newer release than this tree's pin asks for.\n * This is the common case, and it is the ONE case the worktree fixes ITSELF, by\n * raising its own pin. No escalation, nobody to ask, nothing to wait for.\n * `worktree-stale` — every PIN agrees and so does main's install; only this tree's OWN node_modules\n * lags. The one state where a bare `pnpm install` HERE is exactly right.\n * `main-behind` — main is genuinely older and must move forward. Only the main agent can pull it.\n */\ntype SkewCase = 'main-inconsistent' | 'bump' | 'main-ahead' | 'worktree-stale' | 'main-behind';\n\nexport class VersionSyncGuard {\n private readonly inspection = new ReadOnlyInspectionScan();\n private readonly versions = new WebpiecesVersions();\n\n /**\n * True when this tree is a linked worktree whose webpieces version disagrees with the main tree's.\n * This is the `V` dimension of the L1 matrix; the runner asks it for EVERY Bash call so the answer\n * lands in the audit log even when nothing blocks.\n */\n skewed(tree: EffectiveTree): boolean {\n if (!this.applies(tree)) return false;\n return !this.quartetFor(tree).inSync;\n }\n\n /** The deny report, or null to allow. */\n block(command: string, tree: EffectiveTree): string | null {\n if (!this.applies(tree)) return null;\n if (this.inspection.isReadOnlyInspection(command)) return null;\n if (this.isCureOrLook(command)) return null;\n const quartet = this.quartetFor(tree);\n if (quartet.inSync) return null;\n return this.report(tree, quartet);\n }\n\n /**\n * Commands that must pass EVEN WHILE THIS GUARD IS BLOCKING, because they are how you get unblocked\n * — or how you look at the tree first.\n *\n * `ReadOnlyInspectionScan` deliberately excludes git and gh OUTRIGHT (\"the guards exist to police\n * git, and read-only git is not a line worth drawing while flying blind\"), which is right for the\n * guards that police git but WRONG here: this guard's own prescribed cure is `git pull` in both\n * trees. Without this carve-out the guard would deny the exact command it tells the reader to run —\n * the single failure shape this repo has been burned by most often, and the reason the deny text is\n * allowed to promise \"STILL ALLOWED HERE: ... pnpm install, git pull/fetch\".\n *\n * Deliberately NARROW: fetching, pulling and installing cannot make a skew worse, and every one of\n * them moves the tree toward agreement. Anything that BUILDS, TESTS or COMMITS is still blocked,\n * because those are the operations that would be judged by the wrong release.\n */\n private isCureOrLook(command: string): boolean {\n const words = command.trim().split(/\\s+/);\n const head = words[0] ?? '';\n const sub = words[1] ?? '';\n if (head === 'git' || head === 'gh') {\n // `git -C <dir> <sub>` names its own directory; take the first non-flag word after it.\n const subcommand = sub === '-C' ? (words[3] ?? '') : sub;\n return ['pull', 'fetch', 'status', 'log', 'diff', 'show', 'branch', 'rev-parse', 'worktree'].includes(subcommand);\n }\n return (head === 'pnpm' || head === 'npm') && (sub === 'install' || sub === 'i');\n }\n\n /**\n * Is there a cross-tree comparison to make at all? TWO cheap conditions, no file read behind either:\n *\n * • K is `worktree` — git's `--git-dir ≠ --git-common-dir`, so a repo with no linked worktrees can\n * never reach the manifests. (In the primary clone this is also structural escape #1: \"do the\n * work in the main tree\" needs no allowlist entry to keep working.)\n * • the two roots are DIFFERENT directories — a tree compared with itself is not a skew, it is the\n * single-tree pin-vs-install question the L0 drift guard already owns.\n */\n private applies(tree: EffectiveTree): boolean {\n return tree.kind === 'worktree'\n && path.resolve(tree.mainRoot) !== path.resolve(tree.root);\n }\n\n /** Public so the runner can log all four versions on ALLOW as well as on BLOCK (audit, not just deny). */\n quartetFor(tree: EffectiveTree): VersionQuartet {\n return this.versions.quartet(tree.mainRoot, tree.root);\n }\n\n // Short on purpose — L0 ran a deliberate message diet and these blocks regress into a wall of text\n // if each one argues its case. State the skew, show every version WITH its file, give the git cure\n // first, then the two structural escapes, then what is still allowed.\n private report(tree: EffectiveTree, quartet: VersionQuartet): string {\n const skew = this.classify(tree, quartet);\n // The two SELF-SERVE cases print NO escalation, and that omission is the deliverable rather than\n // a saving: an escalation block on a cure the reader can perform HERE teaches it to stop and wait\n // for a main agent who has nothing to do. The other three genuinely need the main tree to move.\n const selfServe = skew === 'main-ahead' || skew === 'worktree-stale';\n const escalation = selfServe ? [] : ['', ...this.escalationLines(tree, quartet, skew)];\n return [\n `❌ @webpieces version SKEW — this worktree and the main tree disagree, so work here is blocked.`,\n '',\n ...this.versionLines(tree, quartet),\n '',\n ` Whichever tree's hooks are live, one of these two releases lints, validates and builds`,\n ` this worktree — and it may be the one this manifest does not ask for.`,\n '',\n ...this.fixLines(tree, quartet, skew),\n ...escalation,\n '',\n ` STILL ALLOWED HERE: every Read, read-only inspection, \\`pnpm install\\`, \\`git pull\\`/\\`fetch\\`,`,\n ` and edits to pnpm-workspace.yaml / package.json / webpieces.config.json.`,\n ` Do NOT lower the MAIN tree's pin to match — that downgrades every tree, including this`,\n ` session's own governor.`,\n ].join('\\n');\n }\n\n /**\n * WHICH of the five states this is — asked ONCE, so the FIX block and the escalation block can never\n * describe two different diagnoses of the same skew.\n *\n * Order is not arbitrary. `main-inconsistent` is asked FIRST because it is a fault in the governing\n * tree itself: comparing this worktree against a tree that disagrees with itself picks a direction\n * out of two numbers that are not yet one answer. `bump` next, because a deliberate raise is the one\n * shape where the direction is real but every ordinary cure is harmful.\n */\n private classify(tree: EffectiveTree, quartet: VersionQuartet): SkewCase {\n const mainPin = quartet.main.pinned;\n const mainInstalled = quartet.main.installed;\n if (mainPin !== null && mainInstalled !== null && mainPin !== mainInstalled) return 'main-inconsistent';\n if (this.isDeliberateBump(tree, quartet)) return 'bump';\n const wtPin = quartet.worktree.pinned;\n if (mainInstalled !== null && wtPin === mainInstalled && quartet.worktree.installed !== null) return 'worktree-stale';\n return this.compare(mainInstalled, wtPin) > 0 ? 'main-ahead' : 'main-behind';\n }\n\n /**\n * SEMVER ORDER of two versions: 1 when `a` is newer, -1 when older, 0 when equal OR undecidable.\n *\n * 0 is the FAIL-SAFE answer and every caller must read it as \"no opinion\": an unreadable leg, a\n * dist-tag, two different pre-releases, or build metadata (which carries no precedence) all land\n * there, and `classify` then falls to `main-behind`, the branch that ASKS rather than acts. Guessing\n * a direction is how a downgrade gets prescribed, and this repo has already paid for that once.\n *\n * The same rules the shim's awk compare uses, so L0 and L1 cannot order one pair two ways: build\n * metadata stripped, numeric cores compared component-wise, and a pre-release sorting BELOW its\n * release.\n */\n private compare(a: string | null, b: string | null): number {\n if (a === null || b === null || a === b) return 0;\n const aParts = this.parts(a);\n const bParts = this.parts(b);\n if (aParts === null || bParts === null) return 0;\n const width = Math.max(aParts.core.length, bParts.core.length);\n for (let i = 0; i < width; i++) {\n const av = aParts.core[i] ?? 0;\n const bv = bParts.core[i] ?? 0;\n if (av !== bv) return av < bv ? -1 : 1;\n }\n if (aParts.pre === bParts.pre) return 0;\n if (aParts.pre !== '' && bParts.pre === '') return -1;\n if (aParts.pre === '' && bParts.pre !== '') return 1;\n return 0;\n }\n\n /** One version split into its numeric core and its pre-release suffix, or null if it is not numeric. */\n private parts(version: string): VersionParts | null {\n const noBuild = version.replace(/\\+.*$/, '');\n const core = noBuild.replace(/-.*$/, '');\n if (!/^[0-9]+(\\.[0-9]+)*$/.test(core)) return null;\n return new VersionParts(\n core.split('.').map((n: string): number => Number(n)),\n noBuild.slice(core.length),\n );\n }\n\n /**\n * The cure list, which is NOT the same list in both directions — but which, in BOTH directions, is a\n * list of things the reader ASKS FOR rather than runs.\n *\n * The ordinary skew is two trees sitting on different commits of main. The pin is tracked, so putting\n * both trees on the same commit and installing genuinely converges them. That cure is WRONG, and worse\n * than useless, when the branch bumped the pin ON PURPOSE: pulling would revert the deliverable, and\n * an install cannot move a pin in either tree. Printing the git cure first in that case is what sent a\n * real upgrade agent round the loop below.\n *\n * WHAT THIS BLOCK IS NOT ALLOWED TO SAY, in either branch: `git -C <the main tree> pull`. Two defects\n * rode on that one line, and it was printed ABOVE the escalation block, so it was the first thing read.\n * (a) A worktree-isolated SUBAGENT — the overwhelmingly common reader of this deny — CANNOT run\n * cross-tree git at all; the harness refuses it (shim-deny-reason.ts records the same\n * measurement). The one printed cure was the one thing the reader could not perform.\n * (b) A bare `git pull` acts on whatever branch that tree currently has checked out, and the primary\n * clone is normally sitting on a feature branch. It pulls the feature branch, the manifest never\n * moves, the pin never converges, and this guard fires again. The cure has to NAME main:\n * `cd <main> && git checkout main && git pull`.\n * So every step is prefixed `Tell main agent:` — INDIVIDUALLY, not under one shared header. That\n * repetition is deliberate and is the deliverable: a reader who skims exactly one of these lines must\n * still see it is not their own action. Do not factor it out.\n *\n * The FIX block still prints ABOVE the escalation block, on purpose. Moving it below would split the\n * numbered steps from the versions they refer to, and the one place caps are spent on ENDING the turn\n * (STOP WORKING NOW / RETRYING IS THE BUG) has to stay last and stay unique — a second STOP beat\n * competing with it is exactly the wall-of-text regression the L0 message diet exists to prevent.\n * Labelling carries the \"not yours to run\" fact instead, which is what the caps header does.\n */\n private fixLines(tree: EffectiveTree, quartet: VersionQuartet, skew: SkewCase): readonly string[] {\n if (skew === 'main-inconsistent') return this.mainInconsistentFix(tree, quartet);\n if (skew === 'main-ahead') return this.mainAheadFix(tree, quartet);\n if (skew === 'worktree-stale') {\n // Every PIN agrees, and so does main's install. The ONLY disagreeing leg is this tree's own\n // node_modules — so this is the one state where a bare `pnpm install` HERE is not a guess,\n // is not a downgrade, and needs nobody's permission. Saying so plainly matters because the\n // other cases all warn AGAINST reaching for it.\n return [\n ` FIX — THIS ONE IS YOURS, AND IT IS ONE COMMAND. Every pin already agrees; only this`,\n ` tree's own node_modules lags at ${this.show(quartet.worktree.installed).trim()}.`,\n ` 1. Run \\`pnpm install\\` HERE, in ${tree.root}.`,\n ` Nothing needs to move in the main tree and there is nobody to ask.`,\n ];\n }\n if (skew === 'bump') {\n return [\n ` THIS BRANCH BUMPED THE PIN ON PURPOSE (${this.show(quartet.main.pinned).trim()} → ${this.show(quartet.worktree.pinned).trim()}), so the usual cures do NOT apply:`,\n ` • \\`pnpm install\\` cannot help in EITHER tree — an install materializes a pin, never moves one.`,\n ` • \\`git pull\\` here would revert the bump, which is the whole deliverable.`,\n ` • Wiping this tree's node_modules does NOT help — the two PINS still disagree, and the`,\n ` L0 drift guard blocks in this guard's place.`,\n ` FIX — YOU CANNOT DO THIS FROM HERE. BOTH ways out need the MAIN tree, and cross-tree`,\n ` git is REFUSED to a subagent, so every step below is something you ASK FOR, not run:`,\n ` 1. Tell main agent: this task has to be redone in the MAIN tree ${tree.mainRoot}`,\n ` — a version bump cannot be done in a worktree at all.`,\n ` 2. Tell main agent: OR raise the MAIN tree's pin to ${this.show(quartet.worktree.pinned).trim()} in`,\n ` ${tree.mainRoot}/${WORKSPACE_MANIFEST} and run \\`pnpm install\\` there.`,\n ` 3. Tell main agent: to tell you when that is complete.`,\n ` THEN AND ONLY THEN will this worktree — and every other subagent — work again.`,\n ];\n }\n return [\n ` FIX — YOU CANNOT DO THIS FROM HERE. Cross-tree git is REFUSED to a subagent, and a bare`,\n ` \\`git pull\\` moves whatever branch that tree is on — so the cure must NAME main, in main:`,\n ` 1. Tell main agent: \\`cd ${tree.mainRoot} && git checkout main && git pull\\``,\n ` 2. Tell main agent: then run \\`pnpm install\\` in ${tree.mainRoot}`,\n ` 3. Tell main agent: then report back what \\`ls ${tree.mainRoot}/node_modules/@webpieces\\``,\n ` shows, so we know whether the hook shim needs re-upgrading too.`,\n ` 4. Tell main agent: to tell you when ALL of that is complete.`,\n ` THEN AND ONLY THEN will this worktree — and every other subagent — work again.`,\n ` The two structural escapes, if converging the trees is not what the coordinator wants:`,\n ` • Do the work in the MAIN tree instead — it is never blocked by this guard.`,\n ` • Or use a separate CLONE, not a worktree, if this tree genuinely needs a DIFFERENT`,\n ` version: a clone gets its own governance. A worktree MAY have its own node_modules, so`,\n ` installing here is fine; what it may not have is a DIFFERENT @webpieces version.`,\n ];\n }\n\n /**\n * CASE A — the MAIN tree disagrees with ITSELF: its `node_modules` is on one version while its own\n * `pnpm-workspace.yaml` pins another. Nothing in this worktree is wrong, and nothing this worktree\n * does can help.\n *\n * The cure is deliberately the SMALL one. The generic branch prints `git checkout main && git pull`\n * first, and that is over-prescribed here: both halves of the disagreement are already in that tree,\n * so an install materializes the pin it already has and the skew is gone. Printing the pull as well\n * invites a main agent to move main's commit for a fault that is not about main's commit at all.\n */\n private mainInconsistentFix(tree: EffectiveTree, quartet: VersionQuartet): readonly string[] {\n return [\n ` THE MAIN TREE IS INTERNALLY INCONSISTENT — its node_modules is on ${this.show(quartet.main.installed).trim()} but its own`,\n ` pin says ${this.show(quartet.main.pinned).trim()}. That is not this worktree's fault and not this worktree's to fix.`,\n ` FIX — YOU CANNOT DO THIS FROM HERE. Cross-tree git is REFUSED to a subagent:`,\n ` 1. Tell main agent: run \\`pnpm install\\` in ${tree.mainRoot} — no pull is needed, both`,\n ` halves are already in that tree.`,\n ` 2. Tell main agent: to tell you when that is complete.`,\n ` THEN AND ONLY THEN will this worktree — and every other subagent — work again.`,\n ];\n }\n\n /**\n * CASE B — the MAIN tree already RUNS a newer release than this tree's pin asks for. This is the\n * common case, and it is the one the old message got exactly backwards.\n *\n * It is the ONLY case a worktree fixes ITSELF. Nothing needs to move in the main tree — the version\n * the guards will judge this tree by is already installed there — so the entire fix is to raise this\n * tree's own pin to match, which is a one-line edit to a TRACKED file this tree owns. Printing an\n * escalation here (as every earlier revision did) tells an agent to stop and wait for a main agent\n * who has nothing to do, which is how a five-minute edit became a stalled turn.\n *\n * The edit is typable from inside the block because pnpm-workspace.yaml is on the L0 allowlist and\n * carved out in the runner's edit path. That carve-out and this text ship together on purpose: a\n * message prescribing a blocked call is the failure shape this repo has been burned by most often.\n *\n * ON A DETACHED HEAD the edit has no branch to belong to, so it is not offered — an edit that\n * survives nothing is worse than no edit. Get onto a branch, then read this message again.\n */\n private mainAheadFix(tree: EffectiveTree, quartet: VersionQuartet): readonly string[] {\n const target = this.show(quartet.main.installed).trim();\n if (this.isDetachedHead(tree.root)) {\n return [\n ` FIX — GET ONTO A BRANCH FIRST. \\`git branch --show-current\\` is empty in ${tree.root}, so`,\n ` HEAD is DETACHED and the one-line pin edit below would belong to no branch at all.`,\n ` 1. Check out a branch here, then re-run this command.`,\n ` 2. The fix is then yours alone: set ${WORKSPACE_MANIFEST}'s catalog pin to ${target}.`,\n ` Nothing needs to move in the main tree — it already runs ${target}.`,\n ];\n }\n const install = quartet.worktree.installed !== null\n ? [` 2. Then run \\`pnpm install\\` HERE — this tree has its own node_modules, so it must be`,\n ` materialized at ${target} too.`]\n : [` 2. Nothing else. This tree has no node_modules of its own, so there is nothing to install.`];\n return [\n ` FIX — THIS ONE IS YOURS, AND YOU CAN DO IT RIGHT HERE. The main tree is AHEAD: it already`,\n ` runs ${target}, so nothing has to move there and there is nobody to ask.`,\n ` 1. Edit ${tree.root}/${WORKSPACE_MANIFEST} — the catalog line for`,\n ` ${UMBRELLA_PACKAGE} — and set it to ${target}. That edit is ALLOWED`,\n ` right now, from inside this block.`,\n ...install,\n ` Do NOT run a bare \\`pnpm install\\` first: this tree's pin is the STALE side, so installing`,\n ` before the edit materializes the OLD release and this guard fires again.`,\n ];\n }\n\n /**\n * Is HEAD DETACHED in this tree — i.e. is there no branch for a pin edit to belong to?\n *\n * `branch --show-current`, NOT `rev-parse --abbrev-ref HEAD`: it answers on an UNBORN branch (which\n * every freshly-created worktree is until its first commit, and where `rev-parse` fatals) and prints\n * EMPTY on a detached HEAD, which is exactly the distinction case B needs.\n *\n * A git FAILURE is NOT detached, and the difference is load-bearing: `--show-current` prints nothing\n * in both situations, so keying off the output alone would tell anyone whose tree git cannot read\n * (no repo, a broken index, git absent) that their HEAD is detached — a confident diagnosis of a\n * state nobody measured. Only an exit-0-with-empty-output is detached; anything else falls back to\n * the ordinary branch wording, whose worst case is prescribing an edit that turns out to be moot.\n */\n private isDetachedHead(root: string): boolean {\n const result = spawnSync('git', ['-C', root, 'branch', '--show-current'], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() === '';\n }\n\n /**\n * THE SUBAGENT CANNOT REACH THE MAIN TREE, so the message it is handed has to be the message it\n * FORWARDS. This used to be one sentence — \"report to your coordinator that one of you must move to\n * the other's version\" — with no command, no direction and nothing pasteable, and the result was a\n * subagent that correctly diagnosed the block, correctly escalated, and handed its coordinator a\n * request too vague to act on. Worse, the obvious guess (\"ask the coordinator to run `pnpm install`\n * in main\") is a NO-OP on a bump: it reinstalls main's own pin and nothing moves.\n *\n * So the escalation is rendered as literal text to forward, with the versions and the direction\n * already filled in. A human cannot sit with every agent; the deny has to carry the whole ask.\n *\n * The ask is ROUTED THROUGH THE MAIN AGENT rather than phrased as a command, and that distinction is\n * the whole point of this block. `git -C <mainRoot> pull` reads like something you run from wherever\n * you are standing — so a subagent reads it, tries it, and only then discovers that CROSS-TREE GIT IS\n * REFUSED to a worktree-isolated agent (same measurement shim-deny-reason.ts records: the harness\n * blocks `git -C <other tree>`, and it is git specifically). This skew needs a git pull in main, so\n * the one printed cure was the one thing this session cannot perform. Be precise about that and do\n * NOT overstate it into \"you cannot reach that tree at all\" — a local `cd <main> && pnpm install`\n * measurably DOES run, it simply cannot move main onto a different commit, which is what a skew\n * requires. The actor who can is the MAIN AGENT running in the MAIN git worktree, so the forwarded\n * text asks for exactly that — and asks to be TOLD WHEN IT IS DONE, because \"the work happened\" is\n * the event that unblocks this subagent and it has no way to observe it otherwise.\n *\n * And it has to say STOP, in caps, because forwarding is only half of what the subagent must do. One\n * measured subagent transcript re-fired this identical deny 13 TIMES (25 across the whole session,\n * counting two sibling subagents and two parent sessions): the subagent read it, escalated exactly\n * as asked — and then kept making tool calls, because nothing here said that forwarding ENDS the\n * turn. Every retry cost a round trip and pushed the one message that mattered further up the\n * scrollback. The block is not transient and no command from this tree slips past it, so retrying is\n * never a strategy; it is the bug. The caps are spent ONLY on that beat (STOP WORKING NOW / NO\n * further tool calls / RETRYING IS THE BUG / WAIT) — shouting the whole report would just restore\n * the wall of text the L0 message diet exists to prevent.\n */\n private escalationLines(tree: EffectiveTree, quartet: VersionQuartet, skew: SkewCase): readonly string[] {\n return [\n ` SUBAGENT? You cannot fix the main tree from here. Forward this to your coordinator verbatim:`,\n ` > My worktree ${tree.root} is on @webpieces ${this.show(quartet.worktree.pinned).trim()};`,\n ` > the main tree ${tree.mainRoot} is on ${this.show(quartet.main.pinned).trim()}.`,\n ...this.askLines(tree, quartet, skew),\n ` THEN STOP WORKING NOW. Forwarding that message IS the end of your turn: make NO further tool`,\n ` calls and do NOT retry this one — RETRYING IS THE BUG. Every retry re-fires this identical`,\n ` deny and buries the ask above; one subagent did it 13 times, 25 across that whole session.`,\n ` Nothing you can do from this tree clears it. WAIT for the main agent to confirm it is done,`,\n ` then resume — nothing between.`,\n ];\n }\n\n /**\n * The forwardable ASK, one per case that needs one. `main-ahead` never reaches here — it has nothing\n * to ask for — so the three shapes below cover every escalating case.\n *\n * The bump ask carries the upgrade sentence VERBATIM as Dean wrote it, because it answers the one\n * question an upgrade agent gets wrong: \"webpieces cannot be upgraded from a worktree\" is not a\n * missing permission it can route around, it is a property of who governs whom. Its `STOP` is about\n * the ROLE (\"you are the wrong agent for this task\"); the caps beat at the end of the block is about\n * the TURN (\"forwarding ends it\"). Two different instructions, and the closer stays last and unique.\n */\n private askLines(tree: EffectiveTree, quartet: VersionQuartet, skew: SkewCase): readonly string[] {\n if (skew === 'bump') {\n return [\n ` > A \\`pnpm install\\` in main will NOT fix this — main's PIN has to move. Pick one:`,\n ` > (a) I redo this task in the MAIN tree (a version bump cannot be done in a worktree), or`,\n ` > (b) you TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to`,\n ` > raise main's catalog pin to ${this.show(quartet.worktree.pinned).trim()} and \\`pnpm install\\` there, and to tell me when it is complete`,\n ` > so I can continue here. I cannot reach that tree from here.`,\n ` If you are a subagent upgrading webpieces, STOP — only main agents in worktrees can do`,\n ` this. Otherwise your main agent must \\`git pull\\` main: main has an earlier version of`,\n ` webpieces pinned and must upgrade. You MUST tell the main agent to pull main and`,\n ` \\`pnpm install\\`.`,\n ];\n }\n if (skew === 'main-inconsistent') {\n return [\n ` > Please TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to run`,\n ` > \\`pnpm install\\` there — its node_modules is on ${this.show(quartet.main.installed).trim()} but its own pin says`,\n ` > ${this.show(quartet.main.pinned).trim()}, so no pull is needed — and to tell me when it is complete so I can`,\n ` > continue working. I cannot reach that tree from here.`,\n ];\n }\n return [\n ` > Please TELL THE MAIN AGENT in the MAIN git worktree ${tree.mainRoot} to run`,\n ` > \\`git checkout main && git pull && pnpm install\\` there — it must NAME main, since a bare`,\n ` > pull moves whatever branch that tree is on — so both trees are on the same release, and`,\n ` > to tell me when it is complete so I can continue working, and what`,\n ` > \\`ls node_modules/@webpieces\\` shows there. I cannot reach that tree from here.`,\n ];\n }\n\n /**\n * Did THIS BRANCH change the pin, as opposed to the two trees having drifted onto different commits?\n *\n * Only answerable now that both pin legs actually resolve — before the catalog reader followed YAML\n * anchors they both read null on the repos that pin via an anchor, so every skew looked alike and the\n * report could only ever print the one generic cure.\n *\n * Two git spawns worst case, on the BLOCK path only (this is never reached on an allow), and\n * best-effort: a git failure answers \"not a deliberate bump\", which falls back to the generic cure\n * that was the only text this report had before.\n */\n private isDeliberateBump(tree: EffectiveTree, quartet: VersionQuartet): boolean {\n if (quartet.main.pinned === null || quartet.worktree.pinned === null) return false;\n if (quartet.main.pinned === quartet.worktree.pinned) return false;\n return this.touchesWorkspaceFile(tree.root, ['status', '--porcelain', '--', WORKSPACE_MANIFEST])\n || this.touchesWorkspaceFile(tree.root, ['diff', '--name-only', 'origin/main...HEAD', '--', WORKSPACE_MANIFEST]);\n }\n\n private touchesWorkspaceFile(root: string, args: readonly string[]): boolean {\n const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() !== '';\n }\n\n // Every version WITH the file it came from. An agent that is told \"they disagree\" without being told\n // WHICH FILE to edit re-derives it by grepping, which is exactly the turn-burning this guard exists\n // to prevent. Unreadable legs are printed as `-` rather than omitted, so the reader can tell\n // \"this one is absent\" from \"I forgot to look\".\n private versionLines(tree: EffectiveTree, quartet: VersionQuartet): readonly string[] {\n const lines = [\n ` main pin ${this.show(quartet.main.pinned)} ${tree.mainRoot}/pnpm-workspace.yaml`,\n ` main installed ${this.show(quartet.main.installed)} ${tree.mainRoot}/node_modules/${UMBRELLA_PACKAGE}`,\n ` ^ the binary judging this very call`,\n ` this worktree ${this.show(quartet.worktree.pinned)} ${tree.root}/pnpm-workspace.yaml`,\n ];\n if (quartet.worktree.installed !== null) {\n lines.push(` its installed ${this.show(quartet.worktree.installed)} ${tree.root}/node_modules/${UMBRELLA_PACKAGE}`);\n lines.push(' ^ what nx, vitest and eslint load IN this tree');\n }\n const others = this.versions.otherWorktrees(tree.mainRoot, tree.root);\n if (others.length > 0) {\n lines.push(` NOTE ${others.length} other worktree(s) exist and are governed the same way — if they are`);\n lines.push(' skewed too, their agents are already mis-governed. Consider clones, or');\n lines.push(' serializing the work in the main tree.');\n }\n return lines;\n }\n\n private show(version: string | null): string {\n return (version ?? '-').padEnd(10);\n }\n}\n"]}