@webpieces/ai-hook-rules 0.4.737 → 0.4.739

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"l2-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l2-rows.ts"],"names":[],"mappings":"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","sourcesContent":["// ---------------------------------------------------------------------------\n// L2 — the BRANCH-STATE layer, as data.\n//\n// L2 answers one question: *may I work here, and is what I read current?* Drawn as a decision matrix\n// that is TWELVE ordered rows plus one terminal fail-open row, first match wins.\n//\n// This module holds those rows, l2-doc.ts renders them into guards/L2-branch-state.md, and a unit test\n// (l2-matrix.spec.ts) locks that file byte-identical to the renderer — the same mechanism that already\n// makes L0's fault table and L1's location table undriftable. Before this existed the L2 doc was 100%\n// hand-written and said so: *\"Until that lands this text is hand-written and can drift.\"*\n//\n// ## HOW L2 JOINS TO THE ROWS TODAY — read this before assuming it works like L1\n//\n// L1 DISPATCHES from its array: `runner.l1LocationBlock` takes the first matching row and switches on\n// its `blockId`, so deleting a row deletes a block. L2 does NOT, and pretending otherwise would be the\n// drift this table exists to remove. The four L2 guard classes each own their own ladder, and those\n// ladders diverge on purpose (guards/L2-branch-state.md, \"Deliberate divergence\": the two Bash guards\n// differ in polarity, quantifier and empty-command handling all at once, so no single parameterised\n// function serves both). Unifying them is where a wrong edit turns an allow into a session-wedging\n// block, so it is deliberately not attempted here.\n//\n// What L2 does instead is a REASON→ROW join. Every exit of every L2 guard already carries a stable\n// reason string into the decision log; `L2_ROW_FOR_REASON` maps each of those to the row it is an\n// instance of, the guards stamp that number as `row=`, and l2-matrix.spec.ts asserts the map is\n// EXHAUSTIVE against the guard sources — a new reason with no row fails the build. So `row=8` in\n// `.webpieces/logs/L2-decisions` opens guards/L2-branch-state.md at row 8 and reads the state, the cure\n// and the tools that row covers, exactly as `row=5` already does for L1.\n//\n// The rows the guards cannot yet honour are named in the doc's \"Not done\" section rather than quietly\n// rendered as if they were live. That section is generated from NOT_DONE below, so it cannot rot.\n//\n// This module is deliberately import-free at runtime, so `pnpm guards:generate` can load it without the\n// package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/** Which tools a row covers. `B` Bash · `R` Read · `E` Write/Edit. */\nexport type L2Tool = 'B' | 'R' | 'E';\n\n/** What L2 does with a row — the same action codebook every layer reports in (GUARD_MATRIX.md). */\nexport type L2ActionKind = 'allow' | 'exempt' | 'block' | 'fail-open';\n\n/**\n * The TERMINAL fail-open row, and the one number in this table that is not from the 1-10 design.\n *\n * Everything in rows 6-10 needs the main-sync cache, and the cache is written by a fire-and-forget\n * refresher that populates it for the NEXT call — so the first tool call of every session has none.\n * \"Stop here and ALLOW\" was written as a DIVIDER in the design table, i.e. as prose between two blocks\n * of rows. Prose cannot be stamped into a log line, and this is the single most frequently taken exit\n * in the whole layer (every session's first call, every unreadable branch, every unreachable forge), so\n * it is a row with a number like any other.\n *\n * It is 11 rather than 6-with-a-renumber because row numbers are IDENTITY here: they are printed in the\n * doc and logged as `row=`, so shifting 6-10 down would silently re-point every reference. The doc\n * prints it in its true position, between rows 5 and 6, with its number shown — same treatment L1 gives\n * row 8, which is printed third and numbered 8.\n */\nexport const L2_FAIL_OPEN_ROW = 11;\n\n/** The `act` cell: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L2Action {\n constructor(readonly label: string, readonly kind: L2ActionKind) {}\n}\n\nexport const L2_ALLOW = new L2Action('1 allow', 'allow');\nexport const L2_EXEMPT = new L2Action('2 exempt', 'exempt');\nexport const L2_BLOCK = new L2Action('4 block', 'block');\nexport const L2_FAIL_OPEN = new L2Action('1 allow (fail-open)', 'fail-open');\n\n/**\n * One row of the \"L2 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * THE POINT OF THIS CLASS is that a use case is added HERE, in code, beside the row it exercises — not\n * into a hand-written doc section that drifts. When a new situation comes up in a session, it becomes\n * one more `new L2UseCase(...)` on the row that judged it, `pnpm guards:generate` re-renders the doc,\n * and the byte-lock spec fails if anyone edits the rendered table instead.\n *\n * The four text fields are rendered VERBATIM. `reason` is the ENFORCEMENT half and is never rendered:\n * it is the exact `reason` string the guard logs for this case, so a spec can push it back through\n * `l2RowForReason` and assert it lands on the row this use case is filed under. That closes the loop\n * the L2 decision log opens — `row=` in the trail, this table on the page, one join between them.\n *\n * `reason` is REQUIRED, and a case that exercises something which is not an L2 row exit says so with\n * `NO_ROW_EXIT` rather than by omitting the argument. An optional field would make opting OUT of the\n * only real enforcement here the shortest thing to type and impossible to grep — the widening-by-absence\n * shape `.claude/rules/no-backwards-compat.md` rejects. `grep NO_ROW_EXIT` now lists every unenforced case.\n */\nexport class L2UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the reason behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly reason: string,\n ) {}\n}\n\n/**\n * The `reason` for a use case that is NOT an L2 row exit, and so has nothing to join back to.\n *\n * The only legitimate case today is row 3: merge-in-progress is L4's state, and L2 exempts it without\n * logging a reason of its own. Named rather than absent, so \"this case is not enforced\" is a value in\n * the table you can grep for instead of a missing argument nobody notices.\n */\nexport const NO_ROW_EXIT = 'NO_ROW_EXIT (not an L2 row exit — another layer owns this state)';\n\n/**\n * One row of L2's decision table.\n *\n * `cure` is rendered verbatim into the doc and is LITERAL by policy: L0's cure-reachability discipline\n * says a message pointing at documentation for its own remedy cannot be tested, and it caught a fault\n * prescribing a bin that had been renamed away. `—` is the only legal non-command cure, and only on a\n * row that allows.\n */\nexport class L2Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- the five cells of one doc row plus its use cases\n constructor(\n readonly num: number,\n readonly tools: readonly L2Tool[],\n /** The `state` cell, verbatim. */\n readonly state: string,\n readonly action: L2Action,\n /** The `cure` cell, verbatim. `—` when the row allows. */\n readonly cure: string,\n /**\n * The observed situations this row judges. Rendered as the \"L2 use cases\" table.\n *\n * A NON-EMPTY tuple, and required: a row nobody has ever seen fire is either dead or\n * undocumented, and both are worth knowing. Expressing that in the TYPE rather than as a\n * runtime assertion is the JwtRoles pattern — the invariant is enforced at the moment the row\n * is written, which is the only moment that changes what somebody types.\n */\n readonly useCases: readonly [L2UseCase, ...L2UseCase[]],\n ) {}\n\n /** `B R E`, the doc's own spelling of the tool cell. */\n toolCell(): string {\n return this.tools.join(' ');\n }\n}\n\n/**\n * THE THIRTEEN L2 ROWS, in first-match-wins order.\n *\n * Rows 1-5 need NO cache and fire on call #1: rows 1, 2 and 4 are text matches, row 3 is a marker-file\n * scan, row 5 is one `git rev-parse`. Row 11 is the cache divider. Rows 6-10 all read the cache.\n *\n * THE ORDER OF ROW 5 IS THE MOST LOAD-BEARING THING IN THIS TABLE. Put \"on main\" BELOW the divider and\n * WRITES on `main` are permitted for the whole first call of every session — and permanently in a\n * multi-worktree repo, where another tree may hold the cache lock indefinitely.\n *\n * `B` AND `E` PART COMPANY ON `main`, and rows 5/6/7 are where. A WRITE on `main` is wrong at any\n * freshness — the work lands somewhere unreviewable and unrevertable — so row 5 is `E` only, judged on\n * the branch alone, above the divider. A READ or a BUILD on a CURRENT `main` is harmless, and blocking\n * it strands the agent right after `pnpm wp-sync-main` put it there; so `B` joins `R` on the\n * FRESHNESS-gated pair below the divider (row 6 behind → block, row 7 current → allow), where \"cannot\n * tell\" fails open at row 11 by construction. `B` and `R` still differ in SHAPE inside row 6: a Read\n * names one file and is judged precisely, a Bash command is opaque and gets default-deny plus row 4.\n */\nexport const L2_ROWS: readonly L2Row[] = [\n new L2Row(1, ['B', 'R', 'E'], 'on the **global allowlist** (inert command, or a universal cure such as reading/editing `webpieces.config.json`)', L2_ALLOW, '—', [\n new L2UseCase(1,\n 'You are blocked by some other L2 row, and need to turn the policy off to get anything done',\n 'any state — this row is ahead of every block',\n 'ALLOW: reading and editing `webpieces.config.json` is never blocked, so the mode-OFF cure is always reachable',\n 'Edit `webpieces.config.json` → `hookGuards` → `branch-state-guard` → `\"mode\": \"OFF\"`',\n 'webpieces-config-read (escape hatch)'),\n new L2UseCase(2,\n 'A Write to `webpieces.config.json` while on `main`, which row 5 would otherwise block',\n 'on `main`, editing the one file that can disable the guard',\n 'ALLOW: the hook adapter bypasses feature-branch-guard for this path before any guard runs',\n 'None needed — the edit proceeds',\n 'config-bypass (feature-branch-guard skipped)'),\n ]),\n new L2Row(2, ['B'], 'bare `git checkout main`, with no `git pull` chained into the same command', L2_BLOCK, '`pnpm wp-sync-main`', [\n new L2UseCase(3,\n '`git checkout main` after a merge, to start the next piece of work',\n 'about to land on whatever local `main` you last had — 157 commits behind, in the incident',\n 'BLOCK: decided from command TEXT alone, before the checkout, because the only `main` this could measure is the one it is about to leave',\n '`pnpm wp-sync-main` — checkout, pull, reap dead branches/worktrees, sweep orphan directories, in one command (hand-rolled, the pull must be in the SAME command as the checkout)',\n 'bare checkout of main ('),\n new L2UseCase(4,\n 'The same command inside a linked worktree, where `git checkout main` fatals anyway',\n 'linked worktree — `main` is already checked out in the primary clone',\n 'BLOCK, and the message prints the worktree form rather than a cure git would refuse',\n '`git fetch origin main`, then work off `origin/main`',\n 'bare checkout of main ('),\n ]),\n new L2Row(3, ['B', 'R', 'E'], '**merge in progress** — L4 owns this state', L2_EXEMPT, 'finish the merge: `pnpm wp-finish-upsert-pr`', [\n new L2UseCase(5,\n 'Reading and editing conflicted files during a 3-point merge, on a branch row 9 would block',\n 'merge markers on disk — `pnpm wp-start-update` has run and not finished',\n 'EXEMPT: everything is permitted, which is exactly what lets row 9 be strict',\n 'Resolve the conflicts, then `pnpm wp-finish-upsert-pr`',\n NO_ROW_EXIT),\n ]),\n new L2Row(4, ['B'], 'on the **skip list** — it gets you OUT, or tells you where you are', L2_ALLOW, '—', [\n new L2UseCase(6,\n '`git status` / `gh pr view` while blocked, to work out where you are',\n 'any state — orientation is never \"working here\"',\n 'ALLOW: metadata tells you where you are without putting stale file CONTENT in context',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(7,\n '`git stash` when `git checkout -b <new> origin/main` refuses because `origin/main` touched the same files you edited',\n 'on a stale `main` or a merged branch, dirty tree, with an overlapping upstream change',\n 'ALLOW: the cure for the row that blocked you must itself never be blocked — and this is the residual step that makes rows 6 and 8 safe to block on a dirty tree',\n 'None needed — then re-run the checkout and `git stash pop`',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(28,\n '`gh pr close 123`, `gh pr comment`, `gh api …` or a `curl` while parked on a stale `main` or a merged branch',\n 'blocked state, running something that touches GitHub or a URL and nothing in this tree',\n 'ALLOW: the skip list asks one question — does this read or write repo CONTENT? `gh` talks to GitHub and `curl`/`wget` talk to a network, so the branch state has nothing to say about them. The forms that write a local file (`gh repo clone`, `gh pr checkout`, `curl -o`, any `> file`) are excluded, and `gh pr create`/`merge` remain governed by their own guards',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(8,\n '`pnpm wp-start-upsert-pr` on a branch whose fork point is broken',\n 'row 9 state, running the tool row 9 prescribes',\n 'ALLOW: every `wp-*` bin is on the skip list, so no row can block its own remedy',\n 'None needed',\n 'merged-branch recovery/inspection (allowlisted)'),\n ]),\n new L2Row(5, ['E'], 'on `main`', L2_BLOCK, '`git checkout -b <new> origin/main`', [\n new L2UseCase(9,\n 'An Edit or Write to any tracked file while `git rev-parse --abbrev-ref HEAD` says `main`',\n 'on `main`, any freshness',\n 'BLOCK: decided by one `git rev-parse`, with NO cache read, so it fires on the first tool call of the session',\n '`git checkout -b <new> origin/main` — uncommitted work comes with you',\n 'on-main'),\n new L2UseCase(25,\n 'The FIRST edit of a session, on `main`, before any cache exists',\n 'on `main`, cache absent — row 11 would fail open',\n 'BLOCK anyway: row 5 is ABOVE the cache divider and reads only `git rev-parse`, so it is armed on call #1. This is why the row is `E` only and must never be gated on the cache',\n '`git checkout -b <new> origin/main`',\n 'on-main'),\n ]),\n new L2Row(L2_FAIL_OPEN_ROW, ['B', 'R', 'E'], '**the state could not be established** — branch undeterminable, no cache yet, the cache holds another branch, `origin/main` unknown, or the forge unreachable', L2_FAIL_OPEN, '— (nothing to fix; the refresher populates the cache for the next call)', [\n new L2UseCase(11,\n 'The very first tool call of a session is allowed even on a badly stale `main`',\n 'no cache — the refresher is fire-and-forget and populates it for the NEXT call',\n 'ALLOW (fail-open), logged as `ALLOW_FAIL_OPEN` so abstentions stay countable',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(12,\n 'Guards quietly stand down on a plane, or when `gh` is unauthenticated or rate-limited',\n 'the forge could not be asked whether the PR is merged',\n 'ALLOW (fail-open) logged as `no-forge` — distinct from \"asked, and it is not merged\", which used to look identical in the trail',\n 'None — restore network/`gh auth` to re-arm the merged-branch policy',\n 'no-forge'),\n new L2UseCase(27,\n 'A build, a `cat` or a `curl` on `main`, on the first Bash call of a session',\n 'on `main`, cache absent — so whether `main` is behind is UNKNOWN',\n 'ALLOW (fail-open), logged `ALLOW_FAIL_OPEN`. `B` on `main` is judged by rows 6/7 and therefore lands here when the cache cannot answer; the WRITE half is not, which is why row 5 sits above this divider',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(14,\n 'Mid-rebase, every guard abstains',\n 'detached HEAD — there is no branch name to judge',\n 'ALLOW (fail-open), logged LOUDLY when the branch is unresolvable rather than merely detached',\n 'None — finish or abort the rebase',\n 'branch-undeterminable'),\n ]),\n // ROWS 12/13 sit ABOVE row 6 because they are judged inside its state: local `main` is established\n // BEHIND, and the only remaining question is whether the command already carries its own cure. They\n // are numbered 12/13 rather than slotted in as 6a/6b for the reason row 11 is numbered 11 — row\n // numbers are logged as `row=` and cited here, so renumbering re-points every existing reference.\n new L2Row(12, ['B'], 'on `main`, behind `origin/main`, and the command STARTS with a refresh-main cure joined to the work by `&&`', L2_ALLOW, '—', [\n new L2UseCase(29,\n '`git fetch --prune origin main -q && git pull --ff-only origin main 2>&1 | tail -1 && sed -n \\'30,75p\\' src/app.ts` — the agent cures and reads in one call',\n 'on `main`, behind `origin/main`, cure first, `&&` between',\n 'ALLOW: `&&` short-circuits, so the `sed` never runs if the pull fails — the guard was refusing a safety property the shell already enforces. Measured fleet-wide as `cure_bundled_and`, and filed as a TOOLING defect, not an agent one',\n 'None needed',\n 'cure-prefixed, && short-circuits the work'),\n ]),\n new L2Row(13, ['B'], 'on `main`, behind `origin/main`, and the cure is joined to the work by `;` (or `||`, `&`, a newline) — the work runs even if the cure fails', L2_BLOCK, '`pnpm wp-sync-main && <your command>`', [\n new L2UseCase(30,\n '`pnpm wp-sync-main >/dev/null 2>&1; git log --oneline -1; sed -n \\'598,612p\\' eslint.config.mjs` — and the agent then quotes an eslint rule out of a file 15 commits stale',\n 'on `main`, behind `origin/main`, cure first, `;` between',\n 'BLOCK: `;` discards the cure\\'s exit code, so a conflict, a dirty tree or no network leaves the `sed` reading still-stale content — and 7 of the 9 observed cases also silenced the cure with `>/dev/null 2>&1`, so the failure was invisible too. The two-step is safer because the NEXT tool call re-computes `localMain` against `originMain`, so a failed pull re-blocks; an allowed `;` compound never gets that second look',\n 'Swap the `;` for `&&` — `pnpm wp-sync-main && <your command>` — or run the cure alone and re-issue the command in the next call',\n 'cure-prefixed, work runs anyway'),\n ]),\n new L2Row(6, ['B', 'R'], 'on `main`, behind `origin/main`', L2_BLOCK, '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`', [\n new L2UseCase(13,\n 'The Read tool refuses a file on a stale `main` while you have UNCOMMITTED edits',\n 'on `main`, behind `origin/main`, dirty tree',\n 'BLOCK. This used to fail open, on the argument that the prescribed `git pull` is not a clean fast-forward when the tree is dirty. That was true of the MESSAGE, not the row: the cure cell always offered a second form, and it works dirty',\n '`git checkout -b <new> origin/main` — uncommitted changes come with you onto the new branch. If git refuses because `origin/main` touched the same files, `git stash` first (never blocked), then retry, then `git stash pop`',\n 'on-stale-main'),\n new L2UseCase(15,\n 'The Read tool refuses a file that exists, on a `main` 18 commits behind',\n 'on `main`, behind `origin/main`, clean tree',\n 'BLOCK: judged by live ancestry (`git merge-base --is-ancestor`), not hash equality, so a pull takes effect instantly',\n '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(16,\n 'Read is blocked, so the session reaches for `cat`, `grep` and `ls` instead — and describes a CI workflow set missing a whole workflow that existed upstream',\n 'the SIDE DOOR: same tree, same staleness, different tool',\n 'BLOCK: `B` is judged here beside `R`, so closing the Read tool no longer opens a shell-shaped hole. The log used to read \"read-stale-guard handled\", which is worse than no guard — it looks covered',\n '`git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(10,\n 'A Bash command that WRITES tracked files as a side effect — `npx expo install`, a formatter, codegen, `sed -i`, a `>` redirect',\n 'on a `main` known to be BEHIND, and the write is incidental to a command whose stated purpose is something else',\n 'BLOCK: inside this row `B` is default-DENY plus row 4\\'s skip list, never a blocklist of readers — a command nobody thought to enumerate is caught by not being on the list, which is the only shape that could have caught this one',\n '`git checkout -b <new> origin/main` BEFORE running anything that may write',\n 'on-stale-main'),\n ]),\n new L2Row(7, ['B', 'R'], 'on `main`, current', L2_ALLOW, '—', [\n new L2UseCase(17,\n 'Reading files on a `main` you just pulled',\n 'on `main`, and `origin/main` is an ancestor of HEAD',\n 'ALLOW: ancestry, not hash equality, so the allow arrives the instant the pull lands rather than when the detached refresher next runs',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n new L2UseCase(24,\n '`curl`, `gh pr close` or a test run, immediately after `pnpm wp-sync-main` landed you on a perfectly current `main`',\n 'on `main`, current — no staleness anywhere',\n 'ALLOW. This used to BLOCK, from the branch alone: the tool the repo prescribes put the agent here, and the guard whose name says STALE then refused everything off a narrow allowlist for a reason that had nothing to do with staleness. WRITES here are still blocked, by row 5 — that hazard is real at any freshness',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n ]),\n new L2Row(8, ['B', 'R', 'E'], 'on a branch whose PR is **already merged**', L2_BLOCK, '`git fetch origin main && git checkout -b <new> origin/main`', [\n new L2UseCase(18,\n 'You keep working on the branch after its PR merged, and the next PR reopens code review already landed',\n 'branch whose PR is merged — `merged` is monotonic, so the cached flag is trusted with no TTL',\n 'BLOCK across all three tools',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n new L2UseCase(26,\n 'You have uncommitted edits on a branch whose PR just merged',\n 'merged branch, dirty tree',\n 'BLOCK. This used to fail open too, and that valve never had an argument behind it — row 8\\'s cure carries uncommitted work onto the fresh branch, so nothing was ever trapped. It was drift from the documented design, which `read-stale-guard`\\'s own class comment still described correctly',\n '`git fetch origin main && git checkout -b <new> origin/main` — your edits come with you',\n 'already-merged PR#'),\n new L2UseCase(19,\n 'A shell-only session sails through on a merged branch',\n 'merged branch, Bash only — both FILE guards are file-scoped, so Bash reached neither',\n 'BLOCK: `merged-branch-bash-guard` exists because `branchAlreadyMerged` was being computed and logged on that very path, then thrown away',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n ]),\n new L2Row(9, ['B', 'R', 'E'], 'no fork point with `origin/main`, or `origin/main` moved and collided with your files', L2_BLOCK, '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open', [\n new L2UseCase(20,\n 'Your branch and `origin/main` share no merge base — usually a branch cut from a squashed-away tip',\n 'no fork point',\n 'BLOCK: nothing built on this branch can be reasoned about relative to main',\n '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open',\n 'no-fork-point'),\n new L2UseCase(21,\n '`origin/main` moved and changed the same files you edited',\n 'main-moved collision',\n 'BLOCK — and row 3 then exempts everything once the merge starts, which is what makes this safe',\n '`pnpm wp-start-update`, resolve, `pnpm wp-finish-upsert-pr`',\n 'main-moved-conflict'),\n ]),\n new L2Row(10, ['B', 'R', 'E'], 'healthy feature branch', L2_ALLOW, '—', [\n new L2UseCase(22,\n 'Ordinary work on a branch cut from a current `origin/main`',\n 'healthy feature branch',\n 'ALLOW — the state every other row exists to push you back into',\n 'None needed',\n 'clean-feature-branch'),\n new L2UseCase(23,\n '`stale-main-bash-guard` sees a feature branch and hands off',\n 'not on `main` — state B belongs to `merged-branch-bash-guard`',\n 'ALLOW: the same verdict about the same tree, logged by the guard that is not responsible for it',\n 'None needed',\n 'not-on-main (state B is another guard)'),\n ]),\n];\n\n/**\n * Every use case on every row, in row order, for the doc and for the exhaustiveness specs.\n *\n * Numbering is GLOBAL and is identity, exactly as the row numbers are: a use case is cited by number in\n * review and in the doc, so add new ones at the END of the highest number rather than renumbering to\n * keep a row's block contiguous.\n */\n// webpieces-disable no-function-outside-class -- pure accessor over L2_ROWS above, in this data module\nexport function allL2UseCases(): readonly L2UseCase[] {\n return L2_ROWS.flatMap((row: L2Row): readonly L2UseCase[] => row.useCases);\n}\n\n/**\n * REASON → ROW. The join between what a guard actually logged and the row it is an instance of.\n *\n * Keys are the exact `reason` strings the four L2 guards pass to their decision log. Two of them are\n * PREFIXES because the guard interpolates a PR number or a matched segment into the reason\n * (`already-merged PR#123`); those are matched by `l2RowForReason` on prefix, which is why they end in\n * a space or a `(`.\n *\n * l2-matrix.spec.ts reads the four guard sources and asserts every reason literal in them resolves\n * here, so a new exit with no row fails the build rather than logging `row=-` forever.\n */\nconst EXACT_REASON_ROWS: Record<string, number> = {\n // Row 1 — the universal cure that must stay reachable from inside any block.\n 'webpieces-config-read (escape hatch)': 1,\n // Row 2 — the preventive half, decided from command TEXT before any cache is read.\n // (prefix, see PREFIX_REASON_ROWS)\n // Row 4 — the skip list, in its two live spellings.\n 'merged-branch recovery/inspection (allowlisted)': 4,\n 'not-a-content-read (cure/build/metadata)': 4,\n // Row 5 — never WRITE on main, at any freshness. `E` only; the Bash half is rows 6/7.\n 'on-main': 5,\n // Row 6/7 — freshness, for `B` and `R` alike. Both guards log these two literals: read-stale-guard\n // for the Read tool, stale-main-bash-guard for Bash. Same cache, same ancestry test, one verdict.\n 'on-stale-main': 6,\n 'local-main-contains-origin (up to date)': 7,\n // Rows 12/13 — composition, judged INSIDE row 6's state: the tree is established behind, and the\n // only remaining question is whether the command carries its own cure and with which operator.\n 'cure-prefixed, && short-circuits the work': 12,\n 'cure-prefixed, work runs anyway': 13,\n // Row 9 — the two unhealthy-fork states.\n 'no-fork-point': 9,\n 'main-moved-conflict': 9,\n // Row 10 — healthy, and the state-B guard's \"this is not my state\" hand-off, which is the same\n // verdict about the same tree.\n 'clean-feature-branch': 10,\n 'not-on-main (state B is another guard)': 10,\n // Row 11 — every \"could not establish\", including the two dirty-tree valves the code still opens\n // (see NOT_DONE) and the unreachable forge.\n 'branch-undeterminable': L2_FAIL_OPEN_ROW,\n 'no-sync-cache': L2_FAIL_OPEN_ROW,\n 'stale-cross-branch-cache': L2_FAIL_OPEN_ROW,\n 'origin-main-unknown': L2_FAIL_OPEN_ROW,\n // `dirty-tree-on-main` and `dirty-merged-branch` used to live here. Both valves are deleted: rows\n // 6 and 8 now block on a dirty tree, because each row's cure carries uncommitted work with you.\n 'no-forge': L2_FAIL_OPEN_ROW,\n // The config-edit bypass logged by the hook adapter before any guard runs — row 1, same universal\n // cure as the config READ above.\n 'config-bypass (feature-branch-guard skipped)': 1,\n};\n\n/** Reasons the guards interpolate a value into. Matched by prefix, longest first. */\nconst PREFIX_REASON_ROWS: Record<string, number> = {\n 'already-merged PR#': 8,\n 'bare checkout of main (': 2,\n // `stale-main content read (` used to live here, mapping the Bash side of row 6. It is gone with\n // the guard exit that emitted it: on `main`, row 5 now blocks before any content-read scan runs,\n // so row 6 is what the table always said it was — the ROW-ONLY row.\n};\n\n/**\n * The row a logged reason belongs to, or null when nothing claims it.\n *\n * Null rather than a default row: a reason with no row is a HOLE in the table, and defaulting it to\n * \"fail-open\" would hide exactly the drift the exhaustiveness spec exists to catch. The guards render\n * null as `row=-`, so an unmapped reason is visible in the log too, not only in CI.\n */\n// webpieces-disable no-function-outside-class -- the matcher over the two tables above, beside them in this data module\nexport function l2RowForReason(reason: string): number | null {\n const exact = EXACT_REASON_ROWS[reason];\n if (exact !== undefined) return exact;\n for (const prefix of Object.keys(PREFIX_REASON_ROWS)) {\n if (reason.startsWith(prefix)) return PREFIX_REASON_ROWS[prefix];\n }\n return null;\n}\n\n/** Every reason string this table claims, for the exhaustiveness spec. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two tables above\nexport function l2MappedReasons(): readonly string[] {\n return [...Object.keys(EXACT_REASON_ROWS), ...Object.keys(PREFIX_REASON_ROWS)];\n}\n\n/** One documented gap between a row and what the guards actually do today. Data-only. */\nexport class L2NotDone {\n constructor(readonly row: number, readonly gap: string, readonly why: string) {}\n}\n\n/**\n * WHERE THE TABLE AND THE CODE DISAGREE, stated rather than papered over.\n *\n * The L1 precedent is `## Not done — \\`o\\` is not exempt yet`: a row the runner cannot reach, named in\n * the generated doc with the reason it has not shipped. The same treatment applies here, and it is what\n * makes it safe to publish a table the guards do not yet dispatch from — a reader is told exactly which\n * rows describe intent rather than behaviour, and the log's `row=` stamps land on row 11 for every one\n * of these, so the trail never claims the strict row fired.\n */\nexport const NOT_DONE: readonly L2NotDone[] = [\n // EMPTY, and that is the goal state: every row in the table is a row the guards actually honour.\n //\n // It held three entries. Row 5's `B` half shipped and then MOVED: on `main`, a write is judged from\n // the branch alone (row 5, above the divider) while Bash is judged on freshness beside the Read tool\n // (rows 6/7), because a build on a CURRENT `main` harms nothing and denying it stranded agents on\n // the very `main` `pnpm wp-sync-main` had just handed them. Rows 6 and 8 held DIRTY-TREE\n // valves, and both are now closed — each of\n // those rows cures with `git checkout -b <new> origin/main`, which carries uncommitted changes onto\n // the new branch, so a dirty tree never trapped anybody. The row 6 entry claimed the dirty argument\n // \"has teeth\" there because its cure is `git pull`; that was a fact about the MESSAGE, which printed\n // only the pull, and the fix was to print both cures rather than to suppress the block.\n //\n // Keep this array. An empty \"Not done\" is a claim worth making explicitly — the doc says so in as\n // many words — and the next divergence between a row and its code belongs here, not in prose.\n];\n"]}
1
+ {"version":3,"file":"l2-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l2-rows.ts"],"names":[],"mappings":"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","sourcesContent":["// ---------------------------------------------------------------------------\n// L2 — the BRANCH-STATE layer, as data.\n//\n// L2 answers one question: *may I work here, and is what I read current?* Drawn as a decision matrix\n// that is TWELVE ordered rows plus one terminal fail-open row, first match wins.\n//\n// This module holds those rows, l2-doc.ts renders them into guards/L2-branch-state.md, and a unit test\n// (l2-matrix.spec.ts) locks that file byte-identical to the renderer — the same mechanism that already\n// makes L0's fault table and L1's location table undriftable. Before this existed the L2 doc was 100%\n// hand-written and said so: *\"Until that lands this text is hand-written and can drift.\"*\n//\n// ## HOW L2 JOINS TO THE ROWS TODAY — read this before assuming it works like L1\n//\n// L1 DISPATCHES from its array: `runner.l1LocationBlock` takes the first matching row and switches on\n// its `blockId`, so deleting a row deletes a block. L2 does NOT, and pretending otherwise would be the\n// drift this table exists to remove. The four L2 guard classes each own their own ladder, and those\n// ladders diverge on purpose (guards/L2-branch-state.md, \"Deliberate divergence\": the two Bash guards\n// differ in polarity, quantifier and empty-command handling all at once, so no single parameterised\n// function serves both). Unifying them is where a wrong edit turns an allow into a session-wedging\n// block, so it is deliberately not attempted here.\n//\n// What L2 does instead is a REASON→ROW join. Every exit of every L2 guard already carries a stable\n// reason string into the decision log; `L2_ROW_FOR_REASON` maps each of those to the row it is an\n// instance of, the guards stamp that number as `row=`, and l2-matrix.spec.ts asserts the map is\n// EXHAUSTIVE against the guard sources — a new reason with no row fails the build. So `row=8` in\n// `.webpieces/logs/L2-decisions` opens guards/L2-branch-state.md at row 8 and reads the state, the cure\n// and the tools that row covers, exactly as `row=5` already does for L1.\n//\n// The rows the guards cannot yet honour are named in the doc's \"Not done\" section rather than quietly\n// rendered as if they were live. That section is generated from NOT_DONE below, so it cannot rot.\n//\n// This module is deliberately import-free at runtime, so `pnpm guards:generate` can load it without the\n// package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/** Which tools a row covers. `B` Bash · `R` Read · `E` Write/Edit. */\nexport type L2Tool = 'B' | 'R' | 'E';\n\n/** What L2 does with a row — the same action codebook every layer reports in (GUARD_MATRIX.md). */\nexport type L2ActionKind = 'allow' | 'exempt' | 'block' | 'fail-open';\n\n/**\n * The TERMINAL fail-open row, and the one number in this table that is not from the 1-10 design.\n *\n * Everything in rows 6-10 needs the main-sync cache, and the cache is written by a fire-and-forget\n * refresher that populates it for the NEXT call — so the first tool call of every session has none.\n * \"Stop here and ALLOW\" was written as a DIVIDER in the design table, i.e. as prose between two blocks\n * of rows. Prose cannot be stamped into a log line, and this is the single most frequently taken exit\n * in the whole layer (every session's first call, every unreadable branch, every unreachable forge), so\n * it is a row with a number like any other.\n *\n * It is 11 rather than 6-with-a-renumber because row numbers are IDENTITY here: they are printed in the\n * doc and logged as `row=`, so shifting 6-10 down would silently re-point every reference. The doc\n * prints it in its true position, between rows 5 and 6, with its number shown — same treatment L1 gives\n * row 8, which is printed third and numbered 8.\n */\nexport const L2_FAIL_OPEN_ROW = 11;\n\n/** The `act` cell: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L2Action {\n constructor(readonly label: string, readonly kind: L2ActionKind) {}\n}\n\nexport const L2_ALLOW = new L2Action('1 allow', 'allow');\nexport const L2_EXEMPT = new L2Action('2 exempt', 'exempt');\nexport const L2_BLOCK = new L2Action('4 block', 'block');\nexport const L2_FAIL_OPEN = new L2Action('1 allow (fail-open)', 'fail-open');\n\n/**\n * One row of the \"L2 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * THE POINT OF THIS CLASS is that a use case is added HERE, in code, beside the row it exercises — not\n * into a hand-written doc section that drifts. When a new situation comes up in a session, it becomes\n * one more `new L2UseCase(...)` on the row that judged it, `pnpm guards:generate` re-renders the doc,\n * and the byte-lock spec fails if anyone edits the rendered table instead.\n *\n * The four text fields are rendered VERBATIM. `reason` is the ENFORCEMENT half and is never rendered:\n * it is the exact `reason` string the guard logs for this case, so a spec can push it back through\n * `l2RowForReason` and assert it lands on the row this use case is filed under. That closes the loop\n * the L2 decision log opens — `row=` in the trail, this table on the page, one join between them.\n *\n * `reason` is REQUIRED, and a case that exercises something which is not an L2 row exit says so with\n * `NO_ROW_EXIT` rather than by omitting the argument. An optional field would make opting OUT of the\n * only real enforcement here the shortest thing to type and impossible to grep — the widening-by-absence\n * shape `.claude/rules/no-backwards-compat.md` rejects. `grep NO_ROW_EXIT` now lists every unenforced case.\n */\nexport class L2UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the reason behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly reason: string,\n ) {}\n}\n\n/**\n * The `reason` for a use case that is NOT an L2 row exit, and so has nothing to join back to.\n *\n * The only legitimate case today is row 3: merge-in-progress is L4's state, and L2 exempts it without\n * logging a reason of its own. Named rather than absent, so \"this case is not enforced\" is a value in\n * the table you can grep for instead of a missing argument nobody notices.\n */\nexport const NO_ROW_EXIT = 'NO_ROW_EXIT (not an L2 row exit — another layer owns this state)';\n\n/**\n * One row of L2's decision table.\n *\n * `cure` is rendered verbatim into the doc and is LITERAL by policy: L0's cure-reachability discipline\n * says a message pointing at documentation for its own remedy cannot be tested, and it caught a fault\n * prescribing a bin that had been renamed away. `—` is the only legal non-command cure, and only on a\n * row that allows.\n */\nexport class L2Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- the five cells of one doc row plus its use cases\n constructor(\n readonly num: number,\n readonly tools: readonly L2Tool[],\n /** The `state` cell, verbatim. */\n readonly state: string,\n readonly action: L2Action,\n /** The `cure` cell, verbatim. `—` when the row allows. */\n readonly cure: string,\n /**\n * The observed situations this row judges. Rendered as the \"L2 use cases\" table.\n *\n * A NON-EMPTY tuple, and required: a row nobody has ever seen fire is either dead or\n * undocumented, and both are worth knowing. Expressing that in the TYPE rather than as a\n * runtime assertion is the JwtRoles pattern — the invariant is enforced at the moment the row\n * is written, which is the only moment that changes what somebody types.\n */\n readonly useCases: readonly [L2UseCase, ...L2UseCase[]],\n ) {}\n\n /** `B R E`, the doc's own spelling of the tool cell. */\n toolCell(): string {\n return this.tools.join(' ');\n }\n}\n\n/**\n * THE THIRTEEN L2 ROWS, in first-match-wins order.\n *\n * Rows 1-5 need NO cache and fire on call #1: rows 1, 2 and 4 are text matches, row 3 is a marker-file\n * scan, row 5 is one `git rev-parse`. Row 11 is the cache divider. Rows 6-10 all read the cache.\n *\n * THE ORDER OF ROW 5 IS THE MOST LOAD-BEARING THING IN THIS TABLE. Put \"on main\" BELOW the divider and\n * WRITES on `main` are permitted for the whole first call of every session — and permanently in a\n * multi-worktree repo, where another tree may hold the cache lock indefinitely.\n *\n * `B` AND `E` PART COMPANY ON `main`, and rows 5/6/7 are where. A WRITE on `main` is wrong at any\n * freshness — the work lands somewhere unreviewable and unrevertable — so row 5 is `E` only, judged on\n * the branch alone, above the divider. A READ or a BUILD on a CURRENT `main` is harmless, and blocking\n * it strands the agent right after `pnpm wp-sync-main` put it there; so `B` joins `R` on the\n * FRESHNESS-gated pair below the divider (row 6 behind → block, row 7 current → allow), where \"cannot\n * tell\" fails open at row 11 by construction. `B` and `R` still differ in SHAPE inside row 6: a Read\n * names one file and is judged precisely, a Bash command is opaque and gets default-deny plus row 4.\n */\nexport const L2_ROWS: readonly L2Row[] = [\n new L2Row(1, ['B', 'R', 'E'], 'on the **global allowlist** (inert command, or a universal cure such as reading/editing `webpieces.config.json`)', L2_ALLOW, '—', [\n new L2UseCase(1,\n 'You are blocked by some other L2 row, and need to turn the policy off to get anything done',\n 'any state — this row is ahead of every block',\n 'ALLOW: reading and editing `webpieces.config.json` is never blocked, so the mode-OFF cure is always reachable',\n 'Edit `webpieces.config.json` → `hookGuards` → `branch-state-guard` → `\"mode\": \"OFF\"`',\n 'webpieces-config-read (escape hatch)'),\n new L2UseCase(2,\n 'A Write to `webpieces.config.json` while on `main`, which row 5 would otherwise block',\n 'on `main`, editing the one file that can disable the guard',\n 'ALLOW: the hook adapter bypasses feature-branch-guard for this path before any guard runs',\n 'None needed — the edit proceeds',\n 'config-bypass (feature-branch-guard skipped)'),\n ]),\n new L2Row(2, ['B'], 'bare `git checkout main`, with no `git pull` chained into the same command', L2_BLOCK, '`pnpm wp-sync-main`', [\n new L2UseCase(3,\n '`git checkout main` after a merge, to start the next piece of work',\n 'about to land on whatever local `main` you last had — 157 commits behind, in the incident',\n 'BLOCK: decided from command TEXT alone, before the checkout, because the only `main` this could measure is the one it is about to leave',\n '`pnpm wp-sync-main` — checkout, pull, reap dead branches/worktrees, sweep orphan directories, in one command (hand-rolled, the pull must be in the SAME command as the checkout)',\n 'bare checkout of main ('),\n new L2UseCase(4,\n 'The same command inside a linked worktree, where `git checkout main` fatals anyway',\n 'linked worktree — `main` is already checked out in the primary clone',\n 'BLOCK, and the message prints the worktree form rather than a cure git would refuse',\n '`git fetch origin main`, then work off `origin/main`',\n 'bare checkout of main ('),\n ]),\n new L2Row(3, ['B', 'R', 'E'], '**merge in progress** — L4 owns this state', L2_EXEMPT, 'finish the merge: `pnpm wp-finish-upsert-pr`', [\n new L2UseCase(5,\n 'Reading and editing conflicted files during a 3-point merge, on a branch row 9 would block',\n 'merge markers on disk — `pnpm wp-start-update` has run and not finished',\n 'EXEMPT: everything is permitted, which is exactly what lets row 9 be strict',\n 'Resolve the conflicts, then `pnpm wp-finish-upsert-pr`',\n NO_ROW_EXIT),\n ]),\n new L2Row(4, ['B'], 'on the **skip list** — it gets you OUT, or tells you where you are', L2_ALLOW, '—', [\n new L2UseCase(6,\n '`git status` / `gh pr view` while blocked, to work out where you are',\n 'any state — orientation is never \"working here\"',\n 'ALLOW: metadata tells you where you are without putting stale file CONTENT in context',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(7,\n '`git stash` when `git checkout -b <new> origin/main` refuses because `origin/main` touched the same files you edited',\n 'on a stale `main` or a merged branch, dirty tree, with an overlapping upstream change',\n 'ALLOW: the cure for the row that blocked you must itself never be blocked — and this is the residual step that makes rows 6 and 8 safe to block on a dirty tree',\n 'None needed — then re-run the checkout and `git stash pop`',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(28,\n '`gh pr close 123`, `gh pr comment`, `gh api …` or a `curl` while parked on a stale `main` or a merged branch',\n 'blocked state, running something that touches GitHub or a URL and nothing in this tree',\n 'ALLOW: the skip list asks one question — does this read or write repo CONTENT? `gh` talks to GitHub and `curl`/`wget` talk to a network, so the branch state has nothing to say about them. The forms that write a local file (`gh repo clone`, `gh pr checkout`, `curl -o`, any `> file`) are excluded, and `gh pr create`/`merge` remain governed by their own guards',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(8,\n '`pnpm wp-start-upsert-pr` on a branch whose fork point is broken',\n 'row 9 state, running the tool row 9 prescribes',\n 'ALLOW: every `wp-*` bin is on the skip list, so no row can block its own remedy',\n 'None needed',\n 'merged-branch recovery/inspection (allowlisted)'),\n ]),\n new L2Row(5, ['E'], 'on `main`', L2_BLOCK, '`git checkout -b <new> origin/main`', [\n new L2UseCase(9,\n 'An Edit or Write to any tracked file while `git rev-parse --abbrev-ref HEAD` says `main`',\n 'on `main`, any freshness',\n 'BLOCK: decided by one `git rev-parse`, with NO cache read, so it fires on the first tool call of the session',\n '`git checkout -b <new> origin/main` — uncommitted work comes with you',\n 'on-main'),\n new L2UseCase(25,\n 'The FIRST edit of a session, on `main`, before any cache exists',\n 'on `main`, cache absent — row 11 would fail open',\n 'BLOCK anyway: row 5 is ABOVE the cache divider and reads only `git rev-parse`, so it is armed on call #1. This is why the row is `E` only and must never be gated on the cache',\n '`git checkout -b <new> origin/main`',\n 'on-main'),\n ]),\n new L2Row(L2_FAIL_OPEN_ROW, ['B', 'R', 'E'], '**the state could not be established** — branch undeterminable, no cache yet, the cache holds another branch, `origin/main` unknown, or the forge unreachable', L2_FAIL_OPEN, '— (nothing to fix; the refresher populates the cache for the next call)', [\n new L2UseCase(11,\n 'The very first tool call of a session is allowed even on a badly stale `main`',\n 'no cache — the refresher is fire-and-forget and populates it for the NEXT call',\n 'ALLOW (fail-open), logged as `ALLOW_FAIL_OPEN` so abstentions stay countable',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(12,\n 'Guards quietly stand down on a plane, or when `gh` is unauthenticated or rate-limited',\n 'the forge could not be asked whether the PR is merged',\n 'ALLOW (fail-open) logged as `no-forge` — distinct from \"asked, and it is not merged\", which used to look identical in the trail',\n 'None — restore network/`gh auth` to re-arm the merged-branch policy',\n 'no-forge'),\n new L2UseCase(27,\n 'A build, a `cat` or a `curl` on `main`, on the first Bash call of a session',\n 'on `main`, cache absent — so whether `main` is behind is UNKNOWN',\n 'ALLOW (fail-open), logged `ALLOW_FAIL_OPEN`. `B` on `main` is judged by rows 6/7 and therefore lands here when the cache cannot answer; the WRITE half is not, which is why row 5 sits above this divider',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(14,\n 'Mid-rebase, every guard abstains',\n 'detached HEAD — there is no branch name to judge',\n 'ALLOW (fail-open), logged LOUDLY when the branch is unresolvable rather than merely detached',\n 'None — finish or abort the rebase',\n 'branch-undeterminable'),\n ]),\n // ROWS 12/13 sit ABOVE row 6 because they are judged inside its state: local `main` is established\n // BEHIND, and the only remaining question is whether the command already carries its own cure. They\n // are numbered 12/13 rather than slotted in as 6a/6b for the reason row 11 is numbered 11 — row\n // numbers are logged as `row=` and cited here, so renumbering re-points every existing reference.\n new L2Row(12, ['B'], 'on `main`, behind `origin/main`, and the command STARTS with a refresh-main cure joined to the work by `&&`', L2_ALLOW, '—', [\n new L2UseCase(29,\n '`git fetch --prune origin main -q && git pull --ff-only origin main 2>&1 | tail -1 && sed -n \\'30,75p\\' src/app.ts` — the agent cures and reads in one call',\n 'on `main`, behind `origin/main`, cure first, `&&` between',\n 'ALLOW: `&&` short-circuits, so the `sed` never runs if the pull fails — the guard was refusing a safety property the shell already enforces. Measured fleet-wide as `cure_bundled_and`, and filed as a TOOLING defect, not an agent one',\n 'None needed',\n 'cure-prefixed, && short-circuits the work'),\n ]),\n new L2Row(13, ['B'], 'on `main`, behind `origin/main`, and the cure is joined to the work by `;` (or `||`, `&`, a newline) — the work runs even if the cure fails', L2_BLOCK, '`pnpm wp-sync-main && <your command>`', [\n new L2UseCase(30,\n '`pnpm wp-sync-main >/dev/null 2>&1; git log --oneline -1; sed -n \\'598,612p\\' eslint.config.mjs` — and the agent then quotes an eslint rule out of a file 15 commits stale',\n 'on `main`, behind `origin/main`, cure first, `;` between',\n 'BLOCK: `;` discards the cure\\'s exit code, so a conflict, a dirty tree or no network leaves the `sed` reading still-stale content — and 7 of the 9 observed cases also silenced the cure with `>/dev/null 2>&1`, so the failure was invisible too. The two-step is safer because the NEXT tool call re-computes `localMain` against `originMain`, so a failed pull re-blocks; an allowed `;` compound never gets that second look',\n 'Swap the `;` for `&&` — `pnpm wp-sync-main && <your command>` — or run the cure alone and re-issue the command in the next call',\n 'cure-prefixed, work runs anyway'),\n ]),\n new L2Row(6, ['B', 'R'], 'on `main`, behind `origin/main`', L2_BLOCK, '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`', [\n new L2UseCase(13,\n 'The Read tool refuses a file on a stale `main` while you have UNCOMMITTED edits',\n 'on `main`, behind `origin/main`, dirty tree',\n 'BLOCK. This used to fail open, on the argument that the prescribed `git pull` is not a clean fast-forward when the tree is dirty. That was true of the MESSAGE, not the row: the cure cell always offered a second form, and it works dirty',\n '`git checkout -b <new> origin/main` — uncommitted changes come with you onto the new branch. If git refuses because `origin/main` touched the same files, `git stash` first (never blocked), then retry, then `git stash pop`',\n 'on-stale-main'),\n new L2UseCase(15,\n 'The Read tool refuses a file that exists, on a `main` 18 commits behind',\n 'on `main`, behind `origin/main`, clean tree',\n 'BLOCK: judged by live ancestry (`git merge-base --is-ancestor`), not hash equality, so a pull takes effect instantly',\n '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(16,\n 'Read is blocked, so the session reaches for `cat`, `grep` and `ls` instead — and describes a CI workflow set missing a whole workflow that existed upstream',\n 'the SIDE DOOR: same tree, same staleness, different tool',\n 'BLOCK: `B` is judged here beside `R`, so closing the Read tool no longer opens a shell-shaped hole. The log used to read \"read-stale-guard handled\", which is worse than no guard — it looks covered',\n '`git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(10,\n 'A Bash command that WRITES tracked files as a side effect — `npx expo install`, a formatter, codegen, `sed -i`, a `>` redirect',\n 'on a `main` known to be BEHIND, and the write is incidental to a command whose stated purpose is something else',\n 'BLOCK: inside this row `B` is default-DENY plus row 4\\'s skip list, never a blocklist of readers — a command nobody thought to enumerate is caught by not being on the list, which is the only shape that could have caught this one',\n '`git checkout -b <new> origin/main` BEFORE running anything that may write',\n 'on-stale-main'),\n ]),\n new L2Row(7, ['B', 'R'], 'on `main`, current', L2_ALLOW, '—', [\n new L2UseCase(17,\n 'Reading files on a `main` you just pulled',\n 'on `main`, and `origin/main` is an ancestor of HEAD',\n 'ALLOW: ancestry, not hash equality, so the allow arrives the instant the pull lands rather than when the detached refresher next runs',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n new L2UseCase(24,\n '`curl`, `gh pr close` or a test run, immediately after `pnpm wp-sync-main` landed you on a perfectly current `main`',\n 'on `main`, current — no staleness anywhere',\n 'ALLOW. This used to BLOCK, from the branch alone: the tool the repo prescribes put the agent here, and the guard whose name says STALE then refused everything off a narrow allowlist for a reason that had nothing to do with staleness. WRITES here are still blocked, by row 5 — that hazard is real at any freshness',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n ]),\n new L2Row(8, ['B', 'R', 'E'], 'on a branch whose PR is **already merged**', L2_BLOCK, '`git fetch origin main && git checkout -b <new> origin/main`', [\n new L2UseCase(18,\n 'You keep working on the branch after its PR merged, and the next PR reopens code review already landed',\n 'branch whose PR is merged — `merged` is monotonic, so the cached flag is trusted with no TTL',\n 'BLOCK across all three tools',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n new L2UseCase(26,\n 'You have uncommitted edits on a branch whose PR just merged',\n 'merged branch, dirty tree',\n 'BLOCK. This used to fail open too, and that valve never had an argument behind it — row 8\\'s cure carries uncommitted work onto the fresh branch, so nothing was ever trapped. It was drift from the documented design, which `read-stale-guard`\\'s own class comment still described correctly',\n '`git fetch origin main && git checkout -b <new> origin/main` — your edits come with you',\n 'already-merged PR#'),\n new L2UseCase(19,\n 'A shell-only session sails through on a merged branch',\n 'merged branch, Bash only — both FILE guards are file-scoped, so Bash reached neither',\n 'BLOCK: `merged-branch-bash-guard` exists because `branchAlreadyMerged` was being computed and logged on that very path, then thrown away',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n ]),\n new L2Row(9, ['B', 'R', 'E'], 'no fork point with `origin/main`, or `origin/main` moved and collided with your files', L2_BLOCK, '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open', [\n new L2UseCase(20,\n 'Your branch and `origin/main` share no merge base — usually a branch cut from a squashed-away tip',\n 'no fork point',\n 'BLOCK: nothing built on this branch can be reasoned about relative to main',\n '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open',\n 'no-fork-point'),\n new L2UseCase(21,\n '`origin/main` moved and changed the same files you edited',\n 'main-moved collision',\n 'BLOCK — and row 3 then exempts everything once the merge starts, which is what makes this safe',\n '`pnpm wp-start-update`, resolve, `pnpm wp-finish-upsert-pr`',\n 'main-moved-conflict'),\n ]),\n new L2Row(10, ['B', 'R', 'E'], 'healthy feature branch', L2_ALLOW, '—', [\n new L2UseCase(22,\n 'Ordinary work on a branch cut from a current `origin/main`',\n 'healthy feature branch',\n 'ALLOW — the state every other row exists to push you back into',\n 'None needed',\n 'clean-feature-branch'),\n new L2UseCase(23,\n '`stale-main-bash-guard` sees a feature branch and hands off',\n 'not on `main` — state B belongs to `merged-branch-bash-guard`',\n 'ALLOW: the same verdict about the same tree, logged by the guard that is not responsible for it',\n 'None needed',\n 'not-on-main (state B is another guard)'),\n ]),\n];\n\n/**\n * Every use case on every row, in row order, for the doc and for the exhaustiveness specs.\n *\n * Numbering is GLOBAL and is identity, exactly as the row numbers are: a use case is cited by number in\n * review and in the doc, so add new ones at the END of the highest number rather than renumbering to\n * keep a row's block contiguous.\n */\n// webpieces-disable no-function-outside-class -- pure accessor over L2_ROWS above, in this data module\nexport function allL2UseCases(): readonly L2UseCase[] {\n return L2_ROWS.flatMap((row: L2Row): readonly L2UseCase[] => row.useCases);\n}\n\n/**\n * REASON → ROW. The join between what a guard actually logged and the row it is an instance of.\n *\n * Keys are the exact `reason` strings the four L2 guards pass to their decision log. Two of them are\n * PREFIXES because the guard interpolates a PR number or a matched segment into the reason\n * (`already-merged PR#123`); those are matched by `l2RowForReason` on prefix, which is why they end in\n * a space or a `(`.\n *\n * l2-matrix.spec.ts reads the four guard sources and asserts every reason literal in them resolves\n * here, so a new exit with no row fails the build rather than logging `row=-` forever.\n */\nconst EXACT_REASON_ROWS: Record<string, number> = {\n // Row 1 — the universal cure that must stay reachable from inside any block.\n 'webpieces-config-read (escape hatch)': 1,\n // Row 2 — the preventive half, decided from command TEXT before any cache is read.\n // (prefix, see PREFIX_REASON_ROWS)\n // Row 4 — the skip list, in its two live spellings.\n 'merged-branch recovery/inspection (allowlisted)': 4,\n 'not-a-content-read (cure/build/metadata)': 4,\n // Row 5 — never WRITE on main, at any freshness. `E` only; the Bash half is rows 6/7.\n 'on-main': 5,\n // Row 6/7 — freshness, for `B` and `R` alike. Both guards log these two literals: read-stale-guard\n // for the Read tool, stale-main-bash-guard for Bash. Same cache, same ancestry test, one verdict.\n 'on-stale-main': 6,\n 'local-main-contains-origin (up to date)': 7,\n // Rows 12/13 — composition, judged INSIDE row 6's state: the tree is established behind, and the\n // only remaining question is whether the command carries its own cure and with which operator.\n 'cure-prefixed, && short-circuits the work': 12,\n 'cure-prefixed, work runs anyway': 13,\n // Row 9 — the two unhealthy-fork states.\n 'no-fork-point': 9,\n 'main-moved-conflict': 9,\n // Row 10 — healthy, and the state-B guard's \"this is not my state\" hand-off, which is the same\n // verdict about the same tree.\n 'clean-feature-branch': 10,\n 'not-on-main (state B is another guard)': 10,\n // Row 11 — every \"could not establish\", including the two dirty-tree valves the code still opens\n // (see NOT_DONE) and the unreachable forge.\n 'branch-undeterminable': L2_FAIL_OPEN_ROW,\n // The target tree HAS no branch name (mid-rebase / mid-bisect), so there is no key into the\n // branch-keyed cache — use case 14, which the row already describes. It used to arrive here as the\n // literal branch `HEAD`, miss in the cache and log `no-sync-cache`: the right verdict recorded under\n // a reason that names a different cause, and therefore uncountable.\n 'detached-head': L2_FAIL_OPEN_ROW,\n // The file being judged lives in a NESTED CLONE (`repositories/**`) — another repo's branch is not\n // this policy's to judge. The bash path calls the same state ALLOW_EXEMPT at L1; on the file path it\n // is an abstention, because nothing about the target tree's state was established.\n 'target-tree-foreign': L2_FAIL_OPEN_ROW,\n 'no-sync-cache': L2_FAIL_OPEN_ROW,\n 'stale-cross-branch-cache': L2_FAIL_OPEN_ROW,\n 'origin-main-unknown': L2_FAIL_OPEN_ROW,\n // `dirty-tree-on-main` and `dirty-merged-branch` used to live here. Both valves are deleted: rows\n // 6 and 8 now block on a dirty tree, because each row's cure carries uncommitted work with you.\n 'no-forge': L2_FAIL_OPEN_ROW,\n // The config-edit bypass logged by the hook adapter before any guard runs — row 1, same universal\n // cure as the config READ above.\n 'config-bypass (feature-branch-guard skipped)': 1,\n};\n\n/** Reasons the guards interpolate a value into. Matched by prefix, longest first. */\nconst PREFIX_REASON_ROWS: Record<string, number> = {\n 'already-merged PR#': 8,\n 'bare checkout of main (': 2,\n // `stale-main content read (` used to live here, mapping the Bash side of row 6. It is gone with\n // the guard exit that emitted it: on `main`, row 5 now blocks before any content-read scan runs,\n // so row 6 is what the table always said it was — the ROW-ONLY row.\n};\n\n/**\n * The row a logged reason belongs to, or null when nothing claims it.\n *\n * Null rather than a default row: a reason with no row is a HOLE in the table, and defaulting it to\n * \"fail-open\" would hide exactly the drift the exhaustiveness spec exists to catch. The guards render\n * null as `row=-`, so an unmapped reason is visible in the log too, not only in CI.\n */\n// webpieces-disable no-function-outside-class -- the matcher over the two tables above, beside them in this data module\nexport function l2RowForReason(reason: string): number | null {\n const exact = EXACT_REASON_ROWS[reason];\n if (exact !== undefined) return exact;\n for (const prefix of Object.keys(PREFIX_REASON_ROWS)) {\n if (reason.startsWith(prefix)) return PREFIX_REASON_ROWS[prefix];\n }\n return null;\n}\n\n/** Every reason string this table claims, for the exhaustiveness spec. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two tables above\nexport function l2MappedReasons(): readonly string[] {\n return [...Object.keys(EXACT_REASON_ROWS), ...Object.keys(PREFIX_REASON_ROWS)];\n}\n\n/** One documented gap between a row and what the guards actually do today. Data-only. */\nexport class L2NotDone {\n constructor(readonly row: number, readonly gap: string, readonly why: string) {}\n}\n\n/**\n * WHERE THE TABLE AND THE CODE DISAGREE, stated rather than papered over.\n *\n * The L1 precedent is `## Not done — \\`o\\` is not exempt yet`: a row the runner cannot reach, named in\n * the generated doc with the reason it has not shipped. The same treatment applies here, and it is what\n * makes it safe to publish a table the guards do not yet dispatch from — a reader is told exactly which\n * rows describe intent rather than behaviour, and the log's `row=` stamps land on row 11 for every one\n * of these, so the trail never claims the strict row fired.\n */\nexport const NOT_DONE: readonly L2NotDone[] = [\n // EMPTY, and that is the goal state: every row in the table is a row the guards actually honour.\n //\n // It held three entries. Row 5's `B` half shipped and then MOVED: on `main`, a write is judged from\n // the branch alone (row 5, above the divider) while Bash is judged on freshness beside the Read tool\n // (rows 6/7), because a build on a CURRENT `main` harms nothing and denying it stranded agents on\n // the very `main` `pnpm wp-sync-main` had just handed them. Rows 6 and 8 held DIRTY-TREE\n // valves, and both are now closed — each of\n // those rows cures with `git checkout -b <new> origin/main`, which carries uncommitted changes onto\n // the new branch, so a dirty tree never trapped anybody. The row 6 entry claimed the dirty argument\n // \"has teeth\" there because its cure is `git pull`; that was a fact about the MESSAGE, which printed\n // only the pull, and the fix was to print both cures rather than to suppress the block.\n //\n // Keep this array. An empty \"Not done\" is a claim worth making explicitly — the doc says so in as\n // many words — and the next divergence between a row and its code belongs here, not in prose.\n];\n"]}
@@ -34,6 +34,33 @@ import { FixHint } from '../fix-hint';
34
34
  * permitted for the whole first call of every session (the cache is populated for the NEXT call), and
