@webpieces/ai-hook-rules 0.4.667 → 0.4.669
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/bin/l0-allowlist.d.ts +7 -1
- package/src/bin/l0-allowlist.js +26 -6
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/shim-drift-fix.js +15 -1
- package/src/bin/shim-drift-fix.js.map +1 -1
- package/src/core/l0-matrix.js +6 -0
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l1-rows.js +1 -1
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/l2-doc.js +1 -1
- package/src/core/l2-doc.js.map +1 -1
- package/src/core/l2-rows.js +2 -2
- package/src/core/l2-rows.js.map +1 -1
- package/src/core/rules/branch-switch-scan.d.ts +5 -0
- package/src/core/rules/branch-switch-scan.js +5 -0
- package/src/core/rules/branch-switch-scan.js.map +1 -1
- package/src/core/rules/catch-error-pattern.d.ts +25 -0
- package/src/core/rules/catch-error-pattern.js +54 -16
- package/src/core/rules/catch-error-pattern.js.map +1 -1
- package/src/core/rules/merged-branch-bash-guard.js +1 -1
- package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
- package/src/core/rules/merged-branch-message.js +4 -3
- package/src/core/rules/merged-branch-message.js.map +1 -1
- package/src/core/rules/redirect-how-to-merge-main.js +9 -3
- package/src/core/rules/redirect-how-to-merge-main.js.map +1 -1
- package/src/core/rules/stale-main-bash-guard.d.ts +5 -2
- package/src/core/rules/stale-main-bash-guard.js +15 -10
- package/src/core/rules/stale-main-bash-guard.js.map +1 -1
- package/src/core/rules/tree-recovery.d.ts +26 -5
- package/src/core/rules/tree-recovery.js +33 -12
- package/src/core/rules/tree-recovery.js.map +1 -1
- package/src/core/runner.js +2 -0
- package/src/core/runner.js.map +1 -1
- package/src/core/version-sync.d.ts +28 -7
- package/src/core/version-sync.js +56 -22
- package/src/core/version-sync.js.map +1 -1
|
@@ -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.\n *\n * The ordinary skew is two trees sitting on different commits of main, and there `git pull` both +\n * `pnpm install` genuinely converges them — the pin is tracked, so the same hash gives the same\n * version. That cure is WRONG, and worse than useless, when the branch bumped the pin ON PURPOSE:\n * pulling would revert the deliverable, and an install cannot move a pin in either tree. Printing\n * the git cure first in that case is what sent a real upgrade agent round the loop below.\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 ` Two ways out, and BOTH need the main tree:`,\n ` 1. Redo this task in the MAIN tree — a version bump cannot be done in a worktree at all.`,\n ` 2. Or raise the MAIN tree's pin to ${this.show(quartet.worktree.pinned).trim()} and \\`pnpm install\\` there, then continue here.`,\n ];\n }\n return [\n ` FIX (usually just git — the pin is TRACKED, so the same commit gives the same version):`,\n ` 1. \\`git -C ${tree.mainRoot} pull\\` and \\`git -C ${tree.root} pull\\` onto the same main,`,\n ` then \\`pnpm install\\` in each tree that has a node_modules. A worktree MAY have its`,\n ` own; what it may not have is a DIFFERENT @webpieces version from the main tree.`,\n ` 2. Or work in the MAIN tree instead — it is never blocked by this guard.`,\n ` 3. Or, if this tree genuinely needs a DIFFERENT version, use a separate CLONE, not a`,\n ` worktree: a clone gets its own governance. (This is the answer to \"I need a different`,\n ` version\", never to \"I need to install here\" — installing here is fine.)`,\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 transcript re-fired this identical deny 35 TIMES: 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 pull && pnpm install\\` there, so both trees are on the same release, and to tell me`,\n ` > when it is complete so I can continue working. 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 transcript did it 35 times. Nothing you can do from this`,\n ` tree clears it. WAIT for the main agent to confirm it is done, 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 { 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"]}
|