@webpieces/ai-hook-rules 0.4.667 → 0.4.669

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +2 -2
  2. package/src/bin/l0-allowlist.d.ts +7 -1
  3. package/src/bin/l0-allowlist.js +26 -6
  4. package/src/bin/l0-allowlist.js.map +1 -1
  5. package/src/bin/shim-drift-fix.js +15 -1
  6. package/src/bin/shim-drift-fix.js.map +1 -1
  7. package/src/core/l0-matrix.js +6 -0
  8. package/src/core/l0-matrix.js.map +1 -1
  9. package/src/core/l1-rows.js +1 -1
  10. package/src/core/l1-rows.js.map +1 -1
  11. package/src/core/l2-doc.js +1 -1
  12. package/src/core/l2-doc.js.map +1 -1
  13. package/src/core/l2-rows.js +2 -2
  14. package/src/core/l2-rows.js.map +1 -1
  15. package/src/core/rules/branch-switch-scan.d.ts +5 -0
  16. package/src/core/rules/branch-switch-scan.js +5 -0
  17. package/src/core/rules/branch-switch-scan.js.map +1 -1
  18. package/src/core/rules/catch-error-pattern.d.ts +25 -0
  19. package/src/core/rules/catch-error-pattern.js +54 -16
  20. package/src/core/rules/catch-error-pattern.js.map +1 -1
  21. package/src/core/rules/merged-branch-bash-guard.js +1 -1
  22. package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
  23. package/src/core/rules/merged-branch-message.js +4 -3
  24. package/src/core/rules/merged-branch-message.js.map +1 -1
  25. package/src/core/rules/redirect-how-to-merge-main.js +9 -3
  26. package/src/core/rules/redirect-how-to-merge-main.js.map +1 -1
  27. package/src/core/rules/stale-main-bash-guard.d.ts +5 -2
  28. package/src/core/rules/stale-main-bash-guard.js +15 -10
  29. package/src/core/rules/stale-main-bash-guard.js.map +1 -1
  30. package/src/core/rules/tree-recovery.d.ts +26 -5
  31. package/src/core/rules/tree-recovery.js +33 -12
  32. package/src/core/rules/tree-recovery.js.map +1 -1
  33. package/src/core/runner.js +2 -0
  34. package/src/core/runner.js.map +1 -1
  35. package/src/core/version-sync.d.ts +28 -7
  36. package/src/core/version-sync.js +56 -22
  37. package/src/core/version-sync.js.map +1 -1