35
35
  * permanently in a multi-worktree repo where another tree can hold the refresh lock. That ordering is
36
36
  * the most load-bearing thing in the L2 table, which is why row 5 sits ABOVE the cache divider.
37
+ *
38
+ * ── WHICH TREE IS JUDGED: THE ONE THAT OWNS THE FILE, NEVER THE ONE THE SESSION SITS IN ─────────────
39
+ *
40
+ * Every state above is a property of A BRANCH IN A CHECKOUT, so the first question this guard answers is
41
+ * WHICH checkout — and the answer comes from the TARGET PATH, through TargetTreeResolver, which is
42
+ * EffectiveTreeResolver asked about a file instead of a cwd. It is emphatically NOT `ctx.workspaceRoot`:
43
+ * that is the walk-up from the session's cwd to the governing `webpieces.config.json`, and for a
44
+ * main-session edit into an agent worktree it names the PRIMARY clone.
45
+ *
46
+ * Issue #851 is the incident. Claude Code checks a worktree out INSIDE the repo at
47
+ * `<repo>/.claude/worktrees/agent-XXXX`, so a containment test answers "primary" for a path that is
48
+ * plainly the worktree's. This guard then read the branch-keyed main-sync cache under the PRIMARY's
49
+ * branch and enforced that verdict on a file belonging to a clean branch in another tree. The cache was
50
+ * not stale and was not wrong — the correct entry sat in the same JSON file, one key over. Only the
51
+ * lookup key was. Four tool calls were refused, including the `review.json` that `wp-review-upsert-pr`
52
+ * requires before `wp-finish-upsert-pr` will open a PR, which is the shape where this wedges the
53
+ * sanctioned flow rather than merely annoying somebody.
54
+ *
55
+ * Two consequences worth stating out loud, because both were argued the other way at some point:
56
+ *
57
+ * - The DENY NAMES THE TREE (JudgedTree.header) and, when the judged tree is not the session's, the
58
+ * `pnpm --dir=<tree>` form of the cure. A resolution nobody can see is a resolution nobody can
59
+ * contradict, and the printed cure was measurably a no-op for the tree it was aimed at.
60
+ * - DETACHED HEAD in the target tree fails OPEN, logged. There is no branch name, so there is no map
61
+ * key and nothing to judge — matrix row 14. It used to reach here as the literal branch `HEAD`, miss
62
+ * in the cache and fail open as `no-sync-cache`, which is the right verdict recorded under a reason
63
+ * that says something else happened.
37
64
  */
