import { EffectiveTree } from './effective-tree'; import { VersionQuartet } from './webpieces-versions'; export declare class VersionSyncGuard { private readonly inspection; private readonly versions; /** * True when this tree is a linked worktree whose webpieces version disagrees with the main tree's. * This is the `V` dimension of the L1 matrix; the runner asks it for EVERY Bash call so the answer * lands in the audit log even when nothing blocks. */ skewed(tree: EffectiveTree): boolean; /** The deny report, or null to allow. */ block(command: string, tree: EffectiveTree): string | null; /** * Commands that must pass EVEN WHILE THIS GUARD IS BLOCKING, because they are how you get unblocked * — or how you look at the tree first. * * `ReadOnlyInspectionScan` deliberately excludes git and gh OUTRIGHT ("the guards exist to police * git, and read-only git is not a line worth drawing while flying blind"), which is right for the * guards that police git but WRONG here: this guard's own prescribed cure is `git pull` in both * trees. Without this carve-out the guard would deny the exact command it tells the reader to run — * the single failure shape this repo has been burned by most often, and the reason the deny text is * allowed to promise "STILL ALLOWED HERE: ... pnpm install, git pull/fetch". * * Deliberately NARROW: fetching, pulling and installing cannot make a skew worse, and every one of * them moves the tree toward agreement. Anything that BUILDS, TESTS or COMMITS is still blocked, * because those are the operations that would be judged by the wrong release. */ private isCureOrLook; /** * Is there a cross-tree comparison to make at all? TWO cheap conditions, no file read behind either: * * • K is `worktree` — git's `--git-dir ≠ --git-common-dir`, so a repo with no linked worktrees can * never reach the manifests. (In the primary clone this is also structural escape #1: "do the * work in the main tree" needs no allowlist entry to keep working.) * • the two roots are DIFFERENT directories — a tree compared with itself is not a skew, it is the * single-tree pin-vs-install question the L0 drift guard already owns. */ private applies; /** Public so the runner can log all four versions on ALLOW as well as on BLOCK (audit, not just deny). */ quartetFor(tree: EffectiveTree): VersionQuartet; private report; /** * WHICH of the five states this is — asked ONCE, so the FIX block and the escalation block can never * describe two different diagnoses of the same skew. * * Order is not arbitrary. `main-inconsistent` is asked FIRST because it is a fault in the governing * tree itself: comparing this worktree against a tree that disagrees with itself picks a direction * out of two numbers that are not yet one answer. `bump` next, because a deliberate raise is the one * shape where the direction is real but every ordinary cure is harmful. */ private classify; /** * SEMVER ORDER of two versions: 1 when `a` is newer, -1 when older, 0 when equal OR undecidable. * * 0 is the FAIL-SAFE answer and every caller must read it as "no opinion": an unreadable leg, a * dist-tag, two different pre-releases, or build metadata (which carries no precedence) all land * there, and `classify` then falls to `main-behind`, the branch that ASKS rather than acts. Guessing * a direction is how a downgrade gets prescribed, and this repo has already paid for that once. * * The same rules the shim's awk compare uses, so L0 and L1 cannot order one pair two ways: build * metadata stripped, numeric cores compared component-wise, and a pre-release sorting BELOW its * release. */ private compare; /** One version split into its numeric core and its pre-release suffix, or null if it is not numeric. */ private parts; /** * The cure list, which is NOT the same list in both directions — but which, in BOTH directions, is a * list of things the reader ASKS FOR rather than runs. * * The ordinary skew is two trees sitting on different commits of main. The pin is tracked, so putting * both trees on the same commit and installing genuinely converges them. That cure is WRONG, and worse * than useless, when the branch bumped the pin ON PURPOSE: pulling would revert the deliverable, and * an install cannot move a pin in either tree. Printing the git cure first in that case is what sent a * real upgrade agent round the loop below. * * WHAT THIS BLOCK IS NOT ALLOWED TO SAY, in either branch: `git -C pull`. Two defects * rode on that one line, and it was printed ABOVE the escalation block, so it was the first thing read. * (a) A worktree-isolated SUBAGENT — the overwhelmingly common reader of this deny — CANNOT run * cross-tree git at all; the harness refuses it (shim-deny-reason.ts records the same * measurement). The one printed cure was the one thing the reader could not perform. * (b) A bare `git pull` acts on whatever branch that tree currently has checked out, and the primary * clone is normally sitting on a feature branch. It pulls the feature branch, the manifest never * moves, the pin never converges, and this guard fires again. The cure has to NAME main: * `cd
&& git checkout main && git pull`. * So every step is prefixed `Tell main agent:` — INDIVIDUALLY, not under one shared header. That * repetition is deliberate and is the deliverable: a reader who skims exactly one of these lines must * still see it is not their own action. Do not factor it out. * * The FIX block still prints ABOVE the escalation block, on purpose. Moving it below would split the * numbered steps from the versions they refer to, and the one place caps are spent on ENDING the turn * (STOP WORKING NOW / RETRYING IS THE BUG) has to stay last and stay unique — a second STOP beat * competing with it is exactly the wall-of-text regression the L0 message diet exists to prevent. * Labelling carries the "not yours to run" fact instead, which is what the caps header does. */ private fixLines; /** * CASE A — the MAIN tree disagrees with ITSELF: its `node_modules` is on one version while its own * `pnpm-workspace.yaml` pins another. Nothing in this worktree is wrong, and nothing this worktree * does can help. * * The cure is deliberately the SMALL one. The generic branch prints `git checkout main && git pull` * first, and that is over-prescribed here: both halves of the disagreement are already in that tree, * so an install materializes the pin it already has and the skew is gone. Printing the pull as well * invites a main agent to move main's commit for a fault that is not about main's commit at all. */ private mainInconsistentFix; /** * CASE B — the MAIN tree already RUNS a newer release than this tree's pin asks for. This is the * common case, and it is the one the old message got exactly backwards. * * It is the ONLY case a worktree fixes ITSELF. Nothing needs to move in the main tree — the version * the guards will judge this tree by is already installed there — so the entire fix is to raise this * tree's own pin to match, which is a one-line edit to a TRACKED file this tree owns. Printing an * escalation here (as every earlier revision did) tells an agent to stop and wait for a main agent * who has nothing to do, which is how a five-minute edit became a stalled turn. * * The edit is typable from inside the block because pnpm-workspace.yaml is on the L0 allowlist and * carved out in the runner's edit path. That carve-out and this text ship together on purpose: a * message prescribing a blocked call is the failure shape this repo has been burned by most often. * * ON A DETACHED HEAD the edit has no branch to belong to, so it is not offered — an edit that * survives nothing is worse than no edit. Get onto a branch, then read this message again. */ private mainAheadFix; /** * Is HEAD DETACHED in this tree — i.e. is there no branch for a pin edit to belong to? * * `branch --show-current`, NOT `rev-parse --abbrev-ref HEAD`: it answers on an UNBORN branch (which * every freshly-created worktree is until its first commit, and where `rev-parse` fatals) and prints * EMPTY on a detached HEAD, which is exactly the distinction case B needs. * * A git FAILURE is NOT detached, and the difference is load-bearing: `--show-current` prints nothing * in both situations, so keying off the output alone would tell anyone whose tree git cannot read * (no repo, a broken index, git absent) that their HEAD is detached — a confident diagnosis of a * state nobody measured. Only an exit-0-with-empty-output is detached; anything else falls back to * the ordinary branch wording, whose worst case is prescribing an edit that turns out to be moot. */ private isDetachedHead; /** * THE SUBAGENT CANNOT REACH THE MAIN TREE, so the message it is handed has to be the message it * FORWARDS. This used to be one sentence — "report to your coordinator that one of you must move to * the other's version" — with no command, no direction and nothing pasteable, and the result was a * subagent that correctly diagnosed the block, correctly escalated, and handed its coordinator a * request too vague to act on. Worse, the obvious guess ("ask the coordinator to run `pnpm install` * in main") is a NO-OP on a bump: it reinstalls main's own pin and nothing moves. * * So the escalation is rendered as literal text to forward, with the versions and the direction * already filled in. A human cannot sit with every agent; the deny has to carry the whole ask. * * The ask is ROUTED THROUGH THE MAIN AGENT rather than phrased as a command, and that distinction is * the whole point of this block. `git -C pull` reads like something you run from wherever * you are standing — so a subagent reads it, tries it, and only then discovers that CROSS-TREE GIT IS * REFUSED to a worktree-isolated agent (same measurement shim-deny-reason.ts records: the harness * blocks `git -C `, and it is git specifically). This skew needs a git pull in main, so * the one printed cure was the one thing this session cannot perform. Be precise about that and do * NOT overstate it into "you cannot reach that tree at all" — a local `cd
&& pnpm install` * measurably DOES run, it simply cannot move main onto a different commit, which is what a skew * requires. The actor who can is the MAIN AGENT running in the MAIN git worktree, so the forwarded * text asks for exactly that — and asks to be TOLD WHEN IT IS DONE, because "the work happened" is * the event that unblocks this subagent and it has no way to observe it otherwise. * * And it has to say STOP, in caps, because forwarding is only half of what the subagent must do. One * measured subagent transcript re-fired this identical deny 13 TIMES (25 across the whole session, * counting two sibling subagents and two parent sessions): the subagent read it, escalated exactly * as asked — and then kept making tool calls, because nothing here said that forwarding ENDS the * turn. Every retry cost a round trip and pushed the one message that mattered further up the * scrollback. The block is not transient and no command from this tree slips past it, so retrying is * never a strategy; it is the bug. The caps are spent ONLY on that beat (STOP WORKING NOW / NO * further tool calls / RETRYING IS THE BUG / WAIT) — shouting the whole report would just restore * the wall of text the L0 message diet exists to prevent. */ private escalationLines; /** * The forwardable ASK, one per case that needs one. `main-ahead` never reaches here — it has nothing * to ask for — so the three shapes below cover every escalating case. * * The bump ask carries the upgrade sentence VERBATIM as Dean wrote it, because it answers the one * question an upgrade agent gets wrong: "webpieces cannot be upgraded from a worktree" is not a * missing permission it can route around, it is a property of who governs whom. Its `STOP` is about * the ROLE ("you are the wrong agent for this task"); the caps beat at the end of the block is about * the TURN ("forwarding ends it"). Two different instructions, and the closer stays last and unique. */ private askLines; /** * Did THIS BRANCH change the pin, as opposed to the two trees having drifted onto different commits? * * Only answerable now that both pin legs actually resolve — before the catalog reader followed YAML * anchors they both read null on the repos that pin via an anchor, so every skew looked alike and the * report could only ever print the one generic cure. * * Two git spawns worst case, on the BLOCK path only (this is never reached on an allow), and * best-effort: a git failure answers "not a deliberate bump", which falls back to the generic cure * that was the only text this report had before. */ private isDeliberateBump; private touchesWorkspaceFile; private versionLines; private show; }