@webpieces/ai-hook-rules 0.4.668 → 0.4.670

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":"l0-matrix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-matrix.ts"],"names":[],"mappings":"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","sourcesContent":["import { CONFIG_FILENAME, writeTemplate } from '@webpieces/rules-config';\n\nimport {\n ADD_HOOK_PKG_CMD, CHECKOUT_MAIN_PULL_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call,\n L0_ALLOWLIST, RECOVERY_CMD, RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, renderShim,\n} from '../bin/shim';\nimport { ENV_SURFACE, REGISTRATION_SURFACE } from '../bin/hook-registration';\nimport { shimStaleDenyReason } from '../bin/shim-deny-reason';\nimport {\n L0_FAULT_BIN_BROKEN, L0_FAULT_BIN_MISSING, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n L0_FAULT_DRIFT, L0_FAULT_NAMES, L0_FAULT_SHIM_STALE, L0_FAULT_UNDECLARED, L0FaultCode,\n L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED, l0GuardHeader, l0MatrixCitation,\n} from './l0-fault-codes';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// L0 — the TOOLING-INTEGRITY layer, as data.\n//\n// L0 is the outermost guard: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its faults are\n// enumerated in L0_FAULTS below and — drawn as a decision matrix — have NO genuine second dimension:\n//\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n//\n// so the only thing that varies per fault is the MESSAGE. This module holds the fault table and\n// renders it, together with L0_ALLOWLIST (../bin/shim), into webpieces.guard-matrix.md — the doc the\n// deny messages point the AI at. Doc and code come from the SAME arrays, so they cannot drift.\n// ---------------------------------------------------------------------------\n\n/**\n * The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,\n * and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.\n *\n * That generated doc is the AUTHORITY for the fault table and the allowlist (same arrays, cannot\n * drift). guards/L0-tooling.md is the hand-written companion: it adds L0's evaluation\n * ORDER, the use cases and the known gaps, and it documents L1, none of which are rendered from code.\n * Change L0_FAULTS or L0_ALLOWLIST and the generated doc follows automatically — guards/L0-tooling.md does\n * not, so update it in the same PR.\n */\nexport const GUARD_MATRIX_DOC = 'webpieces.guard-matrix.md';\n\n/**\n * One CURE for a fault: the exact call, plus the `mention` that must appear in that fault's deny text.\n *\n * Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny\n * message must actually name it. That pairing is the anti-deadlock invariant — a message that\n * prescribes a command the allowlist rejects is exactly the shape of the three deadlocks CLAUDE.md\n * records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.\n */\nexport class L0Cure {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly mention: string,\n readonly call: L0Call,\n /**\n * The one to reach for first when a fault has several. Exactly one cure per fault carries it,\n * so the rendered Fix section never asks the reader to choose between equals.\n */\n readonly preferred: boolean,\n /**\n * WHEN to pick this cure over its siblings — the sentence that makes a list of commands\n * actionable instead of a menu (\"when the PIN is the stale side\", not \"an alternative\").\n */\n readonly discriminator: string,\n ) {}\n\n /** A Bash cure renders as a literal command; a tool-shaped one renders as the edit it stands for. */\n isCommand(): boolean {\n return this.call.toolName === 'Bash';\n }\n}\n\n/** One L0 fault. Data-only → a class, per CLAUDE.md. */\nexport class L0Fault {\n constructor(\n /**\n * The codebook's letter — typed as the UNION of every declared code, not `string`, so\n * `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a\n * `?? 'unknown'` fallback. A fault added without a name fails to compile.\n */\n readonly code: L0FaultCode,\n readonly name: string,\n readonly detectedBy: string,\n readonly enforcedIn: string,\n readonly cures: readonly L0Cure[],\n /**\n * The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for\n * D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —\n * the same bytes the consumer runs, which is what the mention assertion needs to search.\n */\n readonly denyText: string,\n ) {}\n}\n\n// The deny for fault C, and the ONLY message L0 has for a repo with no webpieces.config.json at all.\n// Moved here from runner.ts so the fault table and the runner cannot state different cures.\n//\n// It used to name `./node_modules/.bin/wp-setup-ai-hooks` — a bin that HAS NOT EXISTED since it was\n// renamed to wp-install-ai-hooks. So the one command this deny prescribed was (a) not installable and\n// (b) not on the L0 allowlist in that spelling, i.e. the AI was handed a cure it could neither run nor\n// get past the guard. Now it names the installer that actually seeds the config AND is entry 8 of the\n// allowlist, and it says out loud that writing the config yourself is allowed through.\n//\n// ORDERING (2026-08-02): writing the file yourself now LEADS. The bare installer used to be OPTION 1,\n// but it seeds the config and then PROMPTS twice for a hook target, which hangs a non-interactive\n// agent. Writing the file is the one cure that always works, and it is the same cure every other config\n// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every\n// error at once, so the write/validate loop converges in a couple of passes.\nexport const CONFIG_MISSING_REPORT = [\n `❌ webpieces ai-hooks blocked this call: ${CONFIG_FILENAME} not found.`,\n '',\n l0GuardHeader(L0_FAULT_CONFIG_MISSING, '1 violation'),\n ` ${CONFIG_FILENAME}`,\n ' → the webpieces guards cannot run without it, so every OTHER tool call is blocked.',\n ` → ${l0MatrixCitation(L0_FAULT_CONFIG_MISSING)}`,\n '',\n 'Still allowed while this block is up:',\n ' - any Read',\n ` - any Write/Edit whose target is ${CONFIG_FILENAME}`,\n ' - every command on the L0 allowlist, including the Fix Options below',\n ' THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human.',\n '',\n ` Fix Option 1: (preferred) it needs no other tool and it never prompts - create ${CONFIG_FILENAME}`,\n ' yourself. The validator reports EVERY missing/invalid entry at once (each with the snippet to',\n ' paste), so a minimal first draft converges in about two passes.',\n ' Fix Option 2: pick this ONLY at an interactive terminal where you can answer its two prompts - it',\n ' goes on to wire the Claude Code hooks and asks for a target twice, which hangs a non-interactive',\n ' session.',\n ' run EXACTLY: `pnpm exec wp-install-ai-hooks`',\n '',\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.',\n].join('\\n');\n\n// The HEADER of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail as the `[…]` block's offenders). Kept here so the fault table quotes the same text the runner\n// emits, and so Y opens with the same `[guard-name] (layer=L0 fault=Y row=3)` coordinates as every\n// other L0 fault — that triple is what joins the deny to the audit line and to the matrix row.\nexport const CONFIG_OUT_OF_SYNC_HEADER = [\n `❌ webpieces ai-hooks blocked this call: ${CONFIG_FILENAME} is out of sync.`,\n '',\n l0GuardHeader(L0_FAULT_CONFIG_OUT_OF_SYNC, '1 violation'),\n ` new built-in rules are present that have no entry in ${CONFIG_FILENAME}`,\n ` → ${l0MatrixCitation(L0_FAULT_CONFIG_OUT_OF_SYNC)}`,\n].join('\\n');\n\n// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation\n// invariant in guards/L0-tooling.md: every config problem cures to \"make the file right\", the validator\n// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits\n// this edit unconditionally. (That section is the authority — do not restate its reasoning here.)\nconst CONFIG_WRITE_CURE = new L0Cure(\n CONFIG_FILENAME, new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`), true,\n 'this fault fires at all — it is the only cure that needs no other tool, and it is never denied',\n);\n\n// Cure calls are spelled exactly as the deny messages spell them, so the mention assertion is a real\n// string search rather than a paraphrase.\n// webpieces-disable no-function-outside-class -- pure constructor helper for the L0_FAULTS literal below, in this data module\nfunction bashCure(command: string, preferred: boolean, discriminator: string): L0Cure {\n return new L0Cure(command, new L0Call('Bash', command, ''), preferred, discriminator);\n}\n\n/**\n * THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a\n * stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside\n * the bin, in JS. One model, two enforcement points.\n */\nexport const L0_FAULTS: readonly L0Fault[] = [\n new L0Fault(L0_FAULT_DRIFT, 'version drift — root package.json pin != installed version',\n 'sh, before the bin runs', 'sh',\n [\n // `pnpm install` clears D in BOTH directions — it makes installed == pin by definition — so\n // it is always the preferred cure. The direction only decides whether the PIN is the version\n // you WANT, which is what the second cure is for.\n bashCure('pnpm install', true,\n 'node_modules is OLDER than the pin, OR you are on a feature branch and want YOUR '\n + 'branch pin (usually the case) — it always clears the drift'),\n // The on-main sync is spelled `git checkout main && git pull origin main` and NOT `git pull`:\n // a raw pull on a FEATURE branch merges main into it and destroys the fork point, so it is\n // no longer on the L0 allowlist at all (see CHECKOUT_MAIN_PULL_BODY_ERE). This spelling ends\n // ON main, which is why it is safe from any branch — and it is a no-op checkout when you are\n // already there.\n bashCure(CHECKOUT_MAIN_PULL_CMD, false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'sync first and install second; a bare install would downgrade you'),\n ], renderShim()),\n new L0Fault(L0_FAULT_BIN_MISSING, 'guard bin missing (fresh clone / new worktree / package removed)',\n 'sh, before the bin runs', 'sh',\n [bashCure('pnpm install', true,\n 'this fault fires at all — nothing is installed in THIS tree, and a new git worktree '\n + 'copies no node_modules')],\n renderShim()),\n // U is X with the ONE input that inverts X's cure, which is why it is a separate fault and not a\n // sentence inside X's message: when nothing declares the package, `pnpm install` is not a weaker fix,\n // it is a PROVABLE no-op, and an agent that trusts the X text will run it until it gives up. See the\n // ADD_HOOK_PKG entry in l0-allowlist.ts for the incident.\n new L0Fault(L0_FAULT_UNDECLARED, `guard bin missing AND ${HOOK_PKG} is not declared in package.json`,\n 'sh, before the bin runs', 'sh',\n [bashCure(ADD_HOOK_PKG_CMD, true,\n 'this fault fires at all — package.json asks for nothing, so pnpm install reports '\n + '\"Lockfile is up to date\" and leaves the tree exactly as broken as it found it')],\n renderShim()),\n new L0Fault(L0_FAULT_BIN_BROKEN, 'guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)',\n 'sh, before the bin runs', 'sh',\n [bashCure(RECOVERY_CMD, true,\n 'this fault fires at all — a BARE pnpm install SKIPS the corrupt package, because pnpm sees '\n + 'the right version on disk and considers it installed; only the delete forces a rewrite')],\n renderShim()),\n // S covers the WHOLE managed hook surface, not just the shim: the committed ai-hook.sh, the\n // .claude/settings.json entries that register them AND the managed `env` entry that pins the Bash\n // cwd so the hooks resolve identically for every subagent. They only work as a set — a settings file left\n // on a superseded form silently changes who governs, re-pinning every tree to the\n // primary's release — and nothing validated the registration at all before it joined this fault.\n new L0Fault(L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file, the .claude/settings.json registration or its managed env entry does not match this release',\n 'the guard bin', 'JS',\n [\n // wp-upgrade-shim leads because it is the ONLY cure that repairs all three, and it is\n // still surgical: it rewrites ai-hook.sh, the registration and the managed env entry\n // and touches no config, and it imports only fs/path so it runs on a tree too broken to load\n // the rule engine. The INSTALLER is deliberately NOT a cure here: it also migrates the config\n // and prompts for a target twice, which hangs a non-interactive agent.\n bashCure(UPGRADE_SHIM_CMD, true,\n 'this fault fires at all — it is the only cure that repairs all three managed things '\n + '(ai-hook.sh, the settings.json registration and its managed env entry), and it also '\n + 'deletes the retired guarantee-root.sh and any entry still naming it, and it '\n + 'touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer'),\n // 2026-07-21: the version gap below caused a real \"command not found\" deadlock.\n bashCure(RESTORE_SHIM_CMD, false,\n 'the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does '\n + 'not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade '\n + '@webpieces afterwards and run Option 1 to finish'),\n ],\n shimStaleDenyReason('', '', [SHIM_MARKER, REGISTRATION_SURFACE, ENV_SURFACE], false)),\n new L0Fault(L0_FAULT_CONFIG_MISSING, `${CONFIG_FILENAME} missing`,\n 'the guard bin', 'JS',\n [\n CONFIG_WRITE_CURE,\n // Kept, but demoted: it seeds the file and then PROMPTS twice for a hook target.\n bashCure(INSTALL_HOOKS_CMD, false,\n 'you are at an INTERACTIVE terminal and can answer its two hook-target prompts'),\n ], CONFIG_MISSING_REPORT),\n new L0Fault(L0_FAULT_CONFIG_OUT_OF_SYNC, `a loaded rule has no ${CONFIG_FILENAME} key`,\n 'the guard bin', 'JS',\n [CONFIG_WRITE_CURE], CONFIG_OUT_OF_SYNC_HEADER),\n];\n\n/**\n * One fault's FIX section, rendered from its `cures` array — literal commands only, never prose.\n *\n * This is the half that used to live in hand-written docs and drift. The three fields of L0Cure map\n * onto the three things a blocked reader needs and nothing else: WHAT to type (the call), WHETHER it is\n * the default (preferred), and WHEN to pick a sibling instead (discriminator). A cure with no\n * discriminator would render as a menu of equals, which is how an agent picks the wrong one.\n */\n// webpieces-disable no-function-outside-class -- pure string builder for renderGuardMatrixDoc below, beside the arrays it reads\nfunction renderFixSection(fault: L0Fault): string[] {\n const options = fault.cures.map((cure: L0Cure, i: number): string => {\n // A Bash cure is the command verbatim; a tool-shaped one is the file it edits (allowlist entry 2).\n const literal = cure.isCommand() ? `\\`${cure.call.command}\\`` : `edit \\`${cure.mention}\\` yourself`;\n const label = cure.preferred ? `Option ${i + 1} (preferred)` : `Option ${i + 1}`;\n return `- **${label}**: ${literal} ← pick this when ${cure.discriminator}`;\n });\n return [`### \\`${fault.code}\\` — ${fault.name}`, '', ...options, ''];\n}\n\n/**\n * Render webpieces.guard-matrix.md from L0_FAULTS + L0_ALLOWLIST.\n *\n * A unit test locks the committed template byte-identical to this output, the same way\n * templates/ai-hook.sh is locked to renderShim(). That is what makes the doc assertable instead of\n * aspirational: the table in the doc IS the array the guard consults.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over the two exported arrays, beside them in this module\nexport function renderGuardMatrixDoc(): string {\n return [\n '# webpieces guard matrix — L0 (tooling integrity)',\n '',\n 'GENERATED from `L0_FAULTS` + `L0_ALLOWLIST` in `@webpieces/ai-hook-rules`. Do not hand-edit —',\n 'a unit test locks this file byte-identical to `renderGuardMatrixDoc()`, so the table below is',\n 'the array the guard actually consults, not a description of it.',\n '',\n 'L0 is the OUTERMOST guard layer. It blocks work while `node_modules`, the committed shim, or',\n '`webpieces.config.json` are in a state that makes every other guard untrustworthy. If you are',\n 'reading this, one of the faults below fired and named this file.',\n '',\n '## The faults',\n '',\n 'THE JOIN KEYS ARE `guard`, `fault=` and `row=`. Every L0 deny opens',\n '`[<guard>] (layer=L0 fault=<code> row=3, …)`, and every audit line — from BOTH halves of L0, the',\n '`sh` shim and the guard bin — carries `layer=L0 row=<n> fault=<code>`. So one grep lands you in',\n 'the deny, the log line and the row below. The guard names come from `L0_FAULT_NAMES` and the row',\n 'numbers from `L0_ROW_*`, both spelled in exactly one place (`core/l0-fault-codes.ts`).',\n '',\n '| code | guard | fault | detected by | enforced in |',\n '|---|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string =>\n `| \\`${f.code}\\` | \\`${L0_FAULT_NAMES[f.code]}\\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),\n '',\n 'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',\n 'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',\n '',\n '## The fix, per fault',\n '',\n 'Every command below is rendered from that fault\\'s `cures` array and is asserted, by unit test,',\n 'to be accepted by `isAllowed()` — so nothing here can be a command the guard then rejects. Type',\n 'the option you pick EXACTLY as written and run nothing else on that line.',\n '',\n ...L0_FAULTS.flatMap(renderFixSection),\n ...renderMatrixAndAllowlist(),\n ].join('\\n');\n}\n\n/**\n * The second half of the doc: the three-row matrix and the ONE allowlist. Split out of\n * renderGuardMatrixDoc solely to keep it inside the method-line budget — the join order is what makes\n * the two halves one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- second half of renderGuardMatrixDoc's string, beside it in this module\nfunction renderMatrixAndAllowlist(): string[] {\n return [\n '## The matrix',\n '',\n 'L0 has NO genuine second dimension. Every branch reduces to one question:',\n '',\n '| # | fault | on the allowlist? | outcome |',\n '|---|---|---|---|',\n '| 1 | none | — | hand down to the next guard layer |',\n `| ${L0_ROW_ALLOWLISTED} | any | yes | PASS or ALLOW (see the entry) |`,\n `| ${L0_ROW_BLOCKED} | any | no | BLOCK — **only the message varies by fault** |`,\n '',\n `Row ${L0_ROW_BLOCKED} is the only row that blocks, so every L0 deny cites it — \\`row=${L0_ROW_BLOCKED}\\` in the`,\n 'deny header, `row=' + L0_ROW_BLOCKED + '` on the audit line, and this row here. Same numbers as L1 uses for',\n 'its own rows (see `L1_ROWS`), and for the same reason: a row number is IDENTITY, so it is never reused.',\n '',\n 'The tool is not a dimension either: \"any Read\" is an allowlist ENTRY, not a tool check.',\n '',\n '## The allowlist',\n '',\n 'ONE list, consulted identically by every fault. A cure that cannot help a given fault also',\n 'cannot hurt it, and gating each entry on a fault is what produced four real defects (a stale',\n 'shim that denied `pnpm install` and every git sync; faults that denied every Read; a config fault',\n 'that denied `rm -rf node_modules && pnpm install` while allowing a bare `pnpm install`).',\n '',\n '| # | allowed | outcome |',\n '|---|---|---|',\n ...L0_ALLOWLIST.map((e: L0AllowEntry, i: number): string => `| ${i + 1} | ${e.label} | ${e.kind.toUpperCase()} |`),\n '',\n '- **PASS** — L0 has no objection; the call falls THROUGH so the downstream guards still judge it.',\n '- **ALLOW** — terminal; bypasses everything, because a cure must stay reachable even when a',\n ' downstream guard would block it.',\n '',\n 'Every Bash entry is anchored to the WHOLE command. A leading `cd <dir> &&`, a trailing `2>&1`',\n 'and a pipe into `tail`/`head` are tolerated; nothing else is. Appending `&& git status` makes it',\n 'a DIFFERENT command and it is rejected again — that is not the guard refusing its own cure.',\n '',\n '`git merge` is deliberately NOT on this list. Main is merged ONLY through the 3-point fork merge',\n '(`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is already open).',\n '',\n '## Known asymmetry',\n '',\n 'Under `S`/`C`/`Y` the guard bin IS running, so a PASS really does fall through to the downstream',\n 'guards. Under `D`/`X`/`U`/`K` the bin is never executed, so there is nothing to fall through to and a',\n 'PASS degenerates into a terminal allow — reads are unguarded during those three faults.',\n '',\n '## Widening L0',\n '',\n 'Add an entry to `L0_ALLOWLIST` in `packages/tooling/ai-hook-rules/src/bin/shim.ts`. That array is',\n 'the single source for the JS allowlist, the `grep -E` inside the rendered shim, and this file.',\n '',\n ];\n}\n\n/**\n * Drop the matrix doc where the AI can read it, and return its absolute path ('' if it could not be\n * written). Called from the L0 BLOCK path so the deny can say `READ <path>`.\n *\n * Best-effort by design: this runs while the tree is already known-broken, and a missing template (an\n * @webpieces/rules-config older than this package) must degrade the deny message, never replace it\n * with a crash.\n */\n// webpieces-disable no-function-outside-class -- sibling of renderGuardMatrixDoc in this module\nexport function writeGuardMatrixDoc(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return writeTemplate(workspaceRoot, GUARD_MATRIX_DOC);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: no doc → the deny simply omits the pointer\n return '';\n }\n}\n\n/**\n * The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written.\n *\n * It opens with a NEWLINE, not a space: the JS-side L0 denies render in the house format now (a header,\n * a `[guard-name]` block, `Fix Option N:` lines), so a pointer glued onto the end of the last line would\n * be the one place the shape broke. A real newline is safe on both call paths — denyJson() JSON.stringifies\n * it, exactly as it does for every multi-line L1 report.\n */\n// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module\nexport function guardMatrixPointer(docPath: string): string {\n if (docPath === '') return '';\n return `\\nThe full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;\n}\n"]}
1
+ {"version":3,"file":"l0-matrix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-matrix.ts"],"names":[],"mappings":"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","sourcesContent":["import { CONFIG_FILENAME, writeTemplate } from '@webpieces/rules-config';\n\nimport {\n ADD_HOOK_PKG_CMD, CHECKOUT_MAIN_PULL_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call,\n L0_ALLOWLIST, RECOVERY_CMD, RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, renderShim,\n} from '../bin/shim';\nimport { ENV_SURFACE, REGISTRATION_SURFACE } from '../bin/hook-registration';\nimport { shimStaleDenyReason } from '../bin/shim-deny-reason';\nimport {\n L0_FAULT_BIN_BROKEN, L0_FAULT_BIN_MISSING, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n L0_FAULT_DRIFT, L0_FAULT_NAMES, L0_FAULT_SHIM_STALE, L0_FAULT_UNDECLARED, L0FaultCode,\n L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED, l0GuardHeader, l0MatrixCitation,\n} from './l0-fault-codes';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// L0 — the TOOLING-INTEGRITY layer, as data.\n//\n// L0 is the outermost guard: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its faults are\n// enumerated in L0_FAULTS below and — drawn as a decision matrix — have NO genuine second dimension:\n//\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n//\n// so the only thing that varies per fault is the MESSAGE. This module holds the fault table and\n// renders it, together with L0_ALLOWLIST (../bin/shim), into webpieces.guard-matrix.md — the doc the\n// deny messages point the AI at. Doc and code come from the SAME arrays, so they cannot drift.\n// ---------------------------------------------------------------------------\n\n/**\n * The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,\n * and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.\n *\n * That generated doc is the AUTHORITY for the fault table and the allowlist (same arrays, cannot\n * drift). guards/L0-tooling.md is the hand-written companion: it adds L0's evaluation\n * ORDER, the use cases and the known gaps, and it documents L1, none of which are rendered from code.\n * Change L0_FAULTS or L0_ALLOWLIST and the generated doc follows automatically — guards/L0-tooling.md does\n * not, so update it in the same PR.\n */\nexport const GUARD_MATRIX_DOC = 'webpieces.guard-matrix.md';\n\n/**\n * One CURE for a fault: the exact call, plus the `mention` that must appear in that fault's deny text.\n *\n * Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny\n * message must actually name it. That pairing is the anti-deadlock invariant — a message that\n * prescribes a command the allowlist rejects is exactly the shape of the three deadlocks CLAUDE.md\n * records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.\n */\nexport class L0Cure {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly mention: string,\n readonly call: L0Call,\n /**\n * The one to reach for first when a fault has several. Exactly one cure per fault carries it,\n * so the rendered Fix section never asks the reader to choose between equals.\n */\n readonly preferred: boolean,\n /**\n * WHEN to pick this cure over its siblings — the sentence that makes a list of commands\n * actionable instead of a menu (\"when the PIN is the stale side\", not \"an alternative\").\n */\n readonly discriminator: string,\n ) {}\n\n /** A Bash cure renders as a literal command; a tool-shaped one renders as the edit it stands for. */\n isCommand(): boolean {\n return this.call.toolName === 'Bash';\n }\n}\n\n/** One L0 fault. Data-only → a class, per CLAUDE.md. */\nexport class L0Fault {\n constructor(\n /**\n * The codebook's letter — typed as the UNION of every declared code, not `string`, so\n * `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a\n * `?? 'unknown'` fallback. A fault added without a name fails to compile.\n */\n readonly code: L0FaultCode,\n readonly name: string,\n readonly detectedBy: string,\n readonly enforcedIn: string,\n readonly cures: readonly L0Cure[],\n /**\n * The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for\n * D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —\n * the same bytes the consumer runs, which is what the mention assertion needs to search.\n */\n readonly denyText: string,\n ) {}\n}\n\n// The deny for fault C, and the ONLY message L0 has for a repo with no webpieces.config.json at all.\n// Moved here from runner.ts so the fault table and the runner cannot state different cures.\n//\n// It used to name `./node_modules/.bin/wp-setup-ai-hooks` — a bin that HAS NOT EXISTED since it was\n// renamed to wp-install-ai-hooks. So the one command this deny prescribed was (a) not installable and\n// (b) not on the L0 allowlist in that spelling, i.e. the AI was handed a cure it could neither run nor\n// get past the guard. Now it names the installer that actually seeds the config AND is entry 8 of the\n// allowlist, and it says out loud that writing the config yourself is allowed through.\n//\n// ORDERING (2026-08-02): writing the file yourself now LEADS. The bare installer used to be OPTION 1,\n// but it seeds the config and then PROMPTS twice for a hook target, which hangs a non-interactive\n// agent. Writing the file is the one cure that always works, and it is the same cure every other config\n// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every\n// error at once, so the write/validate loop converges in a couple of passes.\nexport const CONFIG_MISSING_REPORT = [\n `❌ webpieces ai-hooks blocked this call: ${CONFIG_FILENAME} not found.`,\n '',\n l0GuardHeader(L0_FAULT_CONFIG_MISSING, '1 violation'),\n ` ${CONFIG_FILENAME}`,\n ' → the webpieces guards cannot run without it, so every OTHER tool call is blocked.',\n ` → ${l0MatrixCitation(L0_FAULT_CONFIG_MISSING)}`,\n '',\n 'Still allowed while this block is up:',\n ' - any Read',\n ` - any Write/Edit whose target is ${CONFIG_FILENAME}`,\n ' - every command on the L0 allowlist, including the Fix Options below',\n ' THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human.',\n '',\n ` Fix Option 1: (preferred) it needs no other tool and it never prompts - create ${CONFIG_FILENAME}`,\n ' yourself. The validator reports EVERY missing/invalid entry at once (each with the snippet to',\n ' paste), so a minimal first draft converges in about two passes.',\n ' Fix Option 2: pick this ONLY at an interactive terminal where you can answer its two prompts - it',\n ' goes on to wire the Claude Code hooks and asks for a target twice, which hangs a non-interactive',\n ' session.',\n ' run EXACTLY: `pnpm exec wp-install-ai-hooks`',\n '',\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.',\n].join('\\n');\n\n// The HEADER of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail as the `[…]` block's offenders). Kept here so the fault table quotes the same text the runner\n// emits, and so Y opens with the same `[guard-name] (layer=L0 fault=Y row=3)` coordinates as every\n// other L0 fault — that triple is what joins the deny to the audit line and to the matrix row.\nexport const CONFIG_OUT_OF_SYNC_HEADER = [\n `❌ webpieces ai-hooks blocked this call: ${CONFIG_FILENAME} is out of sync.`,\n '',\n l0GuardHeader(L0_FAULT_CONFIG_OUT_OF_SYNC, '1 violation'),\n ` new built-in rules are present that have no entry in ${CONFIG_FILENAME}`,\n ` → ${l0MatrixCitation(L0_FAULT_CONFIG_OUT_OF_SYNC)}`,\n].join('\\n');\n\n// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation\n// invariant in guards/L0-tooling.md: every config problem cures to \"make the file right\", the validator\n// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits\n// this edit unconditionally. (That section is the authority — do not restate its reasoning here.)\nconst CONFIG_WRITE_CURE = new L0Cure(\n CONFIG_FILENAME, new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`), true,\n 'this fault fires at all — it is the only cure that needs no other tool, and it is never denied',\n);\n\n// Cure calls are spelled exactly as the deny messages spell them, so the mention assertion is a real\n// string search rather than a paraphrase.\n// webpieces-disable no-function-outside-class -- pure constructor helper for the L0_FAULTS literal below, in this data module\nfunction bashCure(command: string, preferred: boolean, discriminator: string): L0Cure {\n return new L0Cure(command, new L0Call('Bash', command, ''), preferred, discriminator);\n}\n\n/**\n * THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a\n * stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside\n * the bin, in JS. One model, two enforcement points.\n */\nexport const L0_FAULTS: readonly L0Fault[] = [\n new L0Fault(L0_FAULT_DRIFT, 'version drift — root package.json pin != installed version',\n 'sh, before the bin runs', 'sh',\n [\n // `pnpm install` clears D in BOTH directions — it makes installed == pin by definition — so\n // it is always the preferred cure. The direction only decides whether the PIN is the version\n // you WANT, which is what the second cure is for.\n bashCure('pnpm install', true,\n 'node_modules is OLDER than the pin, OR you are on a feature branch and want YOUR '\n + 'branch pin (usually the case) — it always clears the drift'),\n // The on-main sync is spelled `git checkout main && git pull origin main` and NOT `git pull`:\n // a raw pull on a FEATURE branch merges main into it and destroys the fork point, so it is\n // no longer on the L0 allowlist at all (see CHECKOUT_MAIN_PULL_BODY_ERE). This spelling ends\n // ON main, which is why it is safe from any branch — and it is a no-op checkout when you are\n // already there.\n //\n // And it is RAW GIT on purpose, where the workflow guards now say `pnpm wp-checkout-clean-main`\n // instead. The fault being cured here is `node_modules` disagreeing with the pin, so\n // `node_modules` is the untrustworthy thing — and every `pnpm wp-*` bin resolves through it.\n // An L0 cure may never be a command that has to load the package it is repairing. See the\n // long note on CHECKOUT_MAIN_PULL_BODY_ERE in bin/l0-allowlist.ts.\n bashCure(CHECKOUT_MAIN_PULL_CMD, false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'sync first and install second; a bare install would downgrade you'),\n ], renderShim()),\n new L0Fault(L0_FAULT_BIN_MISSING, 'guard bin missing (fresh clone / new worktree / package removed)',\n 'sh, before the bin runs', 'sh',\n [bashCure('pnpm install', true,\n 'this fault fires at all — nothing is installed in THIS tree, and a new git worktree '\n + 'copies no node_modules')],\n renderShim()),\n // U is X with the ONE input that inverts X's cure, which is why it is a separate fault and not a\n // sentence inside X's message: when nothing declares the package, `pnpm install` is not a weaker fix,\n // it is a PROVABLE no-op, and an agent that trusts the X text will run it until it gives up. See the\n // ADD_HOOK_PKG entry in l0-allowlist.ts for the incident.\n new L0Fault(L0_FAULT_UNDECLARED, `guard bin missing AND ${HOOK_PKG} is not declared in package.json`,\n 'sh, before the bin runs', 'sh',\n [bashCure(ADD_HOOK_PKG_CMD, true,\n 'this fault fires at all — package.json asks for nothing, so pnpm install reports '\n + '\"Lockfile is up to date\" and leaves the tree exactly as broken as it found it')],\n renderShim()),\n new L0Fault(L0_FAULT_BIN_BROKEN, 'guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)',\n 'sh, before the bin runs', 'sh',\n [bashCure(RECOVERY_CMD, true,\n 'this fault fires at all — a BARE pnpm install SKIPS the corrupt package, because pnpm sees '\n + 'the right version on disk and considers it installed; only the delete forces a rewrite')],\n renderShim()),\n // S covers the WHOLE managed hook surface, not just the shim: the committed ai-hook.sh, the\n // .claude/settings.json entries that register them AND the managed `env` entry that pins the Bash\n // cwd so the hooks resolve identically for every subagent. They only work as a set — a settings file left\n // on a superseded form silently changes who governs, re-pinning every tree to the\n // primary's release — and nothing validated the registration at all before it joined this fault.\n new L0Fault(L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file, the .claude/settings.json registration or its managed env entry does not match this release',\n 'the guard bin', 'JS',\n [\n // wp-upgrade-shim leads because it is the ONLY cure that repairs all three, and it is\n // still surgical: it rewrites ai-hook.sh, the registration and the managed env entry\n // and touches no config, and it imports only fs/path so it runs on a tree too broken to load\n // the rule engine. The INSTALLER is deliberately NOT a cure here: it also migrates the config\n // and prompts for a target twice, which hangs a non-interactive agent.\n bashCure(UPGRADE_SHIM_CMD, true,\n 'this fault fires at all — it is the only cure that repairs all three managed things '\n + '(ai-hook.sh, the settings.json registration and its managed env entry), and it also '\n + 'deletes the retired guarantee-root.sh and any entry still naming it, and it '\n + 'touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer'),\n // 2026-07-21: the version gap below caused a real \"command not found\" deadlock.\n bashCure(RESTORE_SHIM_CMD, false,\n 'the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does '\n + 'not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade '\n + '@webpieces afterwards and run Option 1 to finish'),\n ],\n shimStaleDenyReason('', '', [SHIM_MARKER, REGISTRATION_SURFACE, ENV_SURFACE], false)),\n new L0Fault(L0_FAULT_CONFIG_MISSING, `${CONFIG_FILENAME} missing`,\n 'the guard bin', 'JS',\n [\n CONFIG_WRITE_CURE,\n // Kept, but demoted: it seeds the file and then PROMPTS twice for a hook target.\n bashCure(INSTALL_HOOKS_CMD, false,\n 'you are at an INTERACTIVE terminal and can answer its two hook-target prompts'),\n ], CONFIG_MISSING_REPORT),\n new L0Fault(L0_FAULT_CONFIG_OUT_OF_SYNC, `a loaded rule has no ${CONFIG_FILENAME} key`,\n 'the guard bin', 'JS',\n [CONFIG_WRITE_CURE], CONFIG_OUT_OF_SYNC_HEADER),\n];\n\n/**\n * One fault's FIX section, rendered from its `cures` array — literal commands only, never prose.\n *\n * This is the half that used to live in hand-written docs and drift. The three fields of L0Cure map\n * onto the three things a blocked reader needs and nothing else: WHAT to type (the call), WHETHER it is\n * the default (preferred), and WHEN to pick a sibling instead (discriminator). A cure with no\n * discriminator would render as a menu of equals, which is how an agent picks the wrong one.\n */\n// webpieces-disable no-function-outside-class -- pure string builder for renderGuardMatrixDoc below, beside the arrays it reads\nfunction renderFixSection(fault: L0Fault): string[] {\n const options = fault.cures.map((cure: L0Cure, i: number): string => {\n // A Bash cure is the command verbatim; a tool-shaped one is the file it edits (allowlist entry 2).\n const literal = cure.isCommand() ? `\\`${cure.call.command}\\`` : `edit \\`${cure.mention}\\` yourself`;\n const label = cure.preferred ? `Option ${i + 1} (preferred)` : `Option ${i + 1}`;\n return `- **${label}**: ${literal} ← pick this when ${cure.discriminator}`;\n });\n return [`### \\`${fault.code}\\` — ${fault.name}`, '', ...options, ''];\n}\n\n/**\n * Render webpieces.guard-matrix.md from L0_FAULTS + L0_ALLOWLIST.\n *\n * A unit test locks the committed template byte-identical to this output, the same way\n * templates/ai-hook.sh is locked to renderShim(). That is what makes the doc assertable instead of\n * aspirational: the table in the doc IS the array the guard consults.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over the two exported arrays, beside them in this module\nexport function renderGuardMatrixDoc(): string {\n return [\n '# webpieces guard matrix — L0 (tooling integrity)',\n '',\n 'GENERATED from `L0_FAULTS` + `L0_ALLOWLIST` in `@webpieces/ai-hook-rules`. Do not hand-edit —',\n 'a unit test locks this file byte-identical to `renderGuardMatrixDoc()`, so the table below is',\n 'the array the guard actually consults, not a description of it.',\n '',\n 'L0 is the OUTERMOST guard layer. It blocks work while `node_modules`, the committed shim, or',\n '`webpieces.config.json` are in a state that makes every other guard untrustworthy. If you are',\n 'reading this, one of the faults below fired and named this file.',\n '',\n '## The faults',\n '',\n 'THE JOIN KEYS ARE `guard`, `fault=` and `row=`. Every L0 deny opens',\n '`[<guard>] (layer=L0 fault=<code> row=3, …)`, and every audit line — from BOTH halves of L0, the',\n '`sh` shim and the guard bin — carries `layer=L0 row=<n> fault=<code>`. So one grep lands you in',\n 'the deny, the log line and the row below. The guard names come from `L0_FAULT_NAMES` and the row',\n 'numbers from `L0_ROW_*`, both spelled in exactly one place (`core/l0-fault-codes.ts`).',\n '',\n '| code | guard | fault | detected by | enforced in |',\n '|---|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string =>\n `| \\`${f.code}\\` | \\`${L0_FAULT_NAMES[f.code]}\\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),\n '',\n 'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',\n 'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',\n '',\n '## The fix, per fault',\n '',\n 'Every command below is rendered from that fault\\'s `cures` array and is asserted, by unit test,',\n 'to be accepted by `isAllowed()` — so nothing here can be a command the guard then rejects. Type',\n 'the option you pick EXACTLY as written and run nothing else on that line.',\n '',\n ...L0_FAULTS.flatMap(renderFixSection),\n ...renderMatrixAndAllowlist(),\n ].join('\\n');\n}\n\n/**\n * The second half of the doc: the three-row matrix and the ONE allowlist. Split out of\n * renderGuardMatrixDoc solely to keep it inside the method-line budget — the join order is what makes\n * the two halves one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- second half of renderGuardMatrixDoc's string, beside it in this module\nfunction renderMatrixAndAllowlist(): string[] {\n return [\n '## The matrix',\n '',\n 'L0 has NO genuine second dimension. Every branch reduces to one question:',\n '',\n '| # | fault | on the allowlist? | outcome |',\n '|---|---|---|---|',\n '| 1 | none | — | hand down to the next guard layer |',\n `| ${L0_ROW_ALLOWLISTED} | any | yes | PASS or ALLOW (see the entry) |`,\n `| ${L0_ROW_BLOCKED} | any | no | BLOCK — **only the message varies by fault** |`,\n '',\n `Row ${L0_ROW_BLOCKED} is the only row that blocks, so every L0 deny cites it — \\`row=${L0_ROW_BLOCKED}\\` in the`,\n 'deny header, `row=' + L0_ROW_BLOCKED + '` on the audit line, and this row here. Same numbers as L1 uses for',\n 'its own rows (see `L1_ROWS`), and for the same reason: a row number is IDENTITY, so it is never reused.',\n '',\n 'The tool is not a dimension either: \"any Read\" is an allowlist ENTRY, not a tool check.',\n '',\n '## The allowlist',\n '',\n 'ONE list, consulted identically by every fault. A cure that cannot help a given fault also',\n 'cannot hurt it, and gating each entry on a fault is what produced four real defects (a stale',\n 'shim that denied `pnpm install` and every git sync; faults that denied every Read; a config fault',\n 'that denied `rm -rf node_modules && pnpm install` while allowing a bare `pnpm install`).',\n '',\n '| # | allowed | outcome |',\n '|---|---|---|',\n ...L0_ALLOWLIST.map((e: L0AllowEntry, i: number): string => `| ${i + 1} | ${e.label} | ${e.kind.toUpperCase()} |`),\n '',\n '- **PASS** — L0 has no objection; the call falls THROUGH so the downstream guards still judge it.',\n '- **ALLOW** — terminal; bypasses everything, because a cure must stay reachable even when a',\n ' downstream guard would block it.',\n '',\n 'Every Bash entry is anchored to the WHOLE command. A leading `cd <dir> &&`, a trailing `2>&1`',\n 'and a pipe into `tail`/`head` are tolerated; nothing else is. Appending `&& git status` makes it',\n 'a DIFFERENT command and it is rejected again — that is not the guard refusing its own cure.',\n '',\n '`git merge` is deliberately NOT on this list. Main is merged ONLY through the 3-point fork merge',\n '(`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is already open).',\n '',\n '## Known asymmetry',\n '',\n 'Under `S`/`C`/`Y` the guard bin IS running, so a PASS really does fall through to the downstream',\n 'guards. Under `D`/`X`/`U`/`K` the bin is never executed, so there is nothing to fall through to and a',\n 'PASS degenerates into a terminal allow — reads are unguarded during those three faults.',\n '',\n '## Widening L0',\n '',\n 'Add an entry to `L0_ALLOWLIST` in `packages/tooling/ai-hook-rules/src/bin/shim.ts`. That array is',\n 'the single source for the JS allowlist, the `grep -E` inside the rendered shim, and this file.',\n '',\n ];\n}\n\n/**\n * Drop the matrix doc where the AI can read it, and return its absolute path ('' if it could not be\n * written). Called from the L0 BLOCK path so the deny can say `READ <path>`.\n *\n * Best-effort by design: this runs while the tree is already known-broken, and a missing template (an\n * @webpieces/rules-config older than this package) must degrade the deny message, never replace it\n * with a crash.\n */\n// webpieces-disable no-function-outside-class -- sibling of renderGuardMatrixDoc in this module\nexport function writeGuardMatrixDoc(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return writeTemplate(workspaceRoot, GUARD_MATRIX_DOC);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: no doc → the deny simply omits the pointer\n return '';\n }\n}\n\n/**\n * The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written.\n *\n * It opens with a NEWLINE, not a space: the JS-side L0 denies render in the house format now (a header,\n * a `[guard-name]` block, `Fix Option N:` lines), so a pointer glued onto the end of the last line would\n * be the one place the shape broke. A real newline is safe on both call paths — denyJson() JSON.stringifies\n * it, exactly as it does for every multi-line L1 report.\n */\n// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module\nexport function guardMatrixPointer(docPath: string): string {\n if (docPath === '') return '';\n return `\\nThe full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;\n}\n"]}
@@ -261,7 +261,7 @@ function renderNotes() {
261
261
  '| group | commands |',
262
262
  '|---|---|',
263
263
  '| get out | `git checkout -b <new> origin/main` · `git switch -c <new> origin/main` · `git switch <other>` · `git worktree add … -b <new> origin/main` |',
264
- '| make `main` current | `git pull` · `git fetch` · `git checkout main && git pull origin main` *(paired only)* |',
264
+ '| make `main` current | `pnpm wp-checkout-clean-main` *(the prescribed form — also reaps dead branches/worktrees and sweeps orphan directories)* · `git pull` · `git fetch` · `git checkout main && git pull origin main` *(paired only; still allowed, and still the L0 recovery cure, where no `pnpm` bin can be trusted)* |',
265
265
  '| orient | `git status\\|log\\|diff\\|branch` · `gh pr view\\|list\\|status\\|checks` |',
266
266
  '| park work | `git stash` |',
267
267
  '| repair / tooling | `pnpm wp-start-update` · `pnpm wp-start-upsert-pr` · the `wp-*` bins |',
@@ -1 +1 @@
1
- {"version":3,"file":"l2-doc.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l2-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { L2Row, L2NotDone, L2UseCase, L2_ROWS, L2_FAIL_OPEN_ROW, NOT_DONE, allL2UseCases } from './l2-rows';\n\n// ---------------------------------------------------------------------------\n// guards/L2-branch-state.md, rendered from L2_ROWS.\n//\n// Same arrangement as l1-doc.renderL1Doc(): one join('\\n') of literal markdown lines with the ROW DATA\n// interpolated from the array. Everything that is not row data is a literal line here, because that is\n// the half a generator cannot own.\n//\n// A unit test (l2-matrix.spec.ts) locks guards/L2-branch-state.md byte-identical to renderL2Doc(), and\n// `pnpm guards:generate` rewrites the file. The doc that stood here before was 100% hand-written and\n// carried its own warning that it could drift; it did, in three places at once (it proposed\n// `branch-state-guard` as a future key while GUARD_MATRIX.md proposed a different name and\n// docs/plans/guard-layer-toggles.md proposed a third).\n//\n// This module, like l2-rows.ts, has no runtime imports outside this pair so the generator can load it\n// without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL2Doc below, in this render module\nfunction tableRow(row: L2Row): string {\n return `| ${row.num} | \\`${row.toolCell()}\\` | ${row.state} | ${row.action.label} | ${row.cure} |`;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL2Doc below, in this render module\nfunction notDoneRow(entry: L2NotDone): string {\n return `| ${entry.row} | ${entry.gap} | ${entry.why} |`;\n}\n\n/**\n * The \"Not done\" body — a table when there are gaps, a SENTENCE when there are none.\n *\n * An empty table renders as a bare header with nothing under it, which reads like a rendering bug\n * rather than like an achievement. \"Every row is honoured\" is a claim worth making in words, and it is\n * the state this section exists to drive the layer towards.\n */\n// webpieces-disable no-function-outside-class -- section builder for renderL2Doc below, in this render module\nfunction renderNotDoneBody(): string[] {\n if (NOT_DONE.length === 0) {\n return [\n '**Nothing. Every row in the table above is a row the guards actually honour today.**',\n '',\n 'That has not always been true, and the section stays here for when it stops being true again:',\n 'a row the code cannot yet honour is listed here rather than rendered as if it were live, the',\n 'same way L1 lists its unreachable `o` row. The three entries this section used to carry were',\n 'row 5\\'s Bash half (now judged from the branch alone, above the cache divider) and the DIRTY-TREE',\n 'valves on rows 6 and 8 — both closed, because each of those rows cures with',\n '`git checkout -b <new> origin/main`, which carries uncommitted changes onto the new branch. A',\n 'dirty tree never trapped anyone; the row 6 message just printed the one cure that could not run',\n 'dirty, and the fix was to print both.',\n ];\n }\n return [\n 'Each row below describes INTENT the code has not caught up with. They are listed rather than',\n 'silently rendered as if they were live, the same way L1 lists its unreachable `o` row. Every one of',\n `them currently exits at row ${L2_FAIL_OPEN_ROW} instead, so the log never claims the strict row fired.`,\n '',\n '| row | the gap | why it has not shipped |',\n '|---|---|---|',\n ...NOT_DONE.map(notDoneRow),\n ];\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL2Doc below, in this render module\nfunction useCaseRow(useCase: L2UseCase): string {\n return `| ${useCase.num} | ${useCase.symptom} | ${useCase.state} | ${useCase.verdict} | ${useCase.fix} |`;\n}\n\n/**\n * Render guards/L2-branch-state.md.\n *\n * Split into sections purely to stay inside the method-line budget — the join order is what makes them\n * one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over L2_ROWS, beside the array it reads\nexport function renderL2Doc(): string {\n return [\n ...renderHead(),\n ...renderTable(),\n ...renderUseCases(),\n ...renderNotes(),\n ...renderTail(),\n ].join('\\n');\n}\n\n// webpieces-disable no-function-outside-class -- first section of renderL2Doc's string, beside it in this module\nfunction renderHead(): string[] {\n return [\n '# L2 — branch state',\n '',\n '**Goal: may I work here, and is what I read current?**',\n '',\n '**Config key: `branch-state-guard`.** ONE key for the whole policy. It used to be FOUR —',\n '`feature-branch-guard`, `read-stale-guard`, `stale-main-bash-guard`, `merged-branch-bash-guard` —',\n 'and three of them carried nothing but `mode` plus the two escape hatches. Four keys made HALF a',\n 'policy representable: `read-stale-guard: OFF` beside `merged-branch-bash-guard: ON` is \"read the',\n 'file, yes; `cat` the same file, no\" — the same information, opposite verdicts, chosen by nobody.',\n 'One key makes that unconstructible. The four class NAMES are unchanged and still appear as `rule=`',\n 'on every decision-log line, so `grep rule=stale-main-bash-guard` keeps working; only the switch',\n 'merged. The four old keys are rejected by name with this destination — see `retired-config-keys.ts`.',\n '',\n '**Code:** `ai-hook-rules/src/core/rules/{feature-branch,read-stale,stale-main-bash,merged-branch-bash}-guard.ts` ·',\n 'the rows in `ai-hook-rules/src/core/l2-rows.ts` · the shared cache in',\n '`rules-config/src/main-sync-status.ts` + `main-sync-file.ts` · the refresher in',\n '`ai-hook-rules/src/core/sync-main.ts`.',\n '',\n '## The four classes, and why there are four',\n '',\n '| | Write/Edit | Read | Bash |',\n '|---|---|---|---|',\n '| **state A** — stale `main` | `feature-branch-guard` | `read-stale-guard` | `stale-main-bash-guard` |',\n '| **state B** — merged branch | `feature-branch-guard` | `read-stale-guard` | `merged-branch-bash-guard` |',\n '',\n 'The split is TOOL WIRING, not policy. A Read names exactly one file; a Bash command is opaque; a',\n 'Write is neither.',\n '',\n '**The two Bash guards used to differ in polarity, and no longer do.** merged-branch was',\n 'default-DENY + allowlist; stale-main was default-ALLOW + blocklist, so `pnpm build` was denied by',\n 'one and allowed by the other for the same reason — \"you should not be working in this tree\". That',\n 'asymmetry was a consequence of stale-main asking about FRESHNESS, where a blocklist of content',\n 'readers is the right shape. Once it asks about the BRANCH instead (row 5), the right shape is the',\n 'one merged-branch already had, and they now share it: `RecoveryAllowlist`, the row 4 skip list, as',\n 'a single implementation. Two skip lists drift, and the half that drifts is the half that wedges a',\n 'session on its own cure.',\n '',\n 'They remain separate CLASSES because the states they detect are different — one reads the branch',\n 'name, the other the cached merged flag — and because each carries its own message.',\n '',\n '## The cache',\n '',\n '`<primary clone>/.webpieces/main-sync-status.json`, written by a **detached single-flight',\n 'refresher**. It is fire-and-forget: it populates the cache for the NEXT call, never the current',\n 'one — so the first tool call of every session sees no cache and takes row ' + String(L2_FAIL_OPEN_ROW) + '. That is intended,',\n 'and it is why the on-main write block (row 5) must not depend on the cache.',\n '',\n 'The file holds a **map of branch → status**, so every worktree\\'s guards stay armed. Before that it',\n 'held one branch\\'s snapshot, so with N worktrees at most one tree was armed at any instant and the',\n 'rest abstained, thrashing as the lock changed hands.',\n '',\n '**There is no TTL.** `timestamp` is logged and never enforced; an hours-old cache whose branch',\n 'matches is trusted to block. State A is mitigated by a live ancestry check (`git merge-base',\n '--is-ancestor`, not hash equality, so a pull takes effect instantly); state B trusts the cached',\n 'merged flag, which is safe only because \"merged\" is monotonic.',\n '',\n '**`hangTimeoutMinutes` is ONE knob** — `branch-state-guard.hangTimeoutMinutes` — because there is',\n 'one refresher writing one cache. It used to be declared four times and read four times, which,',\n 'with the refresher\\'s at-most-once-per-process latch and two config-blind callers ahead of the',\n 'guards, meant at most one of the four values could ever reach a spawn.',\n '',\n ];\n}\n\n// The legend and the table itself — this is the ROW DATA.\n// webpieces-disable no-function-outside-class -- second section of renderL2Doc's string, beside it in this module\nfunction renderTable(): string[] {\n return [\n '## Table — one table, ordered, first match wins',\n '',\n '**Tools:** `B` Bash · `R` Read · `E` Write/Edit',\n '',\n '| # | tools | state | act | cure |',\n '|---|---|---|---|---|',\n ...L2_ROWS.map(tableRow),\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`,\n `marker-file scan, and row 5 is one \\`git rev-parse\\`. Row ${L2_FAIL_OPEN_ROW} is the divider: everything below it`,\n `reads the main-sync cache, so if the branch is undeterminable, the cache is absent, it holds`,\n `another branch, or the forge could not be reached, evaluation STOPS at row ${L2_FAIL_OPEN_ROW} and ALLOWS.`,\n '',\n `Row ${L2_FAIL_OPEN_ROW} is numbered after row 10 and PRINTED between 5 and 6, and that is not a mistake. Row`,\n 'numbers are identity — they are logged as `row=` and cited here — so renumbering 6-10 to slot it in',\n 'would silently re-point every reference. L1 does the same with its row 8.',\n '',\n '### The one rule that explains the tool column',\n '',\n '**`B` tracks `E` everywhere. `R` is judged separately in exactly one place — rows 6/7, on `main`.**',\n '',\n 'A Read names exactly one file, so the guard can evaluate it precisely. A Bash command is opaque, so',\n 'it gets the conservative answer. Reading a CURRENT `main` is fine; the problem is that `main` is',\n 'almost always behind.',\n '',\n '### Why the order of row 5 is the most load-bearing thing here',\n '',\n 'L2 is armed **from the second tool call onward**, because the refresher populates the cache for the',\n 'NEXT call. That is deliberate — it keeps the blocking path free of network git — and it is fine in',\n 'practice, because the agent discovers the problem within a command or two.',\n '',\n `Row 5 is the exception that must not be relaxed. Put \"on \\`main\\`\" BELOW row ${L2_FAIL_OPEN_ROW} and writes on \\`main\\``,\n 'are permitted for the whole first call of every session — and permanently in a multi-worktree repo,',\n 'where another tree can hold the refresh lock indefinitely.',\n '',\n '### Why row 9 can block reads without trapping you',\n '',\n 'Blocking reads on a broken fork point looks like it traps the agent away from the files it must',\n 'read to resolve the conflict. It does not, because **row 3 comes first**:',\n '',\n 'blocked → `pnpm wp-start-update` (row 4, skip list) → now merge-in-progress → **row 3 exempts',\n 'everything** → read and write freely to resolve → finish. The exemption row is what lets row 9 be',\n 'strict.',\n '',\n '### Why row 8 can block reads on a dirty tree',\n '',\n '`git checkout -b <new> origin/main` **carries uncommitted changes onto the new branch**. The work',\n 'comes with you, so nothing needs reading first and nothing is trapped. Residual: if `origin/main`',\n 'changed the same files you edited, git refuses the switch — `git stash` is on the skip list and',\n 'clears it.',\n '',\n 'Row 6 looked like the one place the dirty argument had teeth, because its FIRST cure is `git pull`,',\n 'which genuinely is not a clean fast-forward on a dirty tree. But row 6 has always carried a SECOND',\n 'cure — `git checkout -b <new> origin/main` — and that one works dirty for exactly the reason above.',\n 'The teeth were in the MESSAGE, which printed only the pull; it now prints both, labelled, so the',\n 'cure an agent reads is always one it can run. **So there is no dirty row anywhere, and no dirty',\n 'valve in the code either** — both were closed, and \"Not done\" is empty as a result.',\n '',\n ];\n}\n\n// The use-case table (ROW DATA, in the doc's own global numbering) and the note that explains it.\n// webpieces-disable no-function-outside-class -- third section of renderL2Doc's string, beside it in this module\nfunction renderUseCases(): string[] {\n return [\n '## L2 use cases',\n '',\n 'Same row shape as L0 and L1: the **Fix** is literal or it is not a fix. Each case is attached IN',\n 'CODE to the row that judges it (`useCases` on `L2Row`), so this table cannot describe a row that no',\n 'longer exists and a row cannot quietly acquire behaviour nothing documents.',\n '',\n '**This table is how the layer LEARNS.** When a new situation comes up in a session, the change is',\n 'one more `new L2UseCase(...)` on the row that judged it — not a paragraph added here, which the',\n 'byte-lock spec would reject anyway. Every case also carries the exact `reason` string the guard',\n 'logs, and a spec pushes that back through `l2RowForReason` to assert it lands on the row it is filed',\n 'under. So a case whose row is wrong fails the build rather than misinforming a reader.',\n '',\n '| # | what you SEE (exact symptom) | state | verdict | Fix |',\n '|---|---|---|---|---|',\n ...allL2UseCases().map(useCaseRow),\n '',\n 'The write-on-main case under row 5 is the one to read beside \"Not done\": it is a real incident from',\n 'another repo on this toolchain, where `npx expo install` on `main` modified two tracked files and no',\n 'guard fired. It is filed under row 5 because row 5 is the row that SHOULD judge it — the table states',\n 'the policy, and \"Not done\" states how far the code has got. That is the arrangement that keeps a gap',\n 'visible instead of letting the doc quietly narrow itself to whatever the code happens to do.',\n '',\n ];\n}\n\n// How the log joins to this table, and the skip list. Prose plus the reason→row contract.\n// webpieces-disable no-function-outside-class -- fourth section of renderL2Doc's string, beside it in this module\nfunction renderNotes(): string[] {\n return [\n '## How a log line joins to a row',\n '',\n 'Every L2 decision is written to `.webpieces/logs/L2-decisions/<writer>.log` with `layer=L2` and',\n '`row=<n>`, where `<n>` is a row number from the table above. So `row=8` means \"this call was judged',\n 'by row 8\" and you read the state, the verdict and the cure straight off this page.',\n '',\n '**The join is by REASON, not by dispatch, and the difference is worth knowing.** L1 takes the first',\n 'matching row and switches on it, so deleting an L1 row deletes a block. L2\\'s four classes each own',\n 'their own ladder (see \"The four classes\" above for why they cannot be one function), and',\n '`L2_ROW_FOR_REASON` in `l2-rows.ts` maps each ladder exit to the row it is an instance of. A unit',\n 'test reads the four guard sources and asserts every reason literal resolves to a row, so a new exit',\n 'with no row fails the build rather than logging `row=-` forever.',\n '',\n '## The skip list (row 4)',\n '',\n 'Principle: **these get you OUT or tell you where you are.** They are not \"working here\".',\n '',\n '| group | commands |',\n '|---|---|',\n '| get out | `git checkout -b <new> origin/main` · `git switch -c <new> origin/main` · `git switch <other>` · `git worktree add … -b <new> origin/main` |',\n '| make `main` current | `git pull` · `git fetch` · `git checkout main && git pull origin main` *(paired only)* |',\n '| orient | `git status\\\\|log\\\\|diff\\\\|branch` · `gh pr view\\\\|list\\\\|status\\\\|checks` |',\n '| park work | `git stash` |',\n '| repair / tooling | `pnpm wp-start-update` · `pnpm wp-start-upsert-pr` · the `wp-*` bins |',\n '',\n '**NOT on it:** `git commit` `add` `push` `merge` `rebase` `reset` `restore` `clean` `cherry-pick` ·',\n '`git grep` `show <rev>:<path>` `cat-file` `ls-files` (those read tracked content). **`pnpm build` /',\n '`pnpm test` are not on it either** — there is no point running them on `main` or on a dead branch.',\n '',\n '## Cannot tell — everything that lands on row ' + String(L2_FAIL_OPEN_ROW),\n '',\n '| state | expected? | treatment |',\n '|---|---|---|',\n '| cache absent | **yes** — the refresher populates for the NEXT call, so this fires on the first tool call of every session | fail open, log |',\n '| detached HEAD | **yes** — mid-rebase, `git checkout <sha>` | fail open, log |',\n '| forge unreachable | **yes** — `gh` missing, unauthenticated, rate-limited or offline | fail open, log as `no-forge` |',\n '| branch unresolvable | **no** — not a repo, git broken | fail open, log LOUDLY |',\n '| cache for another branch | **no** — unreachable since the cache became branch-keyed | fail open, log LOUDLY |',\n '',\n '**Do NOT block to capture these cases.** `cache-absent` fires on every session\\'s first call;',\n 'blocking there deadlocks every session behind a network fetch. And if a guard cannot establish',\n 'state, blocking means a *broken* guard wedges the session — the exact failure this family exists to',\n 'avoid.',\n '',\n '`ALLOW_FAIL_OPEN` is a TYPED VERDICT, not a string suffix on the reason, so abstentions are',\n 'countable. `no-forge` is the newest member: `branchAlreadyMerged: false` used to be produced both by',\n '\"this branch has no merged PR\" and by \"we could not ask\", and both logged a plain ALLOW — so from',\n 'the trail you could not tell whether the merged-branch policy was protecting anything or quietly',\n 'standing down.',\n '',\n ];\n}\n\n// The gaps between the table and the code, and the code anchors.\n// webpieces-disable no-function-outside-class -- last section of renderL2Doc's string, beside it in this module\nfunction renderTail(): string[] {\n return [\n '## Not done — rows the guards do not yet honour',\n '',\n ...renderNotDoneBody(),\n '',\n 'This section is generated from `NOT_DONE` in `l2-rows.ts`, so closing a gap means deleting its entry',\n 'and the doc follows — it cannot rot into a list of things that were fixed years ago.',\n '',\n '## Incidents these guards exist because of',\n '',\n '- **The 157-commit checkout.** An agent ran `git checkout main` in a clone whose local `main` was',\n ' 157 commits behind. That checkout reverted the `@webpieces` pin, reverted the guard shim — **the',\n ' drift guard itself** — to a copy whose message stated the drift backwards, and so reverted the',\n ' agent\\'s judgment: it ran the `pnpm install` that message named and downgraded `node_modules`.',\n ' Lesson, quoted from the code: *a guard a stale checkout can revert cannot be relied on to catch a',\n ' stale checkout.* Hence row 2, which is preventive, matches on command TEXT only, and asks git',\n ' nothing — deliberately, because the only `main` it could measure is the one it is about to leave.',\n '- **The side door.** An agent on a `main` 18 commits behind (108 files, +8069/−3692 upstream) had',\n ' its Read tool blocked exactly as designed, then spent the session `ls`-ing, `grep`-ing and',\n ' `cat`-ing the same stale tree, and described a CI workflow set missing a 186-line workflow that',\n ' existed upstream. *The logs read \"read-stale-guard handled\", which is worse than no guard: it',\n ' looks covered.*',\n '- **Computed and thrown away.** Both file guards are file-scoped, so Bash reached neither. An agent',\n ' that only ran shell sailed through on a merged branch **even though `branchAlreadyMerged` was',\n ' loaded and logged on that very path.**',\n '',\n '---',\n '',\n '',\n '## Code anchors',\n '',\n '| section | file | symbol |',\n '|---|---|---|',\n '| the rows + the reason→row join | `ai-hook-rules/src/core/l2-rows.ts` | `L2_ROWS`, `l2RowForReason`, `NOT_DONE` |',\n '| write policy | `ai-hook-rules/src/core/rules/feature-branch-guard.ts` | `check` |',\n '| read policy | `ai-hook-rules/src/core/rules/read-stale-guard.ts` | `checkStaleMain`, `checkMergedBranch` |',\n '| stale-main Bash | `ai-hook-rules/src/core/rules/stale-main-bash-guard.ts` | `staleContentRead`, `bareCheckoutOfMain` |',\n '| merged-branch Bash | `ai-hook-rules/src/core/rules/merged-branch-bash-guard.ts` | `isFullyRecovery`, `ALLOWED_GIT_SUBCOMMANDS` |',\n '| the cache | `rules-config/src/main-sync-status.ts`, `main-sync-file.ts` | `readMainSyncStatus`, `MainSyncStatusFile`, `forgeReachable` |',\n '| the refresher | `ai-hook-rules/src/core/sync-main.ts` | `refreshMainSync` |',\n '| command scanning | `ai-hook-rules/src/core/rules/content-read-scan.ts`, `shell-segment-scan.ts` | `readsStaleContent`, `classify` |',\n '| the config key | `rules-config/src/main-sync-guard-configs.ts`, `sections.ts` | `BranchStateGuardConfig`, `BRANCH_STATE_GUARD_KEY` |',\n '',\n ];\n}\n"]}
1
+ {"version":3,"file":"l2-doc.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l2-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { L2Row, L2NotDone, L2UseCase, L2_ROWS, L2_FAIL_OPEN_ROW, NOT_DONE, allL2UseCases } from './l2-rows';\n\n// ---------------------------------------------------------------------------\n// guards/L2-branch-state.md, rendered from L2_ROWS.\n//\n// Same arrangement as l1-doc.renderL1Doc(): one join('\\n') of literal markdown lines with the ROW DATA\n// interpolated from the array. Everything that is not row data is a literal line here, because that is\n// the half a generator cannot own.\n//\n// A unit test (l2-matrix.spec.ts) locks guards/L2-branch-state.md byte-identical to renderL2Doc(), and\n// `pnpm guards:generate` rewrites the file. The doc that stood here before was 100% hand-written and\n// carried its own warning that it could drift; it did, in three places at once (it proposed\n// `branch-state-guard` as a future key while GUARD_MATRIX.md proposed a different name and\n// docs/plans/guard-layer-toggles.md proposed a third).\n//\n// This module, like l2-rows.ts, has no runtime imports outside this pair so the generator can load it\n// without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL2Doc below, in this render module\nfunction tableRow(row: L2Row): string {\n return `| ${row.num} | \\`${row.toolCell()}\\` | ${row.state} | ${row.action.label} | ${row.cure} |`;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL2Doc below, in this render module\nfunction notDoneRow(entry: L2NotDone): string {\n return `| ${entry.row} | ${entry.gap} | ${entry.why} |`;\n}\n\n/**\n * The \"Not done\" body — a table when there are gaps, a SENTENCE when there are none.\n *\n * An empty table renders as a bare header with nothing under it, which reads like a rendering bug\n * rather than like an achievement. \"Every row is honoured\" is a claim worth making in words, and it is\n * the state this section exists to drive the layer towards.\n */\n// webpieces-disable no-function-outside-class -- section builder for renderL2Doc below, in this render module\nfunction renderNotDoneBody(): string[] {\n if (NOT_DONE.length === 0) {\n return [\n '**Nothing. Every row in the table above is a row the guards actually honour today.**',\n '',\n 'That has not always been true, and the section stays here for when it stops being true again:',\n 'a row the code cannot yet honour is listed here rather than rendered as if it were live, the',\n 'same way L1 lists its unreachable `o` row. The three entries this section used to carry were',\n 'row 5\\'s Bash half (now judged from the branch alone, above the cache divider) and the DIRTY-TREE',\n 'valves on rows 6 and 8 — both closed, because each of those rows cures with',\n '`git checkout -b <new> origin/main`, which carries uncommitted changes onto the new branch. A',\n 'dirty tree never trapped anyone; the row 6 message just printed the one cure that could not run',\n 'dirty, and the fix was to print both.',\n ];\n }\n return [\n 'Each row below describes INTENT the code has not caught up with. They are listed rather than',\n 'silently rendered as if they were live, the same way L1 lists its unreachable `o` row. Every one of',\n `them currently exits at row ${L2_FAIL_OPEN_ROW} instead, so the log never claims the strict row fired.`,\n '',\n '| row | the gap | why it has not shipped |',\n '|---|---|---|',\n ...NOT_DONE.map(notDoneRow),\n ];\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL2Doc below, in this render module\nfunction useCaseRow(useCase: L2UseCase): string {\n return `| ${useCase.num} | ${useCase.symptom} | ${useCase.state} | ${useCase.verdict} | ${useCase.fix} |`;\n}\n\n/**\n * Render guards/L2-branch-state.md.\n *\n * Split into sections purely to stay inside the method-line budget — the join order is what makes them\n * one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over L2_ROWS, beside the array it reads\nexport function renderL2Doc(): string {\n return [\n ...renderHead(),\n ...renderTable(),\n ...renderUseCases(),\n ...renderNotes(),\n ...renderTail(),\n ].join('\\n');\n}\n\n// webpieces-disable no-function-outside-class -- first section of renderL2Doc's string, beside it in this module\nfunction renderHead(): string[] {\n return [\n '# L2 — branch state',\n '',\n '**Goal: may I work here, and is what I read current?**',\n '',\n '**Config key: `branch-state-guard`.** ONE key for the whole policy. It used to be FOUR —',\n '`feature-branch-guard`, `read-stale-guard`, `stale-main-bash-guard`, `merged-branch-bash-guard` —',\n 'and three of them carried nothing but `mode` plus the two escape hatches. Four keys made HALF a',\n 'policy representable: `read-stale-guard: OFF` beside `merged-branch-bash-guard: ON` is \"read the',\n 'file, yes; `cat` the same file, no\" — the same information, opposite verdicts, chosen by nobody.',\n 'One key makes that unconstructible. The four class NAMES are unchanged and still appear as `rule=`',\n 'on every decision-log line, so `grep rule=stale-main-bash-guard` keeps working; only the switch',\n 'merged. The four old keys are rejected by name with this destination — see `retired-config-keys.ts`.',\n '',\n '**Code:** `ai-hook-rules/src/core/rules/{feature-branch,read-stale,stale-main-bash,merged-branch-bash}-guard.ts` ·',\n 'the rows in `ai-hook-rules/src/core/l2-rows.ts` · the shared cache in',\n '`rules-config/src/main-sync-status.ts` + `main-sync-file.ts` · the refresher in',\n '`ai-hook-rules/src/core/sync-main.ts`.',\n '',\n '## The four classes, and why there are four',\n '',\n '| | Write/Edit | Read | Bash |',\n '|---|---|---|---|',\n '| **state A** — stale `main` | `feature-branch-guard` | `read-stale-guard` | `stale-main-bash-guard` |',\n '| **state B** — merged branch | `feature-branch-guard` | `read-stale-guard` | `merged-branch-bash-guard` |',\n '',\n 'The split is TOOL WIRING, not policy. A Read names exactly one file; a Bash command is opaque; a',\n 'Write is neither.',\n '',\n '**The two Bash guards used to differ in polarity, and no longer do.** merged-branch was',\n 'default-DENY + allowlist; stale-main was default-ALLOW + blocklist, so `pnpm build` was denied by',\n 'one and allowed by the other for the same reason — \"you should not be working in this tree\". That',\n 'asymmetry was a consequence of stale-main asking about FRESHNESS, where a blocklist of content',\n 'readers is the right shape. Once it asks about the BRANCH instead (row 5), the right shape is the',\n 'one merged-branch already had, and they now share it: `RecoveryAllowlist`, the row 4 skip list, as',\n 'a single implementation. Two skip lists drift, and the half that drifts is the half that wedges a',\n 'session on its own cure.',\n '',\n 'They remain separate CLASSES because the states they detect are different — one reads the branch',\n 'name, the other the cached merged flag — and because each carries its own message.',\n '',\n '## The cache',\n '',\n '`<primary clone>/.webpieces/main-sync-status.json`, written by a **detached single-flight',\n 'refresher**. It is fire-and-forget: it populates the cache for the NEXT call, never the current',\n 'one — so the first tool call of every session sees no cache and takes row ' + String(L2_FAIL_OPEN_ROW) + '. That is intended,',\n 'and it is why the on-main write block (row 5) must not depend on the cache.',\n '',\n 'The file holds a **map of branch → status**, so every worktree\\'s guards stay armed. Before that it',\n 'held one branch\\'s snapshot, so with N worktrees at most one tree was armed at any instant and the',\n 'rest abstained, thrashing as the lock changed hands.',\n '',\n '**There is no TTL.** `timestamp` is logged and never enforced; an hours-old cache whose branch',\n 'matches is trusted to block. State A is mitigated by a live ancestry check (`git merge-base',\n '--is-ancestor`, not hash equality, so a pull takes effect instantly); state B trusts the cached',\n 'merged flag, which is safe only because \"merged\" is monotonic.',\n '',\n '**`hangTimeoutMinutes` is ONE knob** — `branch-state-guard.hangTimeoutMinutes` — because there is',\n 'one refresher writing one cache. It used to be declared four times and read four times, which,',\n 'with the refresher\\'s at-most-once-per-process latch and two config-blind callers ahead of the',\n 'guards, meant at most one of the four values could ever reach a spawn.',\n '',\n ];\n}\n\n// The legend and the table itself — this is the ROW DATA.\n// webpieces-disable no-function-outside-class -- second section of renderL2Doc's string, beside it in this module\nfunction renderTable(): string[] {\n return [\n '## Table — one table, ordered, first match wins',\n '',\n '**Tools:** `B` Bash · `R` Read · `E` Write/Edit',\n '',\n '| # | tools | state | act | cure |',\n '|---|---|---|---|---|',\n ...L2_ROWS.map(tableRow),\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`,\n `marker-file scan, and row 5 is one \\`git rev-parse\\`. Row ${L2_FAIL_OPEN_ROW} is the divider: everything below it`,\n `reads the main-sync cache, so if the branch is undeterminable, the cache is absent, it holds`,\n `another branch, or the forge could not be reached, evaluation STOPS at row ${L2_FAIL_OPEN_ROW} and ALLOWS.`,\n '',\n `Row ${L2_FAIL_OPEN_ROW} is numbered after row 10 and PRINTED between 5 and 6, and that is not a mistake. Row`,\n 'numbers are identity — they are logged as `row=` and cited here — so renumbering 6-10 to slot it in',\n 'would silently re-point every reference. L1 does the same with its row 8.',\n '',\n '### The one rule that explains the tool column',\n '',\n '**`B` tracks `E` everywhere. `R` is judged separately in exactly one place — rows 6/7, on `main`.**',\n '',\n 'A Read names exactly one file, so the guard can evaluate it precisely. A Bash command is opaque, so',\n 'it gets the conservative answer. Reading a CURRENT `main` is fine; the problem is that `main` is',\n 'almost always behind.',\n '',\n '### Why the order of row 5 is the most load-bearing thing here',\n '',\n 'L2 is armed **from the second tool call onward**, because the refresher populates the cache for the',\n 'NEXT call. That is deliberate — it keeps the blocking path free of network git — and it is fine in',\n 'practice, because the agent discovers the problem within a command or two.',\n '',\n `Row 5 is the exception that must not be relaxed. Put \"on \\`main\\`\" BELOW row ${L2_FAIL_OPEN_ROW} and writes on \\`main\\``,\n 'are permitted for the whole first call of every session — and permanently in a multi-worktree repo,',\n 'where another tree can hold the refresh lock indefinitely.',\n '',\n '### Why row 9 can block reads without trapping you',\n '',\n 'Blocking reads on a broken fork point looks like it traps the agent away from the files it must',\n 'read to resolve the conflict. It does not, because **row 3 comes first**:',\n '',\n 'blocked → `pnpm wp-start-update` (row 4, skip list) → now merge-in-progress → **row 3 exempts',\n 'everything** → read and write freely to resolve → finish. The exemption row is what lets row 9 be',\n 'strict.',\n '',\n '### Why row 8 can block reads on a dirty tree',\n '',\n '`git checkout -b <new> origin/main` **carries uncommitted changes onto the new branch**. The work',\n 'comes with you, so nothing needs reading first and nothing is trapped. Residual: if `origin/main`',\n 'changed the same files you edited, git refuses the switch — `git stash` is on the skip list and',\n 'clears it.',\n '',\n 'Row 6 looked like the one place the dirty argument had teeth, because its FIRST cure is `git pull`,',\n 'which genuinely is not a clean fast-forward on a dirty tree. But row 6 has always carried a SECOND',\n 'cure — `git checkout -b <new> origin/main` — and that one works dirty for exactly the reason above.',\n 'The teeth were in the MESSAGE, which printed only the pull; it now prints both, labelled, so the',\n 'cure an agent reads is always one it can run. **So there is no dirty row anywhere, and no dirty',\n 'valve in the code either** — both were closed, and \"Not done\" is empty as a result.',\n '',\n ];\n}\n\n// The use-case table (ROW DATA, in the doc's own global numbering) and the note that explains it.\n// webpieces-disable no-function-outside-class -- third section of renderL2Doc's string, beside it in this module\nfunction renderUseCases(): string[] {\n return [\n '## L2 use cases',\n '',\n 'Same row shape as L0 and L1: the **Fix** is literal or it is not a fix. Each case is attached IN',\n 'CODE to the row that judges it (`useCases` on `L2Row`), so this table cannot describe a row that no',\n 'longer exists and a row cannot quietly acquire behaviour nothing documents.',\n '',\n '**This table is how the layer LEARNS.** When a new situation comes up in a session, the change is',\n 'one more `new L2UseCase(...)` on the row that judged it — not a paragraph added here, which the',\n 'byte-lock spec would reject anyway. Every case also carries the exact `reason` string the guard',\n 'logs, and a spec pushes that back through `l2RowForReason` to assert it lands on the row it is filed',\n 'under. So a case whose row is wrong fails the build rather than misinforming a reader.',\n '',\n '| # | what you SEE (exact symptom) | state | verdict | Fix |',\n '|---|---|---|---|---|',\n ...allL2UseCases().map(useCaseRow),\n '',\n 'The write-on-main case under row 5 is the one to read beside \"Not done\": it is a real incident from',\n 'another repo on this toolchain, where `npx expo install` on `main` modified two tracked files and no',\n 'guard fired. It is filed under row 5 because row 5 is the row that SHOULD judge it — the table states',\n 'the policy, and \"Not done\" states how far the code has got. That is the arrangement that keeps a gap',\n 'visible instead of letting the doc quietly narrow itself to whatever the code happens to do.',\n '',\n ];\n}\n\n// How the log joins to this table, and the skip list. Prose plus the reason→row contract.\n// webpieces-disable no-function-outside-class -- fourth section of renderL2Doc's string, beside it in this module\nfunction renderNotes(): string[] {\n return [\n '## How a log line joins to a row',\n '',\n 'Every L2 decision is written to `.webpieces/logs/L2-decisions/<writer>.log` with `layer=L2` and',\n '`row=<n>`, where `<n>` is a row number from the table above. So `row=8` means \"this call was judged',\n 'by row 8\" and you read the state, the verdict and the cure straight off this page.',\n '',\n '**The join is by REASON, not by dispatch, and the difference is worth knowing.** L1 takes the first',\n 'matching row and switches on it, so deleting an L1 row deletes a block. L2\\'s four classes each own',\n 'their own ladder (see \"The four classes\" above for why they cannot be one function), and',\n '`L2_ROW_FOR_REASON` in `l2-rows.ts` maps each ladder exit to the row it is an instance of. A unit',\n 'test reads the four guard sources and asserts every reason literal resolves to a row, so a new exit',\n 'with no row fails the build rather than logging `row=-` forever.',\n '',\n '## The skip list (row 4)',\n '',\n 'Principle: **these get you OUT or tell you where you are.** They are not \"working here\".',\n '',\n '| group | commands |',\n '|---|---|',\n '| get out | `git checkout -b <new> origin/main` · `git switch -c <new> origin/main` · `git switch <other>` · `git worktree add … -b <new> origin/main` |',\n '| make `main` current | `pnpm wp-checkout-clean-main` *(the prescribed form — also reaps dead branches/worktrees and sweeps orphan directories)* · `git pull` · `git fetch` · `git checkout main && git pull origin main` *(paired only; still allowed, and still the L0 recovery cure, where no `pnpm` bin can be trusted)* |',\n '| orient | `git status\\\\|log\\\\|diff\\\\|branch` · `gh pr view\\\\|list\\\\|status\\\\|checks` |',\n '| park work | `git stash` |',\n '| repair / tooling | `pnpm wp-start-update` · `pnpm wp-start-upsert-pr` · the `wp-*` bins |',\n '',\n '**NOT on it:** `git commit` `add` `push` `merge` `rebase` `reset` `restore` `clean` `cherry-pick` ·',\n '`git grep` `show <rev>:<path>` `cat-file` `ls-files` (those read tracked content). **`pnpm build` /',\n '`pnpm test` are not on it either** — there is no point running them on `main` or on a dead branch.',\n '',\n '## Cannot tell — everything that lands on row ' + String(L2_FAIL_OPEN_ROW),\n '',\n '| state | expected? | treatment |',\n '|---|---|---|',\n '| cache absent | **yes** — the refresher populates for the NEXT call, so this fires on the first tool call of every session | fail open, log |',\n '| detached HEAD | **yes** — mid-rebase, `git checkout <sha>` | fail open, log |',\n '| forge unreachable | **yes** — `gh` missing, unauthenticated, rate-limited or offline | fail open, log as `no-forge` |',\n '| branch unresolvable | **no** — not a repo, git broken | fail open, log LOUDLY |',\n '| cache for another branch | **no** — unreachable since the cache became branch-keyed | fail open, log LOUDLY |',\n '',\n '**Do NOT block to capture these cases.** `cache-absent` fires on every session\\'s first call;',\n 'blocking there deadlocks every session behind a network fetch. And if a guard cannot establish',\n 'state, blocking means a *broken* guard wedges the session — the exact failure this family exists to',\n 'avoid.',\n '',\n '`ALLOW_FAIL_OPEN` is a TYPED VERDICT, not a string suffix on the reason, so abstentions are',\n 'countable. `no-forge` is the newest member: `branchAlreadyMerged: false` used to be produced both by',\n '\"this branch has no merged PR\" and by \"we could not ask\", and both logged a plain ALLOW — so from',\n 'the trail you could not tell whether the merged-branch policy was protecting anything or quietly',\n 'standing down.',\n '',\n ];\n}\n\n// The gaps between the table and the code, and the code anchors.\n// webpieces-disable no-function-outside-class -- last section of renderL2Doc's string, beside it in this module\nfunction renderTail(): string[] {\n return [\n '## Not done — rows the guards do not yet honour',\n '',\n ...renderNotDoneBody(),\n '',\n 'This section is generated from `NOT_DONE` in `l2-rows.ts`, so closing a gap means deleting its entry',\n 'and the doc follows — it cannot rot into a list of things that were fixed years ago.',\n '',\n '## Incidents these guards exist because of',\n '',\n '- **The 157-commit checkout.** An agent ran `git checkout main` in a clone whose local `main` was',\n ' 157 commits behind. That checkout reverted the `@webpieces` pin, reverted the guard shim — **the',\n ' drift guard itself** — to a copy whose message stated the drift backwards, and so reverted the',\n ' agent\\'s judgment: it ran the `pnpm install` that message named and downgraded `node_modules`.',\n ' Lesson, quoted from the code: *a guard a stale checkout can revert cannot be relied on to catch a',\n ' stale checkout.* Hence row 2, which is preventive, matches on command TEXT only, and asks git',\n ' nothing — deliberately, because the only `main` it could measure is the one it is about to leave.',\n '- **The side door.** An agent on a `main` 18 commits behind (108 files, +8069/−3692 upstream) had',\n ' its Read tool blocked exactly as designed, then spent the session `ls`-ing, `grep`-ing and',\n ' `cat`-ing the same stale tree, and described a CI workflow set missing a 186-line workflow that',\n ' existed upstream. *The logs read \"read-stale-guard handled\", which is worse than no guard: it',\n ' looks covered.*',\n '- **Computed and thrown away.** Both file guards are file-scoped, so Bash reached neither. An agent',\n ' that only ran shell sailed through on a merged branch **even though `branchAlreadyMerged` was',\n ' loaded and logged on that very path.**',\n '',\n '---',\n '',\n '',\n '## Code anchors',\n '',\n '| section | file | symbol |',\n '|---|---|---|',\n '| the rows + the reason→row join | `ai-hook-rules/src/core/l2-rows.ts` | `L2_ROWS`, `l2RowForReason`, `NOT_DONE` |',\n '| write policy | `ai-hook-rules/src/core/rules/feature-branch-guard.ts` | `check` |',\n '| read policy | `ai-hook-rules/src/core/rules/read-stale-guard.ts` | `checkStaleMain`, `checkMergedBranch` |',\n '| stale-main Bash | `ai-hook-rules/src/core/rules/stale-main-bash-guard.ts` | `staleContentRead`, `bareCheckoutOfMain` |',\n '| merged-branch Bash | `ai-hook-rules/src/core/rules/merged-branch-bash-guard.ts` | `isFullyRecovery`, `ALLOWED_GIT_SUBCOMMANDS` |',\n '| the cache | `rules-config/src/main-sync-status.ts`, `main-sync-file.ts` | `readMainSyncStatus`, `MainSyncStatusFile`, `forgeReachable` |',\n '| the refresher | `ai-hook-rules/src/core/sync-main.ts` | `refreshMainSync` |',\n '| command scanning | `ai-hook-rules/src/core/rules/content-read-scan.ts`, `shell-segment-scan.ts` | `readsStaleContent`, `classify` |',\n '| the config key | `rules-config/src/main-sync-guard-configs.ts`, `sections.ts` | `BranchStateGuardConfig`, `BRANCH_STATE_GUARD_KEY` |',\n '',\n ];\n}\n"]}
@@ -175,8 +175,8 @@ exports.L2_ROWS = [
175
175
  new L2UseCase(1, 'You are blocked by some other L2 row, and need to turn the policy off to get anything done', 'any state — this row is ahead of every block', 'ALLOW: reading and editing `webpieces.config.json` is never blocked, so the mode-OFF cure is always reachable', 'Edit `webpieces.config.json` → `hookGuards` → `branch-state-guard` → `"mode": "OFF"`', 'webpieces-config-read (escape hatch)'),
176
176
  new L2UseCase(2, 'A Write to `webpieces.config.json` while on `main`, which row 5 would otherwise block', 'on `main`, editing the one file that can disable the guard', 'ALLOW: the hook adapter bypasses feature-branch-guard for this path before any guard runs', 'None needed — the edit proceeds', 'config-bypass (feature-branch-guard skipped)'),
177
177
  ]),