38
65
  export declare class FeatureBranchGuardRule extends FileRuleBase<BranchStateGuardConfig> {
39
66
  constructor(config: BranchStateGuardConfig);
@@ -45,6 +72,15 @@ export declare class FeatureBranchGuardRule extends FileRuleBase<BranchStateGuar
45
72
  };
46
73
  readonly fixHint: FixHint;
47
74
  check(ctx: FileContext): readonly Violation[];
75
+ /**
76
+ * WHICH TREE this decision was judged against, for the log.
77
+ *
78
+ * The trail already carried a `tree=` field, but it is stamped from the root the decision is LOGGED
79
+ * to (the session's), so during issue #851 every misfired block recorded `tree=primary` for a path
80
+ * under `.claude/worktrees/` and read as perfectly ordinary. This one is the root the verdict was
81
+ * actually computed from, so the two disagreeing is the defect, visible in one grep.
82
+ */
83
+ private treeSummary;
48
84
  private cacheSummary;
49
85
  /**
50
86
  * The guard could not ESTABLISH the state it judges on, so it judged nothing.
@@ -12,7 +12,9 @@ const main_sync_timeout_1 = require("../main-sync-timeout");
12
12
  const decision_log_1 = require("../decision-log");
13
13
  const l2_matrix_doc_1 = require("../l2-matrix-doc");
14
14
  const l0_fault_codes_1 = require("../l0-fault-codes");
15
+ const target_tree_1 = require("../target-tree");
15
16
  const merged_branch_message_1 = require("./merged-branch-message");
17
+ const judged_tree_1 = require("./judged-tree");
16
18
  const tree_recovery_1 = require("./tree-recovery");
17
19
  /**
18
20
  * Comprehensive "are you on a proper feature branch?" guard — the single rule that blocks edits when
@@ -46,6 +48,33 @@ const tree_recovery_1 = require("./tree-recovery");
46
48
  * permitted for the whole first call of every session (the cache is populated for the NEXT call), and
47
49
  * permanently in a multi-worktree repo where another tree can hold the refresh lock. That ordering is
48
50
  * the most load-bearing thing in the L2 table, which is why row 5 sits ABOVE the cache divider.
51
+ *
52
+ * ── WHICH TREE IS JUDGED: THE ONE THAT OWNS THE FILE, NEVER THE ONE THE SESSION SITS IN ─────────────
53
+ *
54
+ * Every state above is a property of A BRANCH IN A CHECKOUT, so the first question this guard answers is
55
+ * WHICH checkout — and the answer comes from the TARGET PATH, through TargetTreeResolver, which is
56
+ * EffectiveTreeResolver asked about a file instead of a cwd. It is emphatically NOT `ctx.workspaceRoot`:
57
+ * that is the walk-up from the session's cwd to the governing `webpieces.config.json`, and for a
58
+ * main-session edit into an agent worktree it names the PRIMARY clone.
59
+ *
60
+ * Issue #851 is the incident. Claude Code checks a worktree out INSIDE the repo at
61
+ * `<repo>/.claude/worktrees/agent-XXXX`, so a containment test answers "primary" for a path that is
62
+ * plainly the worktree's. This guard then read the branch-keyed main-sync cache under the PRIMARY's
63
+ * branch and enforced that verdict on a file belonging to a clean branch in another tree. The cache was
64
+ * not stale and was not wrong — the correct entry sat in the same JSON file, one key over. Only the
65
+ * lookup key was. Four tool calls were refused, including the `review.json` that `wp-review-upsert-pr`
66
+ * requires before `wp-finish-upsert-pr` will open a PR, which is the shape where this wedges the
67
+ * sanctioned flow rather than merely annoying somebody.
68
+ *
69
+ * Two consequences worth stating out loud, because both were argued the other way at some point:
70
+ *
71
+ * - The DENY NAMES THE TREE (JudgedTree.header) and, when the judged tree is not the session's, the
72
+ * `pnpm --dir=<tree>` form of the cure. A resolution nobody can see is a resolution nobody can
73
+ * contradict, and the printed cure was measurably a no-op for the tree it was aimed at.
74
+ * - DETACHED HEAD in the target tree fails OPEN, logged. There is no branch name, so there is no map
75
+ * key and nothing to judge — matrix row 14. It used to reach here as the literal branch `HEAD`, miss
76
+ * in the cache and fail open as `no-sync-cache`, which is the right verdict recorded under a reason
77
+ * that says something else happened.
49
78
  */