@@ -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"]}
@@ -207,7 +207,7 @@ exports.L1_ROWS = [
207
207
  new L1UseCase(6, '`pnpm test` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', 'ALLOW (handed to L2)', 'none — deliberately untouched, so package-local test runs stay natural', new L1Classification('p', false, false, false, false)),
208
208
  new L1UseCase(10, '`echo "cd sub && git push"` passes', '`pw` / `n` / `root` — row 4', 'ALLOW (handed to L2)', 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape', new L1Classification('p', false, false, false, true)),
209
209
  new L1UseCase(13, 'the same command from a **subagent** runs normally', '`w` / `y` — row 8 does not match', 'ALLOW (handed to L2)', 'none — a subagent pinned to a worktree is the correct pattern', new L1Classification('w', false, false, false, false)),
210
- new L1UseCase(14, 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`', '`w` / `n` / `y` — row 8 does not match', 'ALLOW (handed to L2)', 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you', new L1Classification('w', true, true, false, false)),
210
+ new L1UseCase(14, 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`', '`w` / `n` / `y` — row 8 does not match', 'ALLOW (handed to L2)', 'none — inspection is always open; so are the `Read` tool, `git -C <dir INSIDE this tree> …` and `git show <branch>:<file>`, none of which move you. `git -C <ANOTHER tree>` is a different matter: the harness refuses cross-tree git to a subagent, so it is never the cure for a skew — tell the MAIN agent instead', new L1Classification('w', true, true, false, false)),
211
211
  ]),
212
212
  new L1Row(5, 'pw', '-', '-', 'y', 'sub', exports.ACT_BLOCK, '`cd <root> && <original>`', new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true), 'force-to-root', [
213
213
  new L1UseCase(7, '`git status` from `packages/http/` is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK_AI_CURE', 'Option 1 (preferred): `cd <root> && git status`', new L1Classification('p', false, false, true, false)),
@@ -1 +1 @@
1
- {"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers four questions: do we govern this call at all, does the directory still EXIST, is the\n// WRONG AGENT standing here, and is the agent stranded away from the root? Drawn as a decision matrix\n// that is SEVEN ordered rows over five dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/**\n * V is the same one boolean wearing the doc's own letters: `n` the webpieces versions do NOT agree\n * between this worktree and the main tree, `y` they do.\n *\n * This dimension used to be A (`c` coordinator / `s` subagent). It was replaced rather than removed\n * because agent identity was measured untrustworthy as a proxy for \"which tree am I in\" — a\n * worktree-isolated agent auto-reaped at a turn boundary silently resumes on the primary clone. A\n * version read off the PATH being acted on cannot lie in that way.\n */\nexport type L1VersionSync = 'y' | 'n' | '-';\n\n/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */\nexport type L1ActionKind = 'exempt' | 'down' | 'block';\n\n/**\n * WHICH structural block a blocking row dispatches to. This is the field that makes the array\n * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,\n * so deleting a row from the array removes the block.\n */\nexport type L1BlockId = 'trinary-version-skew' | 'force-to-root' | 'missing-directory';\n\n/**\n * The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a\n * tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use\n * (asking L1_ROWS to classify it would need the very resolution its answer determines).\n *\n * ZERO rather than a seventh row, deliberately. It has to appear in the table — an L1 block the\n * generated doc did not describe is precisely the drift the table exists to prevent, and it was\n * carrying a `KNOWN GAP` comment saying so. But numbering it 7 would assert it sits in the same\n * first-match scan as the others, which is the one thing that is not true about it. Row 0 says\n * \"decided before the scan\" in the number itself. `renderL1Doc()` PRINTS this row above the six, so\n * `row=0` in the L1 log joins to a line the reader can actually find.\n */\nexport const L1_PRESTAGE_ROW = '0';\n\n/**\n * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.\n *\n * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), V (webpieces versions in sync or\n * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).\n */\nexport class L1Classification {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape\n constructor(\n readonly kind: L1Kind,\n readonly versionsSkewed: boolean,\n readonly readOnly: boolean,\n readonly git: boolean,\n readonly atRoot: boolean,\n ) {}\n\n /**\n * The classification the RUNNER enforces on, built from the resolved tree and the caller.\n *\n * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by\n * effective-tree.ts (git has no answer for the directory) and consumed NOWHERE, so a command in no git repo is\n * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what\n * SHOULD happen and is deliberately unreachable from here until the \"Not done\" fix in\n * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,\n * so the two ship together or neither does. Mapping it to `p` here is what preserves today's\n * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.\n */\n // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to\n // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject\n static forEnforcement(\n treeKind: TreeKind,\n versionsSkewed: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f'\n : treeKind === 'missing' ? 'm'\n : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, versionsSkewed, readOnly, git, atRoot);\n }\n}\n\n/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L1Action {\n constructor(readonly label: string, readonly kind: L1ActionKind) {}\n}\n\nexport const ACT_EXEMPT = new L1Action('2 exempt', 'exempt');\nexport const ACT_DOWN = new L1Action('→ L2', 'down');\nexport const ACT_BLOCK = new L1Action('4 block', 'block');\n\n/**\n * The CURE a blocking row prescribes.\n *\n * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,\n * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — \"spawn a\n * subagent bound to the worktree\" — which no allowlist can accept and no reclassification can model,\n * so it declares `runnable: false` and is asserted only on the deny text.\n */\nexport class L1Cure {\n constructor(\n /** How the doc's `why` column spells it. */\n readonly summary: string,\n /** A substring that MUST appear in the deny text the guard emits for this row. */\n readonly denyMention: string,\n readonly runnable: boolean,\n ) {}\n}\n\n/**\n * One row of the \"L1 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed\n * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement\n * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither\n * of which is a row) can carry `null` there.\n */\nexport class L1UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification 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 classification: L1Classification | null = null,\n ) {}\n}\n\n/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */\nexport class L1Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases\n constructor(\n readonly num: number,\n readonly k: L1KindMatch,\n readonly a: L1VersionSync,\n readonly r: L1Flag,\n readonly g: L1Flag,\n readonly p: 'root' | 'sub' | '-',\n readonly action: L1Action,\n /** The `why` cell, verbatim. */\n readonly why: string,\n readonly cure: L1Cure | null,\n readonly blockId: L1BlockId | null,\n readonly useCases: readonly L1UseCase[],\n ) {}\n\n matches(c: L1Classification): boolean {\n if (!this.kindMatches(c.kind)) return false;\n if (this.a !== '-' && (this.a === 'n') !== c.versionsSkewed) return false;\n if (!flagMatches(this.r, c.readOnly)) return false;\n if (!flagMatches(this.g, c.git)) return false;\n return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');\n }\n\n private kindMatches(kind: L1Kind): boolean {\n if (this.k === '-') return true;\n if (this.k === 'pw') return kind === 'p' || kind === 'w';\n return this.k === kind;\n }\n}\n\n// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the\n// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)\n// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module\nfunction flagMatches(cell: L1Flag, value: boolean): boolean {\n if (cell === '-') return true;\n return (cell === 'y') === value;\n}\n\n/**\n * THE seven L1 rows, in first-match-wins order.\n *\n * Rows 3, 5 and 7 are the structural blocks and they run as ONE step (runner.l1LocationBlock) so they\n * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. \"L1 has no\n * objection\" — which is why only those three carry a blockId.\n *\n * Row 7 (`m`, the vanished directory) sits LAST only because row numbers are stable across releases —\n * they are printed in the doc and logged as `row=`, so renumbering rows 1-6 to slot it in front would\n * silently invalidate every existing reference. Position costs nothing here: `m` is matched by no other\n * row, so first-match reaches it wherever it sits.\n */\nexport const L1_ROWS: readonly L1Row[] = [\n new L1Row(1, 'f', '-', '-', '-', '-', ACT_EXEMPT, 'different git repo — hands off', null, null, [\n new L1UseCase(1,\n '`cd repositories/vendored && git commit` goes through untouched',\n '`f` / `y` / - — row 1',\n 'ALLOW_EXEMPT',\n 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off',\n new L1Classification('f', false, false, true, false)),\n ]),\n new L1Row(2, 'o', '-', '-', '-', '-', ACT_DOWN, 'see \"Not done\" below', null, null, []),\n // ROW 3 IS RETIRED — it was coordinator-in-worktree, deleted with CoordinatorWorktreeGuard when the\n // guard hooks went ABSOLUTE (one governor, so the filesystem/governance split it policed became\n // unconstructible). The NUMBER is never reused: row numbers are identity here — they are printed in\n // denies, logged as `row=`, and cited in guards/L1-location.md — so renumbering would silently\n // re-point every historical reference. Its replacement is row 8.\n new L1Row(8, 'w', 'n', 'n', '-', '-', ACT_BLOCK,\n 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it',\n new L1Cure('align the pins (same git hash -> same tracked pin -> install in each tree), work in the main tree, or use a separate clone',\n '@webpieces version SKEW', false),\n 'trinary-version-skew', [\n new L1UseCase(12,\n 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked',\n '`w` / `n` / `n` — row 8',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then `pnpm install` in each tree that has a node_modules — the pin is tracked, so the same commit gives both trees the same version<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a DIFFERENT version, use a separate CLONE — a clone gets its own governance. That is the answer to \"I need a different version\", never to \"I need to install here\": a worktree MAY have its own node_modules (nx, vitest and the eslint plugin all load from it), it just may not hold a different @webpieces version<br>Do NOT: lower the MAIN tree\\'s pin to match — that downgrades every tree, including this session\\'s own governor',\n new L1Classification('w', true, false, false, false)),\n new L1UseCase(16,\n 'a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX`',\n '`w` / `n` / `n` — row 8; in-repo placement is still `w`',\n 'BLOCK_AI_CURE',\n 'A subagent CANNOT fix this alone — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Report to the coordinator: \"my worktree is on X, the main tree is on Y — one of us must move\"<br>Do NOT: expect exemption because it sits under the repo — K is git\\'s `--git-common-dir` answer, not a path test',\n new L1Classification('w', true, false, false, false)),\n ]),\n new L1Row(4, 'pw', '-', '-', 'n', '-', ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [\n new L1UseCase(5,\n '`ls` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — force-to-root has no jurisdiction over non-git commands',\n new L1Classification('p', false, true, false, false)),\n new L1UseCase(6,\n '`pnpm test` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — deliberately untouched, so package-local test runs stay natural',\n new L1Classification('p', false, false, false, false)),\n new L1UseCase(10,\n '`echo \"cd sub && git push\"` passes',\n '`pw` / `n` / `root` — row 4',\n 'ALLOW (handed to L2)',\n 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape',\n new L1Classification('p', false, false, false, true)),\n new L1UseCase(13,\n 'the same command from a **subagent** runs normally',\n '`w` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — a subagent pinned to a worktree is the correct pattern',\n new L1Classification('w', false, false, false, false)),\n new L1UseCase(14,\n 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`',\n '`w` / `n` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you',\n new L1Classification('w', true, true, false, false)),\n ]),\n new L1Row(5, 'pw', '-', '-', 'y', 'sub', ACT_BLOCK, '`cd <root> && <original>`',\n new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true),\n 'force-to-root', [\n new L1UseCase(7,\n '`git status` from `packages/http/` is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(8,\n '`cd packages/http && git status` **typed from the root** is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(11,\n '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one',\n '`pw` / `y` / `sub` — row 5; force-to-root runs first',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\\'s real answer ← costs one extra turn by design; still blocked',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(17,\n 'the printed cure REPLACES your `cd`, it does not stack in front of it',\n '`pw` / `y` / `sub` — row 5, on the cure itself',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed line VERBATIM — `cd <root> && <the work>`, with your own leading `cd` dropped<br>Do NOT: paste `cd <root> && cd <subdir> && <work>`; `effectiveCwd` resolves the leading `cd`s left to right, so that lands in `<subdir>` again and re-fires this exact block',\n new L1Classification('p', false, false, true, false)),\n ]),\n new L1Row(6, 'pw', '-', '-', 'y', 'root', ACT_DOWN, '', null, null, [\n new L1UseCase(9,\n '`cd <root> && git status` passes from anywhere',\n '`pw` / `y` / `root` — row 6',\n 'ALLOW (handed to L2)',\n 'none — this IS the prescribed cure',\n new L1Classification('p', false, false, true, true)),\n ]),\n new L1Row(7, 'm', '-', '-', '-', '-', ACT_BLOCK, 'the directory is GONE — nothing can run there',\n new L1Cure('`cd <root> && <the work>`, never back through the dead path',\n 'no longer exists', true),\n 'missing-directory', [\n new L1UseCase(18,\n 'every command from a worktree another agent REAPED mid-session is blocked',\n '`m` — row 7',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed `cd <root> && <the work>` line — it does NOT route back through the dead path<br>Do NOT: re-`cd` into the worktree, or `git worktree add` it back expecting your uncommitted work; that work is gone',\n new L1Classification('m', false, false, true, false)),\n new L1UseCase(19,\n 'the same block for a NON-git command there — `m` does not care about G',\n '`m` — row 7; K alone decides it',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): the same printed line. A vanished cwd is not a git question — nothing at all can run in a directory that does not exist',\n new L1Classification('m', false, false, false, false)),\n ]),\n];\n\n/**\n * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0\n * allowlist that runs ahead of L1 (15).\n *\n * They are use cases of L1 all the same — \"exempt\" is what emerges when the filter empties the rule\n * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the\n * doc's one numbered table. They carry no classification because no row classifies them.\n */\nexport const L1_UNROWED_USE_CASES: readonly L1UseCase[] = [\n new L1UseCase(2,\n 'Edit `repositories/vendored/foo.ts` allowed even on stale main',\n 'filter — the path is in `excludePaths`',\n 'ALLOW_EXEMPT',\n 'none needed'),\n new L1UseCase(3,\n 'Edit `packages/http/foo.ts` blocked on stale main',\n 'filter keeps the rules → L2 fires',\n 'BLOCK (at L2)',\n 'that is L2\\'s write-on-main verdict, not L1\\'s — follow the L2 message'),\n new L1UseCase(4,\n 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`',\n 'filter, on the TARGET path',\n '→ L2',\n 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to \"fix\" it'),\n new L1UseCase(15,\n '`cd <worktree> && pnpm install` still runs while row 8 is live — it is the CURE',\n 'L0 allowlist, ahead of L1',\n 'ALLOW',\n 'none — a cure must stay reachable from every tree'),\n];\n\n/** Every use case, in the doc's numbering — the order the table is rendered and read in. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module\nexport function allL1UseCases(): readonly L1UseCase[] {\n const all = [...L1_ROWS.flatMap((row: L1Row): readonly L1UseCase[] => row.useCases), ...L1_UNROWED_USE_CASES];\n return all.sort((a: L1UseCase, b: L1UseCase): number => a.num - b.num);\n}\n\n/**\n * FIRST MATCH WINS — the one lookup the guard and the tests share.\n *\n * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a\n * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly\n * that, which is why this returns L1Row rather than L1Row | null.\n */\n// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads\nexport function firstMatchingL1Row(c: L1Classification): L1Row {\n const row = L1_ROWS.find((r: L1Row): boolean => r.matches(c));\n if (row === undefined) throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);\n return row;\n}\n"]}
1
+ {"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers four questions: do we govern this call at all, does the directory still EXIST, is the\n// WRONG AGENT standing here, and is the agent stranded away from the root? Drawn as a decision matrix\n// that is SEVEN ordered rows over five dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/**\n * V is the same one boolean wearing the doc's own letters: `n` the webpieces versions do NOT agree\n * between this worktree and the main tree, `y` they do.\n *\n * This dimension used to be A (`c` coordinator / `s` subagent). It was replaced rather than removed\n * because agent identity was measured untrustworthy as a proxy for \"which tree am I in\" — a\n * worktree-isolated agent auto-reaped at a turn boundary silently resumes on the primary clone. A\n * version read off the PATH being acted on cannot lie in that way.\n */\nexport type L1VersionSync = 'y' | 'n' | '-';\n\n/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */\nexport type L1ActionKind = 'exempt' | 'down' | 'block';\n\n/**\n * WHICH structural block a blocking row dispatches to. This is the field that makes the array\n * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,\n * so deleting a row from the array removes the block.\n */\nexport type L1BlockId = 'trinary-version-skew' | 'force-to-root' | 'missing-directory';\n\n/**\n * The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a\n * tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use\n * (asking L1_ROWS to classify it would need the very resolution its answer determines).\n *\n * ZERO rather than a seventh row, deliberately. It has to appear in the table — an L1 block the\n * generated doc did not describe is precisely the drift the table exists to prevent, and it was\n * carrying a `KNOWN GAP` comment saying so. But numbering it 7 would assert it sits in the same\n * first-match scan as the others, which is the one thing that is not true about it. Row 0 says\n * \"decided before the scan\" in the number itself. `renderL1Doc()` PRINTS this row above the six, so\n * `row=0` in the L1 log joins to a line the reader can actually find.\n */\nexport const L1_PRESTAGE_ROW = '0';\n\n/**\n * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.\n *\n * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), V (webpieces versions in sync or\n * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).\n */\nexport class L1Classification {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape\n constructor(\n readonly kind: L1Kind,\n readonly versionsSkewed: boolean,\n readonly readOnly: boolean,\n readonly git: boolean,\n readonly atRoot: boolean,\n ) {}\n\n /**\n * The classification the RUNNER enforces on, built from the resolved tree and the caller.\n *\n * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by\n * effective-tree.ts (git has no answer for the directory) and consumed NOWHERE, so a command in no git repo is\n * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what\n * SHOULD happen and is deliberately unreachable from here until the \"Not done\" fix in\n * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,\n * so the two ship together or neither does. Mapping it to `p` here is what preserves today's\n * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.\n */\n // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to\n // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject\n static forEnforcement(\n treeKind: TreeKind,\n versionsSkewed: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f'\n : treeKind === 'missing' ? 'm'\n : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, versionsSkewed, readOnly, git, atRoot);\n }\n}\n\n/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L1Action {\n constructor(readonly label: string, readonly kind: L1ActionKind) {}\n}\n\nexport const ACT_EXEMPT = new L1Action('2 exempt', 'exempt');\nexport const ACT_DOWN = new L1Action('→ L2', 'down');\nexport const ACT_BLOCK = new L1Action('4 block', 'block');\n\n/**\n * The CURE a blocking row prescribes.\n *\n * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,\n * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — \"spawn a\n * subagent bound to the worktree\" — which no allowlist can accept and no reclassification can model,\n * so it declares `runnable: false` and is asserted only on the deny text.\n */\nexport class L1Cure {\n constructor(\n /** How the doc's `why` column spells it. */\n readonly summary: string,\n /** A substring that MUST appear in the deny text the guard emits for this row. */\n readonly denyMention: string,\n readonly runnable: boolean,\n ) {}\n}\n\n/**\n * One row of the \"L1 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed\n * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement\n * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither\n * of which is a row) can carry `null` there.\n */\nexport class L1UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification 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 classification: L1Classification | null = null,\n ) {}\n}\n\n/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */\nexport class L1Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases\n constructor(\n readonly num: number,\n readonly k: L1KindMatch,\n readonly a: L1VersionSync,\n readonly r: L1Flag,\n readonly g: L1Flag,\n readonly p: 'root' | 'sub' | '-',\n readonly action: L1Action,\n /** The `why` cell, verbatim. */\n readonly why: string,\n readonly cure: L1Cure | null,\n readonly blockId: L1BlockId | null,\n readonly useCases: readonly L1UseCase[],\n ) {}\n\n matches(c: L1Classification): boolean {\n if (!this.kindMatches(c.kind)) return false;\n if (this.a !== '-' && (this.a === 'n') !== c.versionsSkewed) return false;\n if (!flagMatches(this.r, c.readOnly)) return false;\n if (!flagMatches(this.g, c.git)) return false;\n return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');\n }\n\n private kindMatches(kind: L1Kind): boolean {\n if (this.k === '-') return true;\n if (this.k === 'pw') return kind === 'p' || kind === 'w';\n return this.k === kind;\n }\n}\n\n// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the\n// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)\n// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module\nfunction flagMatches(cell: L1Flag, value: boolean): boolean {\n if (cell === '-') return true;\n return (cell === 'y') === value;\n}\n\n/**\n * THE seven L1 rows, in first-match-wins order.\n *\n * Rows 3, 5 and 7 are the structural blocks and they run as ONE step (runner.l1LocationBlock) so they\n * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. \"L1 has no\n * objection\" — which is why only those three carry a blockId.\n *\n * Row 7 (`m`, the vanished directory) sits LAST only because row numbers are stable across releases —\n * they are printed in the doc and logged as `row=`, so renumbering rows 1-6 to slot it in front would\n * silently invalidate every existing reference. Position costs nothing here: `m` is matched by no other\n * row, so first-match reaches it wherever it sits.\n */\nexport const L1_ROWS: readonly L1Row[] = [\n new L1Row(1, 'f', '-', '-', '-', '-', ACT_EXEMPT, 'different git repo — hands off', null, null, [\n new L1UseCase(1,\n '`cd repositories/vendored && git commit` goes through untouched',\n '`f` / `y` / - — row 1',\n 'ALLOW_EXEMPT',\n 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off',\n new L1Classification('f', false, false, true, false)),\n ]),\n new L1Row(2, 'o', '-', '-', '-', '-', ACT_DOWN, 'see \"Not done\" below', null, null, []),\n // ROW 3 IS RETIRED — it was coordinator-in-worktree, deleted with CoordinatorWorktreeGuard when the\n // guard hooks went ABSOLUTE (one governor, so the filesystem/governance split it policed became\n // unconstructible). The NUMBER is never reused: row numbers are identity here — they are printed in\n // denies, logged as `row=`, and cited in guards/L1-location.md — so renumbering would silently\n // re-point every historical reference. Its replacement is row 8.\n new L1Row(8, 'w', 'n', 'n', '-', '-', ACT_BLOCK,\n 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it',\n new L1Cure('align the pins (same git hash -> same tracked pin -> install in each tree), work in the main tree, or use a separate clone',\n '@webpieces version SKEW', false),\n 'trinary-version-skew', [\n new L1UseCase(12,\n 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked',\n '`w` / `n` / `n` — row 8',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then `pnpm install` in each tree that has a node_modules — the pin is tracked, so the same commit gives both trees the same version<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a DIFFERENT version, use a separate CLONE — a clone gets its own governance. That is the answer to \"I need a different version\", never to \"I need to install here\": a worktree MAY have its own node_modules (nx, vitest and the eslint plugin all load from it), it just may not hold a different @webpieces version<br>Do NOT: lower the MAIN tree\\'s pin to match — that downgrades every tree, including this session\\'s own governor',\n new L1Classification('w', true, false, false, false)),\n new L1UseCase(16,\n 'a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX`',\n '`w` / `n` / `n` — row 8; in-repo placement is still `w`',\n 'BLOCK_AI_CURE',\n 'A subagent CANNOT fix this alone — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Report to the coordinator: \"my worktree is on X, the main tree is on Y — one of us must move\"<br>Do NOT: expect exemption because it sits under the repo — K is git\\'s `--git-common-dir` answer, not a path test',\n new L1Classification('w', true, false, false, false)),\n ]),\n new L1Row(4, 'pw', '-', '-', 'n', '-', ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [\n new L1UseCase(5,\n '`ls` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — force-to-root has no jurisdiction over non-git commands',\n new L1Classification('p', false, true, false, false)),\n new L1UseCase(6,\n '`pnpm test` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — deliberately untouched, so package-local test runs stay natural',\n new L1Classification('p', false, false, false, false)),\n new L1UseCase(10,\n '`echo \"cd sub && git push\"` passes',\n '`pw` / `n` / `root` — row 4',\n 'ALLOW (handed to L2)',\n 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape',\n new L1Classification('p', false, false, false, true)),\n new L1UseCase(13,\n 'the same command from a **subagent** runs normally',\n '`w` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — a subagent pinned to a worktree is the correct pattern',\n new L1Classification('w', false, false, false, false)),\n new L1UseCase(14,\n 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`',\n '`w` / `n` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — inspection is always open; so are the `Read` tool, `git -C <dir INSIDE this tree> …` and `git show <branch>:<file>`, none of which move you. `git -C <ANOTHER tree>` is a different matter: the harness refuses cross-tree git to a subagent, so it is never the cure for a skew — tell the MAIN agent instead',\n new L1Classification('w', true, true, false, false)),\n ]),\n new L1Row(5, 'pw', '-', '-', 'y', 'sub', ACT_BLOCK, '`cd <root> && <original>`',\n new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true),\n 'force-to-root', [\n new L1UseCase(7,\n '`git status` from `packages/http/` is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(8,\n '`cd packages/http && git status` **typed from the root** is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(11,\n '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one',\n '`pw` / `y` / `sub` — row 5; force-to-root runs first',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\\'s real answer ← costs one extra turn by design; still blocked',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(17,\n 'the printed cure REPLACES your `cd`, it does not stack in front of it',\n '`pw` / `y` / `sub` — row 5, on the cure itself',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed line VERBATIM — `cd <root> && <the work>`, with your own leading `cd` dropped<br>Do NOT: paste `cd <root> && cd <subdir> && <work>`; `effectiveCwd` resolves the leading `cd`s left to right, so that lands in `<subdir>` again and re-fires this exact block',\n new L1Classification('p', false, false, true, false)),\n ]),\n new L1Row(6, 'pw', '-', '-', 'y', 'root', ACT_DOWN, '', null, null, [\n new L1UseCase(9,\n '`cd <root> && git status` passes from anywhere',\n '`pw` / `y` / `root` — row 6',\n 'ALLOW (handed to L2)',\n 'none — this IS the prescribed cure',\n new L1Classification('p', false, false, true, true)),\n ]),\n new L1Row(7, 'm', '-', '-', '-', '-', ACT_BLOCK, 'the directory is GONE — nothing can run there',\n new L1Cure('`cd <root> && <the work>`, never back through the dead path',\n 'no longer exists', true),\n 'missing-directory', [\n new L1UseCase(18,\n 'every command from a worktree another agent REAPED mid-session is blocked',\n '`m` — row 7',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed `cd <root> && <the work>` line — it does NOT route back through the dead path<br>Do NOT: re-`cd` into the worktree, or `git worktree add` it back expecting your uncommitted work; that work is gone',\n new L1Classification('m', false, false, true, false)),\n new L1UseCase(19,\n 'the same block for a NON-git command there — `m` does not care about G',\n '`m` — row 7; K alone decides it',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): the same printed line. A vanished cwd is not a git question — nothing at all can run in a directory that does not exist',\n new L1Classification('m', false, false, false, false)),\n ]),\n];\n\n/**\n * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0\n * allowlist that runs ahead of L1 (15).\n *\n * They are use cases of L1 all the same — \"exempt\" is what emerges when the filter empties the rule\n * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the\n * doc's one numbered table. They carry no classification because no row classifies them.\n */\nexport const L1_UNROWED_USE_CASES: readonly L1UseCase[] = [\n new L1UseCase(2,\n 'Edit `repositories/vendored/foo.ts` allowed even on stale main',\n 'filter — the path is in `excludePaths`',\n 'ALLOW_EXEMPT',\n 'none needed'),\n new L1UseCase(3,\n 'Edit `packages/http/foo.ts` blocked on stale main',\n 'filter keeps the rules → L2 fires',\n 'BLOCK (at L2)',\n 'that is L2\\'s write-on-main verdict, not L1\\'s — follow the L2 message'),\n new L1UseCase(4,\n 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`',\n 'filter, on the TARGET path',\n '→ L2',\n 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to \"fix\" it'),\n new L1UseCase(15,\n '`cd <worktree> && pnpm install` still runs while row 8 is live — it is the CURE',\n 'L0 allowlist, ahead of L1',\n 'ALLOW',\n 'none — a cure must stay reachable from every tree'),\n];\n\n/** Every use case, in the doc's numbering — the order the table is rendered and read in. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module\nexport function allL1UseCases(): readonly L1UseCase[] {\n const all = [...L1_ROWS.flatMap((row: L1Row): readonly L1UseCase[] => row.useCases), ...L1_UNROWED_USE_CASES];\n return all.sort((a: L1UseCase, b: L1UseCase): number => a.num - b.num);\n}\n\n/**\n * FIRST MATCH WINS — the one lookup the guard and the tests share.\n *\n * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a\n * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly\n * that, which is why this returns L1Row rather than L1Row | null.\n */\n// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads\nexport function firstMatchingL1Row(c: L1Classification): L1Row {\n const row = L1_ROWS.find((r: L1Row): boolean => r.matches(c));\n if (row === undefined) throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);\n return row;\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`', [