178
- new L2Row(2, ['B'], 'bare `git checkout main`, with no `git pull` chained into the same command', exports.L2_BLOCK, '`git checkout main && git pull origin main`', [
179
- new L2UseCase(3, '`git checkout main` after a merge, to start the next piece of work', 'about to land on whatever local `main` you last had — 157 commits behind, in the incident', 'BLOCK: decided from command TEXT alone, before the checkout, because the only `main` this could measure is the one it is about to leave', '`git checkout main && git pull origin main` the pull must be in the SAME command', 'bare checkout of main ('),
178
+ new L2Row(2, ['B'], 'bare `git checkout main`, with no `git pull` chained into the same command', exports.L2_BLOCK, '`pnpm wp-checkout-clean-main`', [
179
+ new L2UseCase(3, '`git checkout main` after a merge, to start the next piece of work', 'about to land on whatever local `main` you last had — 157 commits behind, in the incident', 'BLOCK: decided from command TEXT alone, before the checkout, because the only `main` this could measure is the one it is about to leave', '`pnpm wp-checkout-clean-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)', 'bare checkout of main ('),
180
180
  new L2UseCase(4, 'The same command inside a linked worktree, where `git checkout main` fatals anyway', 'linked worktree — `main` is already checked out in the primary clone', 'BLOCK, and the message prints the worktree form rather than a cure git would refuse', '`git fetch origin main`, then work off `origin/main`', 'bare checkout of main ('),
181
181
  ]),
182
182
  new L2Row(3, ['B', 'R', 'E'], '**merge in progress** — L4 owns this state', exports.L2_EXEMPT, 'finish the merge: `pnpm wp-finish-upsert-pr`', [
@@ -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 TEN 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.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 ELEVEN 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` tracks `E` everywhere; `R` is judged separately in exactly ONE place, rows 6/7 on `main`. A Read\n * names exactly one file so the guard can evaluate it precisely; a Bash command is opaque and gets the\n * conservative answer. Reading a CURRENT `main` is fine — the problem is that `main` is almost always\n * behind.\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, '`git checkout main && git pull origin 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 '`git checkout main && git pull origin main` — the pull must be in the SAME command',\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(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, ['B', '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(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 `main`, and the write is incidental to a command whose stated purpose is something else',\n 'BLOCK: default-DENY on `main` plus row 4\\'s skip list, so 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-main'),\n new L2UseCase(24,\n 'A build or a test run on a `main` that is perfectly up to date',\n 'on `main`, current — no staleness anywhere',\n 'BLOCK: freshness is not the question. `main` is not a place to work even when current, and the cure is a new branch, not a pull',\n '`git checkout -b <new> origin/main`',\n 'on-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, different tool',\n 'BLOCK. This case used to be judged by row 6 (a stale-content blocklist on the Bash side); row 5 now subsumes it, because being on `main` is already the finding and no enumeration of readers is needed. 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-main'),\n new L2UseCase(25,\n 'The FIRST command 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 the case the cache-gated version could never catch',\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(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 new L2Row(6, ['R'], 'on `main`, behind `origin/main`', L2_BLOCK, '`git pull origin 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 '`git pull origin main`, or `git checkout -b <new> origin/main`',\n 'on-stale-main'),\n ]),\n new L2Row(7, ['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: this is the ONE place a Read is judged differently from a Bash command, because a Read names exactly one file and can be evaluated precisely',\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 work on main.\n 'on-main': 5,\n // Row 6/7 — the ONE place a Read is judged differently from a Bash command.\n 'on-stale-main': 6,\n 'local-main-contains-origin (up to date)': 7,\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 (on `main` is now judged from the branch alone,\n // above the cache divider). Rows 6 and 8 held DIRTY-TREE 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 TEN 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.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 ELEVEN 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` tracks `E` everywhere; `R` is judged separately in exactly ONE place, rows 6/7 on `main`. A Read\n * names exactly one file so the guard can evaluate it precisely; a Bash command is opaque and gets the\n * conservative answer. Reading a CURRENT `main` is fine — the problem is that `main` is almost always\n * behind.\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-checkout-clean-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-checkout-clean-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(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, ['B', '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(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 `main`, and the write is incidental to a command whose stated purpose is something else',\n 'BLOCK: default-DENY on `main` plus row 4\\'s skip list, so 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-main'),\n new L2UseCase(24,\n 'A build or a test run on a `main` that is perfectly up to date',\n 'on `main`, current — no staleness anywhere',\n 'BLOCK: freshness is not the question. `main` is not a place to work even when current, and the cure is a new branch, not a pull',\n '`git checkout -b <new> origin/main`',\n 'on-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, different tool',\n 'BLOCK. This case used to be judged by row 6 (a stale-content blocklist on the Bash side); row 5 now subsumes it, because being on `main` is already the finding and no enumeration of readers is needed. 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-main'),\n new L2UseCase(25,\n 'The FIRST command 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 the case the cache-gated version could never catch',\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(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 new L2Row(6, ['R'], 'on `main`, behind `origin/main`', L2_BLOCK, '`git pull origin 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 '`git pull origin main`, or `git checkout -b <new> origin/main`',\n 'on-stale-main'),\n ]),\n new L2Row(7, ['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: this is the ONE place a Read is judged differently from a Bash command, because a Read names exactly one file and can be evaluated precisely',\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 work on main.\n 'on-main': 5,\n // Row 6/7 — the ONE place a Read is judged differently from a Bash command.\n 'on-stale-main': 6,\n 'local-main-contains-origin (up to date)': 7,\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 (on `main` is now judged from the branch alone,\n // above the cache divider). Rows 6 and 8 held DIRTY-TREE 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"]}
@@ -28,6 +28,11 @@ export declare class BranchSwitchScan {
28
28
  * True only for landing on an EXISTING branch named `main` — the form that can hand you a stale
29
29
  * tree, and the form `git checkout main && git pull origin main` uses. `-b`/`-B`/`-c`/`-C` are
30
30
  * excluded because they create the branch here and now.
31
+ *
32
+ * This scan reads raw GIT, and it has to keep doing so even though the workflow messages now
33
+ * prescribe `pnpm wp-checkout-clean-main` instead: what the guards recommend is not what an agent
34
+ * necessarily types, the raw pair is still allowed and is still the L0 recovery cure, and a scan
35
+ * that stopped recognising it would stop catching the bare `git checkout main` it exists to catch.
31
36
  */
32
37
  landsOnExistingMain(segment: string): boolean;
33
38
  /** The same question for a target already parsed (switchesIn). One spelling, two entry shapes. */
@@ -88,6 +88,11 @@ class BranchSwitchScan {
88
88
  * True only for landing on an EXISTING branch named `main` — the form that can hand you a stale
89
89
  * tree, and the form `git checkout main && git pull origin main` uses. `-b`/`-B`/`-c`/`-C` are
90
90
  * excluded because they create the branch here and now.
91
+ *
92
+ * This scan reads raw GIT, and it has to keep doing so even though the workflow messages now
93
+ * prescribe `pnpm wp-checkout-clean-main` instead: what the guards recommend is not what an agent
94
+ * necessarily types, the raw pair is still allowed and is still the L0 recovery cure, and a scan
95
+ * that stopped recognising it would stop catching the bare `git checkout main` it exists to catch.
91
96
  */
92
97
  landsOnExistingMain(segment) {
93
98
  const target = this.targetOf(segment);