50
79
  class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
51
80
  // NAME is this class's operator identity (every `rule=` in the log, every deny header);
@@ -66,22 +95,37 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
66
95
  // Only files inside the workspace root — guard has no jurisdiction, nothing worth logging.
67
96
  if (ctx.relativePath.startsWith('..'))
68
97
  return [];
69
- const branch = this.currentBranch(ctx.workspaceRoot);
98
+ // WHICH CHECKOUT owns this file? Git's answer, via the resolver the bash half already uses —
99
+ // never a path-containment test, which is exactly what answered "primary" for a worktree
100
+ // checked out INSIDE the repo (issue #851, and this class's header).
101
+ const target = new target_tree_1.TargetTreeResolver().resolve(ctx.filePath, ctx.workspaceRoot);
102
+ // A NESTED CLONE under `repositories/**` is somebody else's repo. The bash path calls this
103
+ // ALLOW_EXEMPT; here it is an abstention, because the state was not established, not waived.
104
+ if (target.kind === 'foreign')
105
+ return this.failOpen(ctx, null, 'target-tree-foreign');
106
+ const judgedRoot = target.root;
107
+ const branch = this.currentBranch(judgedRoot);
70
108
  // Can't determine branch (e.g. not a git repo) → don't block. Fail-open.
71
109
  if (branch === null)
72
- return this.failOpen(ctx, branch, 'branch-undeterminable');
110
+ return this.failOpen(ctx, branch, 'branch-undeterminable', this.treeSummary(judgedRoot));
111
+ // Mid-rebase / mid-bisect: `--abbrev-ref HEAD` prints the literal `HEAD`, there is no branch
112
+ // name, and therefore no key into the branch-keyed cache. Matrix row 14 — abstain, out loud.
113
+ if (branch === 'HEAD')
114
+ return this.failOpen(ctx, branch, 'detached-head', this.treeSummary(judgedRoot));
115
+ const judged = new judged_tree_1.JudgedTree(target, branch, target.governedRoot);
73
116
  // State 1: on main — synchronous, no cache needed.
74
117
  if (branch === 'main') {
75
- return this.block(ctx, branch, 'on-main', this.onMainMessage());
118
+ return this.block(ctx, branch, 'on-main', this.onMainMessage(judged), this.treeSummary(judgedRoot));
76
119
  }
77
- // Keep the cache warm for the next call. Detached; never blocks this edit.
78
- (0, main_sync_refresh_1.triggerMainSyncRefresh)(ctx.workspaceRoot, (0, main_sync_timeout_1.hangTimeoutOf)(this.config));
79
- const status = (0, rules_config_1.readMainSyncStatus)(ctx.workspaceRoot, branch);
120
+ // Keep the cache warm for the next call. Detached; never blocks this edit. Rooted at the JUDGED
121
+ // tree, so the refresher recomputes the entry this guard is about to read rather than a sibling's.
122
+ (0, main_sync_refresh_1.triggerMainSyncRefresh)(judgedRoot, (0, main_sync_timeout_1.hangTimeoutOf)(this.config));
123
+ const status = (0, rules_config_1.readMainSyncStatus)(judgedRoot, branch);
80
124
  // No cache yet (first edit of the session) → allow; the refresh we just spawned populates it
81
125
  // for the next call. Fail-open: never block on missing data.
82
126
  if (status === null)
83
127
  return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');
84
- const cache = this.cacheSummary(status);
128
+ const cache = `${this.treeSummary(judgedRoot)} ${this.cacheSummary(status)}`;
85
129
  // BELT-AND-BRACES since the cache became branch-keyed: we looked this entry up BY `branch`, so
86
130
  // a mismatch now means the map's key and the entry's own `branch` field disagree — a shape bug,
87
131
  // not a normal state. Kept (rather than deleted) so such a bug degrades to an allow instead of
@@ -91,16 +135,16 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
91
135
  // State 2: this feature branch was already merged into main.
92
136
  if (status.branchAlreadyMerged) {
93
137
  const pr = status.mergedPr !== '' ? status.mergedPr : '?';
94
- const merged = this.alreadyMergedMessage(ctx.workspaceRoot, branch, status.mergedPr);
138
+ const merged = this.alreadyMergedMessage(judged, status.mergedPr);
95
139
  return this.block(ctx, branch, `already-merged PR#${pr}`, merged, cache);
96
140
  }
97
141
  // State 3: no fork point — main was merged into the branch.
98
142
  if (!status.hasForkPoint) {
99
- return this.block(ctx, branch, 'no-fork-point', this.noForkPointMessage(branch), cache);
143
+ return this.block(ctx, branch, 'no-fork-point', this.noForkPointMessage(judged), cache);
100
144
  }
101
145
  // State 4: origin/main moved and touches files you changed.
102
146
  if (status.conflict) {
103
- return this.block(ctx, branch, 'main-moved-conflict', this.conflictMessage(status.conflictFiles, status.openPr), cache);
147
+ return this.block(ctx, branch, 'main-moved-conflict', this.conflictMessage(judged, status.conflictFiles, status.openPr), cache);
104
148
  }
105
149
  // NOT-MERGED, or NOT-ASKED? `branchAlreadyMerged: false` is produced both by "this branch has
106
150
  // no merged PR" and by "the forge could not be reached" (`gh` missing, unauthenticated,
@@ -114,6 +158,17 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
114
158
  return this.failOpen(ctx, branch, 'no-forge', cache);
115
159
  return this.allow(ctx, branch, 'clean-feature-branch', cache);
116
160
  }
161
+ /**
162
+ * WHICH TREE this decision was judged against, for the log.
163
+ *
164
+ * The trail already carried a `tree=` field, but it is stamped from the root the decision is LOGGED
165
+ * to (the session's), so during issue #851 every misfired block recorded `tree=primary` for a path
166
+ * under `.claude/worktrees/` and read as perfectly ordinary. This one is the root the verdict was
167
+ * actually computed from, so the two disagreeing is the defect, visible in one grep.
168
+ */
169
+ treeSummary(judgedRoot) {
170
+ return `judged=${judgedRoot}`;
171
+ }
117
172
  // One-line summary of the async-written cache that drove this decision, for the SYNC log — so a
118
173
  // wrong allow/block is traceable to the exact (possibly stale) main-sync-status.json read.
119
174
  cacheSummary(status) {
@@ -182,32 +237,51 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
182
237
  * uncommitted work onto the new branch. Three halves of one row printing one cure is the point:
183
238
  * a fragile cure is a defect here even when it often happens to work.
184
239
  */
185
- onMainMessage() {
240
+ onMainMessage(judged) {
186
241
  const convention = this.config.branchNamingConvention ?? '{whoami}/{featurename}';
187
242
  return [
243
+ judged.header(),
188
244
  'Blocked: this is a WRITE on main. Reading main to PLAN is fine — writing here is not, because the feature branch is the unit of work: reviewable, revertable, and not one `git checkout` from being lost.',
189
245
  'And if local main is BEHIND origin/main, what you read here was out of date too — so the fetch below is not an extra step, it is what makes your reads true.',
190
246
  'Branch off origin/main (it fetches first, and uncommitted work comes with you):',
191
247
  ' git fetch origin main && git checkout -b <new-branch> origin/main',
192
248
  `Name <new-branch> by the convention (from webpieces.config.json): ${convention}`,
193
249
  'Example: git fetch origin main && git checkout -b ' + convention.replace(/<[^>]+>/g, 'value') + ' origin/main',
250
+ judged.redirectNote([judged.cdCure('git fetch origin main && git checkout -b <new-branch> origin/main')]),
194
251
  ].join('\n');
195
252
  }
196
253
  // Shared with read-stale-guard, which blocks READS in this same state — see MergedBranchMessage.
197
254
  // The tree kind picks the flavour of the cure: a dead LINKED WORKTREE is told to open a new
198
255
  // worktree off origin/main and reap this one; the primary clone is told to branch off origin/main.
199
- alreadyMergedMessage(workspaceRoot, branch, mergedPr) {
200
- return new merged_branch_message_1.MergedBranchMessage(workspaceRoot).forEdits(branch, mergedPr, new tree_recovery_1.TreeRecovery().kindOf(workspaceRoot), workspaceRoot);
256
+ // Every one of those questions is asked of the JUDGED tree, not the session's: the branch that is
257
+ // merged, the checkout to recover, and the directory the cure has to run in are all properties of
258
+ // the tree that owns the file (issue #851).
259
+ alreadyMergedMessage(judged, mergedPr) {
260
+ const body = new merged_branch_message_1.MergedBranchMessage(judged.root).forEdits(judged.branch, mergedPr, new tree_recovery_1.TreeRecovery().kindOf(judged.root), judged.root);
261
+ // NO redirect note here, deliberately. MergedBranchMessage was built with the JUDGED root and
262
+ // already aims every command it prints through `atRoot` — and it picks the WORKTREE flavour of
263
+ // the cure (open a NEW worktree off origin/main and reap this dead one) when that is what the
264
+ // tree is. Appending a generic `checkout -b` would contradict the paragraph directly above it,
265
+ // in exactly the redirected-and-a-worktree case where the worktree flavour is the right advice.
266
+ return [judged.header(), body].join('\n');
201
267
  }
202
- conflictMessage(conflictFiles, openPr) {
268
+ conflictMessage(judged, conflictFiles, openPr) {
203
269
  const files = conflictFiles.length > 0
204
270
  ? conflictFiles.map((f) => ` - ${f}`).join('\n')
205
271
  : ' (see git diff)';
272
+ // NAME THE BRANCH AND THE TREE. Without them this read "files you also changed", listing files
273
+ // the edit had never touched on a branch it was not on, and there was nothing in the text to
274
+ // contradict — see JudgedTree.
206
275
  const header = [
207
- 'origin/main moved and touched files you also changed since your fork point:',
276
+ judged.header(),
277
+ `origin/main moved and touched files that \`${judged.branch}\` also changed since its fork point:`,
208
278
  files,
209
279
  '',
210
280
  ];
281
+ const redirect = judged.redirectNote([
282
+ judged.pnpmCure('wp-start-update'),
283
+ `${judged.pnpmCure('wp-start-upsert-pr')} (instead, when a PR is open)`,
284
+ ]);
211
285
  // Steer EARLY: if a PR already tracks this branch, the update-only flow would just fail-fast
212
286
  // (a 3-point update strands the PR on the old branch generation), so recommend ONLY the PR
213
287
  // flow and don't waste the AI's tokens on wp-start-update.
@@ -223,6 +297,7 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
223
297
  ])
224
298
  .concat(guidance.prFlow())
225
299
  .concat(['', ...guidance.whyPrForcesFlowB()])
300
+ .concat([redirect])
226
301
  .join('\n');
227
302
  }
228
303
  return header
@@ -232,14 +307,17 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
232
307
  '',
233
308
  ])
234
309
  .concat(guidance.flows())
310
+ .concat([redirect])
235
311
  .join('\n');
236
312
  }
237
- noForkPointMessage(branch) {
313
+ noForkPointMessage(judged) {
238
314
  return [
315
+ judged.header(),
239
316
  'No fork point with origin/main — main appears to have been merged into this branch,',
240
317
  'so a clean squash-merge is impossible. A human must redo the work on a fresh branch:',
241
318
  '',
242
- ...(0, rules_config_1.squashRecoverySteps)(branch),
319
+ ...(0, rules_config_1.squashRecoverySteps)(judged.branch),
320
+ judged.redirectNote([judged.pnpmCure('wp-start-update')]),
243
321
  ].join('\n');
244
322
  }
245
323
  }