@webpieces/ai-hook-rules 0.4.622 → 0.4.624
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -19
- package/package.json +2 -2
- package/src/adapters/hook-core.js +13 -14
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/hook-registration.d.ts +160 -50
- package/src/bin/hook-registration.js +227 -96
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/managed-env.d.ts +46 -0
- package/src/bin/managed-env.js +50 -0
- package/src/bin/managed-env.js.map +1 -0
- package/src/bin/setup.d.ts +1 -2
- package/src/bin/setup.js +38 -46
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.js +12 -1
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +6 -0
- package/src/bin/shim-deny-reason.js +82 -0
- package/src/bin/shim-deny-reason.js.map +1 -0
- package/src/bin/shim.d.ts +0 -1
- package/src/bin/shim.js +3 -57
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.js +140 -30
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/decision-log.d.ts +3 -3
- package/src/core/decision-log.js +8 -8
- package/src/core/decision-log.js.map +1 -1
- package/src/core/effective-tree.d.ts +5 -2
- package/src/core/effective-tree.js +1 -1
- package/src/core/effective-tree.js.map +1 -1
- package/src/core/l0-matrix.js +15 -13
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l1-doc.js +29 -68
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l1-rows.d.ts +17 -9
- package/src/core/l1-rows.js +18 -13
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/log-stream.d.ts +6 -4
- package/src/core/log-stream.js +6 -4
- package/src/core/log-stream.js.map +1 -1
- package/src/core/log-streams.d.ts +13 -3
- package/src/core/log-streams.js +15 -5
- package/src/core/log-streams.js.map +1 -1
- package/src/core/runner.d.ts +1 -2
- package/src/core/runner.js +29 -24
- package/src/core/runner.js.map +1 -1
- package/src/core/version-sync.d.ts +67 -0
- package/src/core/version-sync.js +148 -0
- package/src/core/version-sync.js.map +1 -0
- package/src/core/webpieces-versions.d.ts +83 -0
- package/src/core/webpieces-versions.js +169 -0
- package/src/core/webpieces-versions.js.map +1 -0
- package/templates/ai-hook.sh +15 -4
- package/templates/claude-settings-hook.json +6 -3
- package/src/bin/guarantee-root.d.ts +0 -95
- package/src/bin/guarantee-root.js +0 -297
- package/src/bin/guarantee-root.js.map +0 -1
- package/src/core/coordinator-worktree.d.ts +0 -61
- package/src/core/coordinator-worktree.js +0 -94
- package/src/core/coordinator-worktree.js.map +0 -1
- package/templates/guarantee-root.sh +0 -113
|
@@ -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, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call, L0_ALLOWLIST, RECOVERY_CMD,\n RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, renderShim, shimStaleDenyReason,\n} from '../bin/shim';\nimport { GUARANTEE_ROOT_MARKER } from '../bin/guarantee-root';\nimport { REGISTRATION_SURFACE } from '../bin/hook-registration';\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_SHIM_STALE, L0_FAULT_UNDECLARED,\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 readonly code: string,\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 `${CONFIG_FILENAME} not found — the webpieces guards cannot run without it, so every other tool call is blocked.\\n` +\n 'THIS IS NOT A DEADLOCK: both options below are explicitly allowed through while this guard is up.\\n' +\n `OPTION 1 (preferred — it needs no other tool and it never prompts) - create ${CONFIG_FILENAME}\\n` +\n 'yourself: any Read, and any Write/Edit whose target is that file, is always allowed through, so\\n' +\n 'you can inspect the repo and write it. The validator reports EVERY missing/invalid entry at once\\n' +\n '(each with the snippet to paste), so a minimal first draft converges in about two passes.\\n' +\n 'OPTION 2 (pick this ONLY at an interactive terminal where you can answer its two prompts) - run\\n' +\n 'EXACTLY this command to seed the config: `pnpm exec wp-install-ai-hooks`. It goes on to wire the\\n' +\n 'Claude Code hooks and asks for a target twice, which hangs a non-interactive session.\\n' +\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.';\n\n// The first line of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail). Kept here so the fault table quotes the same text the runner emits.\nexport const CONFIG_OUT_OF_SYNC_HEADER =\n `${CONFIG_FILENAME} is out of sync — new built-in rules are present that have no entry in ${CONFIG_FILENAME}.`;\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 bashCure('git pull', false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'pull 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 two committed .sh files AND the\n // .claude/settings.json entries that register them. They only work as a set — a settings file left\n // on the old two-absolute-hook form disables the L-1 hook and re-pins every worktree 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 or the .claude/settings.json registration does not match this release',\n 'the guard bin', 'JS',\n [\n // wp-upgrade-shim leads because it is now the ONLY cure that repairs all three, and it is\n // still surgical: it rewrites the two .sh files and the registration and touches no config,\n // and it imports only fs/path so it runs on a tree too broken to load the rule engine. The\n // INSTALLER is deliberately NOT a cure here: it also migrates the config and prompts for a\n // 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 + '(both .sh files and the settings.json registration) and it touches no config; needs '\n + '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, GUARANTEE_ROOT_MARKER, REGISTRATION_SURFACE])),\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 '| code | fault | detected by | enforced in |',\n '|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string => `| \\`${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 '| 2 | any | yes | PASS or ALLOW (see the entry) |',\n '| 3 | any | no | BLOCK — **only the message varies by fault** |',\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 `git pull`; 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/** The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written. */\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 ` The 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, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call, L0_ALLOWLIST, RECOVERY_CMD,\n 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_SHIM_STALE, L0_FAULT_UNDECLARED,\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 readonly code: string,\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 `${CONFIG_FILENAME} not found — the webpieces guards cannot run without it, so every other tool call is blocked.\\n` +\n 'THIS IS NOT A DEADLOCK: both options below are explicitly allowed through while this guard is up.\\n' +\n `OPTION 1 (preferred — it needs no other tool and it never prompts) - create ${CONFIG_FILENAME}\\n` +\n 'yourself: any Read, and any Write/Edit whose target is that file, is always allowed through, so\\n' +\n 'you can inspect the repo and write it. The validator reports EVERY missing/invalid entry at once\\n' +\n '(each with the snippet to paste), so a minimal first draft converges in about two passes.\\n' +\n 'OPTION 2 (pick this ONLY at an interactive terminal where you can answer its two prompts) - run\\n' +\n 'EXACTLY this command to seed the config: `pnpm exec wp-install-ai-hooks`. It goes on to wire the\\n' +\n 'Claude Code hooks and asks for a target twice, which hangs a non-interactive session.\\n' +\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.';\n\n// The first line of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail). Kept here so the fault table quotes the same text the runner emits.\nexport const CONFIG_OUT_OF_SYNC_HEADER =\n `${CONFIG_FILENAME} is out of sync — new built-in rules are present that have no entry in ${CONFIG_FILENAME}.`;\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 bashCure('git pull', false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'pull 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 '| code | fault | detected by | enforced in |',\n '|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string => `| \\`${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 '| 2 | any | yes | PASS or ALLOW (see the entry) |',\n '| 3 | any | no | BLOCK — **only the message varies by fault** |',\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 `git pull`; 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/** The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written. */\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 ` The 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"]}
|
package/src/core/l1-doc.js
CHANGED
|
@@ -46,61 +46,15 @@ function renderL1Doc() {
|
|
|
46
46
|
...renderTail(),
|
|
47
47
|
].join('\n');
|
|
48
48
|
}
|
|
49
|
-
// Why L-1's guarantee-root and L1's force-to-root are NOT the same rule. Its own function because
|
|
50
|
-
// renderHead is at the 80-line method cap, and because this section is one self-contained argument.
|
|
51
|
-
// webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module
|
|
52
|
-
function renderTwoLayerForceToRoot() {
|
|
53
|
-
return [
|
|
54
|
-
'## Force-to-root is TWO rules, in two layers — do not collapse them',
|
|
55
|
-
'',
|
|
56
|
-
'Collapsing them is how the `shellAtRoot` bug happened the first time, and the two now live in different',
|
|
57
|
-
'layers, so it is worth stating plainly:',
|
|
58
|
-
'',
|
|
59
|
-
'| | judged by | what it judges | for | verdict |',
|
|
60
|
-
'|---|---|---|---|---|',
|
|
61
|
-
'| **L-1** | `.claude/webpieces/guarantee-root.sh` (POSIX sh, before any binary) | the `cd` **destination** of the command | **Bash** | ALLOW unless the destination is inside `$CLAUDE_PROJECT_DIR` and holds no `.git` — i.e. **sticky AND unguarded** |',
|
|
62
|
-
'| **L1** | `ForceToRootGuard` (`force-to-root.ts`) | the post-`cd` `effectiveCwd` | **git / gh only** | BLOCK unless it is THE root |',
|
|
63
|
-
'',
|
|
64
|
-
'They ask different questions. L-1 asks *"can the relative guard hooks launch there?"* — because the',
|
|
65
|
-
'guard hooks are registered relative, and a hook that cannot resolve exits 127, which the harness treats',
|
|
66
|
-
'as a non-blocking error and lets the call proceed UNGUARDED. It therefore ALLOWS a `cd` into a foreign',
|
|
67
|
-
'nested clone (its own `.git`) and a `cd` outside the project (the harness resets the cwd next call),',
|
|
68
|
-
'neither of which L1 would tolerate for a `git` command. L1 asks *"is this git command being run from the',
|
|
69
|
-
'one root it is meant to run from?"*, which is a narrower question about a narrower set of commands.',
|
|
70
|
-
'',
|
|
71
|
-
'A denied `cd` never executes — PreToolUse denies the whole tool call before the shell moves — so there',
|
|
72
|
-
'is no bad state to recover from and L-1 needs no cure command on any allowlist.',
|
|
73
|
-
'',
|
|
74
|
-
'### The seam between them — where the worktree bypass actually lived',
|
|
75
|
-
'',
|
|
76
|
-
'Read the two rows above together. L-1 treats **"the destination holds a `.git`"** as **"L1 will govern',
|
|
77
|
-
'this"** — that is the whole reason it allows a `cd` into a nested clone. L1 then treated **"it has its',
|
|
78
|
-
'own git toplevel"** as **"someone else\'s repo, hands off"** and exempted it. Each rule is defensible',
|
|
79
|
-
'alone; COMPOSED, they left a hole exactly the shape of a linked worktree — L-1 deferred to L1, and L1',
|
|
80
|
-
'deferred to nobody. Every bash guard was silently skipped inside `.claude/worktrees/**`, which is the',
|
|
81
|
-
'sandbox agents are told to work in; a live `git push --dry-run` that was blocked from the primary clone',
|
|
82
|
-
'executed from the worktree.',
|
|
83
|
-
'',
|
|
84
|
-
'Resolving K from git\'s dirs closes it. State the seam anyway, because it is a SPEC-level invariant and',
|
|
85
|
-
'not a bug in either rule: **L-1\'s `.git` test is a proxy for "governed elsewhere", and that proxy is',
|
|
86
|
-
'sound only while L1 actually governs every tree whose `.git` belongs to this repo.** Change either side',
|
|
87
|
-
'and check the other.',
|
|
88
|
-
'',
|
|
89
|
-
'The same hole did NOT exist for Read/Write/Edit, which is why it stayed hidden: file tools are judged on',
|
|
90
|
-
'the TARGET PATH, not the cwd (use case 4), so writes into a worktree were guarded the whole time while',
|
|
91
|
-
'Bash in that same directory was exempt.',
|
|
92
|
-
'',
|
|
93
|
-
];
|
|
94
|
-
}
|
|
95
49
|
// The three questions L1 answers, the preamble and the filter — all prose, none of it row data.
|
|
96
50
|
// webpieces-disable no-function-outside-class -- first section of renderL1Doc's string, beside it in this module
|
|
97
51
|
function renderHead() {
|
|
98
52
|
return [
|
|
99
53
|
'# L1 — location',
|
|
100
54
|
'',
|
|
101
|
-
'**Goal: is this call ours to judge, is the
|
|
55
|
+
'**Goal: is this call ours to judge, is this tree governed by the release it asks for, and is git being run from the root?**',
|
|
102
56
|
'',
|
|
103
|
-
'**Config key: `location-guard` (proposed).** Force-to-root and
|
|
57
|
+
'**Config key: `location-guard` (proposed).** Force-to-root and trinary-version-skew both have **no',
|
|
104
58
|
'config key today** and cannot be disabled; `excludePaths` is a top-level block, not a `hookGuards`',
|
|
105
59
|
'entry.',
|
|
106
60
|
'',
|
|
@@ -109,8 +63,8 @@ function renderHead() {
|
|
|
109
63
|
'`TreeKind`) · `packages/tooling/ai-hook-rules/src/core/runner.ts` (`l1LocationBlock`,',
|
|
110
64
|
'`filterByExcludedPaths`, the `foreign` check) · `.../force-to-root.ts` (`ForceToRootGuard`) ·',
|
|
111
65
|
'`packages/tooling/ai-hook-rules/src/core/missing-directory.ts` (`MissingDirectoryGuard`) ·',
|
|
112
|
-
'`packages/tooling/ai-hook-rules/src/core/
|
|
113
|
-
'`
|
|
66
|
+
'`packages/tooling/ai-hook-rules/src/core/version-sync.ts` (`VersionSyncGuard`,',
|
|
67
|
+
'`WebpiecesVersions`).',
|
|
114
68
|
'',
|
|
115
69
|
'L1 answers four questions, and they are genuinely separate:',
|
|
116
70
|
'',
|
|
@@ -119,11 +73,13 @@ function renderHead() {
|
|
|
119
73
|
'2. **Does the directory still EXIST?** — row 7. A worktree reaped out from under a live shell leaves',
|
|
120
74
|
' a cwd that names nothing, and that state needs its own name and its own message, because the',
|
|
121
75
|
' remedy for "you are in a subdirectory" is a `cd` back into the very directory that is gone.',
|
|
122
|
-
'3. **Is
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
76
|
+
'3. **Is this tree governed by a release it did not ask for?** — row 8. The guard hooks are registered',
|
|
77
|
+
' ABSOLUTE, so the MAIN tree governs every tree. That is not new — a linked worktree has no',
|
|
78
|
+
' `node_modules`, so the shim\'s upward walk always ran the main tree\'s binary. It matters when the',
|
|
79
|
+
' two trees PIN different `@webpieces` versions: the worktree is then linted, validated and built by',
|
|
80
|
+
' a release its own manifest does not ask for. Asked of the PATH acted on, never of who is asking —',
|
|
81
|
+
' agent identity was measured untrustworthy for tree detection (a worktree-isolated agent whose tree',
|
|
82
|
+
' is auto-reaped at a turn boundary silently resumes on the primary clone).',
|
|
127
83
|
'4. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where',
|
|
128
84
|
' they are constantly, and `cd` gives them two different ways to be wrong: a `cd` that stays INSIDE',
|
|
129
85
|
' the workspace PERSISTS to later calls (so the shell can be parked in a subdirectory left by an',
|
|
@@ -131,7 +87,6 @@ function renderHead() {
|
|
|
131
87
|
' so — `Shell cwd was reset to <root>`. Neither can be assumed, which is why every remedy names the',
|
|
132
88
|
' root explicitly instead of telling the agent to `cd` first.',
|
|
133
89
|
'',
|
|
134
|
-
...renderTwoLayerForceToRoot(),
|
|
135
90
|
'## Preamble — resolve the target first (Bash only)',
|
|
136
91
|
'',
|
|
137
92
|
'`EffectiveTreeResolver.resolve()` computes `effectiveCwd`: the directory the command actually runs in,',
|
|
@@ -187,7 +142,7 @@ function renderTable() {
|
|
|
187
142
|
'| col | dimension | values |',
|
|
188
143
|
'|---|---|---|',
|
|
189
144
|
'| **K** | tree kind of the resolved target, from git\'s own dirs | `f` foreign repo (a DIFFERENT `--git-common-dir`) · `m` the directory does not exist · `o` outside any repo · `w` a LINKED worktree of ours (`--git-dir` ≠ `--git-common-dir`), wherever it sits on disk · `pw` ours (primary **or** worktree) |',
|
|
190
|
-
'| **
|
|
145
|
+
'| **V** | do the `@webpieces` versions agree between this worktree and the MAIN tree | `n` skewed · `y` in sync |',
|
|
191
146
|
'| **R** | command is provably read-only inspection | `n` · `y` |',
|
|
192
147
|
'| **G** | command invokes git/gh | `n` · `y` |',
|
|
193
148
|
'| **P** | position of the resolved target | `root` · `sub` |',
|
|
@@ -196,27 +151,31 @@ function renderTable() {
|
|
|
196
151
|
'dimensions — the filter is all that applies to them. **The Read tool is never blocked by L1.**',
|
|
197
152
|
'',
|
|
198
153
|
'A linked worktree is deliberately **not** foreign: it is the same project, so the guards run against',
|
|
199
|
-
'THAT tree\'s branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row
|
|
200
|
-
'is the ONE place they separate, and it turns on **
|
|
154
|
+
'THAT tree\'s branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row 8 below',
|
|
155
|
+
'is the ONE place they separate, and it turns on **V** — the versions, read off the tree itself.',
|
|
201
156
|
'',
|
|
202
157
|
'PLACEMENT IS NOT IDENTITY. A worktree checked out INSIDE the repo — `<repo>/.claude/worktrees/agent-XXXX`,',
|
|
203
158
|
'which is where Claude Code puts every agent worktree — is `w` exactly like a sibling `../feature-dir` one.',
|
|
204
159
|
'K comes from git\'s own dirs (`--git-common-dir` is identical for every checkout of one repo,',
|
|
205
160
|
'`--git-dir` differs only in a linked worktree), never from whether the path sits under the governed root.',
|
|
206
161
|
'It used to short-circuit on that path test, so an in-repo worktree read as `f` — every bash guard exempt,',
|
|
207
|
-
'and row
|
|
162
|
+
'and row 8 unreachable, for the only layout the harness actually produces. A nested clone under',
|
|
208
163
|
'`repositories/**` still reads `f`, because its shared git dir is its own.',
|
|
209
164
|
'',
|
|
210
|
-
'`
|
|
211
|
-
'
|
|
212
|
-
'
|
|
165
|
+
'`V` comes from reading manifests off disk — the MAIN tree\'s `pnpm-workspace.yaml` catalog pin, its',
|
|
166
|
+
'installed `node_modules` version, this worktree\'s pin, and this worktree\'s own installed version when',
|
|
167
|
+
'it has one (which it does the moment anyone runs `pnpm add` there). Three always, a fourth when',
|
|
168
|
+
'present. Anything unreadable is NO OPINION, never skew: a guard that cannot measure must not block.',
|
|
169
|
+
'It is deliberately NOT read from `agent_id`/`agent_type` — the dimension this replaced was, and a',
|
|
170
|
+
'worktree-isolated agent was measured resuming on the primary clone after its tree was reaped, so who',
|
|
171
|
+
'is asking cannot be trusted to say which tree is being acted on.',
|
|
213
172
|
'',
|
|
214
173
|
'`R` is `ReadOnlyInspectionScan` — the same paranoid "provably inert" test the unloadable-config escape',
|
|
215
174
|
'hatch uses (allowlisted viewers/searchers only, no redirects, no `sed -i`).',
|
|
216
175
|
'',
|
|
217
176
|
'## Table',
|
|
218
177
|
'',
|
|
219
|
-
'| # | K |
|
|
178
|
+
'| # | K | V | R | G | P | act | why |',
|
|
220
179
|
'|---|---|---|---|---|---|---|---|',
|
|
221
180
|
// Row 0 is the PRE-STAGE (`misplacedCdBlock`). It decides from command TEXT before a tree is
|
|
222
181
|
// resolved, so it cannot be classified over the five dimensions rows 1-6 share — but it IS an
|
|
@@ -227,12 +186,12 @@ function renderTable() {
|
|
|
227
186
|
...l1_rows_1.L1_ROWS.map(tableRow),
|
|
228
187
|
'',
|
|
229
188
|
'Rows 3, 5 and 7 are the structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',
|
|
230
|
-
'so they can never be reordered by accident — row 7 (the directory is gone) first, then row
|
|
189
|
+
'so they can never be reordered by accident — row 7 (the directory is gone) first, then row 8, then',
|
|
231
190
|
'force-to-root. Row 7 is printed LAST above only because row numbers are stable across releases and',
|
|
232
191
|
'renumbering 1-6 would invalidate every `row=` in the logs; `m` matches no other row, so its position',
|
|
233
192
|
'in the scan is immaterial. All three sit after the L0',
|
|
234
193
|
'allowlist, after the `f` check, and after the `excludePaths` filter and the config-sync check. So a',
|
|
235
|
-
'cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0\'s invariant, and row
|
|
194
|
+
'cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0\'s invariant, and row 8 does',
|
|
236
195
|
'not weaken it.',
|
|
237
196
|
'',
|
|
238
197
|
];
|
|
@@ -255,7 +214,9 @@ function renderUseCases() {
|
|
|
255
214
|
'`shellAtRoot || cdsToRoot` — two variables OR\'d, so the same destination got opposite verdicts',
|
|
256
215
|
'depending on where the shell happened to start. It is now one variable, `effectiveCwd === root`.',
|
|
257
216
|
'',
|
|
258
|
-
'Row 12 is the incident that produced table row
|
|
217
|
+
'Row 12 is the incident that produced table row 8, and it is a VERSION SKEW incident — which is why',
|
|
218
|
+
'the row that replaced it measures versions rather than agent identity. The coordinator ran',
|
|
219
|
+
'`git worktree add`, `cd`\'d in,',
|
|
259
220
|
'and worked there. An L0 version-drift fault then fired against the PRIMARY (pin `0.4.545` vs',
|
|
260
221
|
'`node_modules` `0.4.526`) and prescribed `pnpm install` — which ran in the WORKTREE, internally',
|
|
261
222
|
'consistent at `0.4.526`/`0.4.526`, so it succeeded, changed nothing in the measured tree, and the guard',
|
|
@@ -304,7 +265,7 @@ function renderTail() {
|
|
|
304
265
|
'|---|---|---|',
|
|
305
266
|
'| resolver, K | `ai-hook-rules/src/core/effective-tree.ts` | `EffectiveTreeResolver`, `TreeKind` |',
|
|
306
267
|
'| the two structural blocks, in order | `ai-hook-rules/src/core/runner.ts` | `l1LocationBlock` |',
|
|
307
|
-
'|
|
|
268
|
+
'| trinary-version-skew (row 8), V, R | `ai-hook-rules/src/core/version-sync.ts` | `VersionSyncGuard`, `WebpiecesVersions` |',
|
|
308
269
|
'| force-to-root (row 5) | `ai-hook-rules/src/core/force-to-root.ts` | `ForceToRootGuard` |',
|
|
309
270
|
'| the directory is gone (row 7) | `ai-hook-rules/src/core/missing-directory.ts` | `MissingDirectoryGuard` |',
|
|
310
271
|
'| the filter | `ai-hook-rules/src/core/runner.ts` | `filterByExcludedPaths` |',
|
package/src/core/l1-doc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l1-doc.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { L1Row, L1UseCase, L1_ROWS, L1_PRESTAGE_ROW, allL1UseCases } from './l1-rows';\n\n// ---------------------------------------------------------------------------\n// guards/L1-location.md, rendered from L1_ROWS.\n//\n// Same arrangement as l0-matrix.renderGuardMatrixDoc(): one join('\\n') of literal markdown lines with\n// the ROW DATA interpolated from the array the guard consults. Everything that is not row data — every\n// prose section — is a literal line here, because that is the half a generator cannot own.\n//\n// A unit test (l1-matrix.spec.ts) locks guards/L1-location.md byte-identical to renderL1Doc(), and\n// `pnpm guards:generate` rewrites the file. So the table in the doc IS the array, not a description of\n// it. This module, like l1-rows.ts, has no runtime imports outside this pair so the generator can load\n// it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/** A dimension cell: the wildcard renders bare, every value renders as code. */\n// webpieces-disable no-function-outside-class -- pure cell formatter for renderL1Doc below, in this render module\nfunction cell(value: string): string {\n return value === '-' ? '-' : `\\`${value}\\``;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL1Doc below, in this render module\nfunction tableRow(row: L1Row): string {\n const dims = [row.k, row.a, row.r, row.g, row.p].map(cell).join(' | ');\n // Row 6 has no `why` — an EMPTY cell is `| |`, not `| |`. Two spaces would render the same in a\n // browser and fail the byte-lock, which is the whole point of locking bytes rather than markdown.\n const why = row.why === '' ? ' ' : ` ${row.why} `;\n return `| ${row.num} | ${dims} | ${row.action.label} |${why}|`;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL1Doc below, in this render module\nfunction useCaseRow(useCase: L1UseCase): string {\n return `| ${useCase.num} | ${useCase.symptom} | ${useCase.state} | ${useCase.verdict} | ${useCase.fix} |`;\n}\n\n/**\n * Render guards/L1-location.md.\n *\n * Split into three consecutive halves purely to stay inside the method-line budget — the join order is\n * what makes them 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 L1_ROWS, beside the array it reads\nexport function renderL1Doc(): string {\n return [\n ...renderHead(),\n ...renderTable(),\n ...renderUseCases(),\n ...renderTail(),\n ].join('\\n');\n}\n\n// Why L-1's guarantee-root and L1's force-to-root are NOT the same rule. Its own function because\n// renderHead is at the 80-line method cap, and because this section is one self-contained argument.\n// webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module\nfunction renderTwoLayerForceToRoot(): string[] {\n return [\n '## Force-to-root is TWO rules, in two layers — do not collapse them',\n '',\n 'Collapsing them is how the `shellAtRoot` bug happened the first time, and the two now live in different',\n 'layers, so it is worth stating plainly:',\n '',\n '| | judged by | what it judges | for | verdict |',\n '|---|---|---|---|---|',\n '| **L-1** | `.claude/webpieces/guarantee-root.sh` (POSIX sh, before any binary) | the `cd` **destination** of the command | **Bash** | ALLOW unless the destination is inside `$CLAUDE_PROJECT_DIR` and holds no `.git` — i.e. **sticky AND unguarded** |',\n '| **L1** | `ForceToRootGuard` (`force-to-root.ts`) | the post-`cd` `effectiveCwd` | **git / gh only** | BLOCK unless it is THE root |',\n '',\n 'They ask different questions. L-1 asks *\"can the relative guard hooks launch there?\"* — because the',\n 'guard hooks are registered relative, and a hook that cannot resolve exits 127, which the harness treats',\n 'as a non-blocking error and lets the call proceed UNGUARDED. It therefore ALLOWS a `cd` into a foreign',\n 'nested clone (its own `.git`) and a `cd` outside the project (the harness resets the cwd next call),',\n 'neither of which L1 would tolerate for a `git` command. L1 asks *\"is this git command being run from the',\n 'one root it is meant to run from?\"*, which is a narrower question about a narrower set of commands.',\n '',\n 'A denied `cd` never executes — PreToolUse denies the whole tool call before the shell moves — so there',\n 'is no bad state to recover from and L-1 needs no cure command on any allowlist.',\n '',\n '### The seam between them — where the worktree bypass actually lived',\n '',\n 'Read the two rows above together. L-1 treats **\"the destination holds a `.git`\"** as **\"L1 will govern',\n 'this\"** — that is the whole reason it allows a `cd` into a nested clone. L1 then treated **\"it has its',\n 'own git toplevel\"** as **\"someone else\\'s repo, hands off\"** and exempted it. Each rule is defensible',\n 'alone; COMPOSED, they left a hole exactly the shape of a linked worktree — L-1 deferred to L1, and L1',\n 'deferred to nobody. Every bash guard was silently skipped inside `.claude/worktrees/**`, which is the',\n 'sandbox agents are told to work in; a live `git push --dry-run` that was blocked from the primary clone',\n 'executed from the worktree.',\n '',\n 'Resolving K from git\\'s dirs closes it. State the seam anyway, because it is a SPEC-level invariant and',\n 'not a bug in either rule: **L-1\\'s `.git` test is a proxy for \"governed elsewhere\", and that proxy is',\n 'sound only while L1 actually governs every tree whose `.git` belongs to this repo.** Change either side',\n 'and check the other.',\n '',\n 'The same hole did NOT exist for Read/Write/Edit, which is why it stayed hidden: file tools are judged on',\n 'the TARGET PATH, not the cwd (use case 4), so writes into a worktree were guarded the whole time while',\n 'Bash in that same directory was exempt.',\n '',\n ];\n}\n\n// The three questions L1 answers, the preamble and the filter — all prose, none of it row data.\n// webpieces-disable no-function-outside-class -- first section of renderL1Doc's string, beside it in this module\nfunction renderHead(): string[] {\n return [\n '# L1 — location',\n '',\n '**Goal: is this call ours to judge, is the right AGENT making it, and is git being run from the root?**',\n '',\n '**Config key: `location-guard` (proposed).** Force-to-root and coordinator-in-worktree both have **no',\n 'config key today** and cannot be disabled; `excludePaths` is a top-level block, not a `hookGuards`',\n 'entry.',\n '',\n '',\n '**Code:** `packages/tooling/ai-hook-rules/src/core/effective-tree.ts` (`EffectiveTreeResolver`,',\n '`TreeKind`) · `packages/tooling/ai-hook-rules/src/core/runner.ts` (`l1LocationBlock`,',\n '`filterByExcludedPaths`, the `foreign` check) · `.../force-to-root.ts` (`ForceToRootGuard`) ·',\n '`packages/tooling/ai-hook-rules/src/core/missing-directory.ts` (`MissingDirectoryGuard`) ·',\n '`packages/tooling/ai-hook-rules/src/core/coordinator-worktree.ts` (`CoordinatorWorktreeGuard`,',\n '`AgentIdentity`).',\n '',\n 'L1 answers four questions, and they are genuinely separate:',\n '',\n '1. **Do we govern this at all?** — the escape hatches, for other repos and non-governed paths.',\n ' Answered by asking GIT (`--git-common-dir`), never by path math: see the legend under **K**.',\n '2. **Does the directory still EXIST?** — row 7. A worktree reaped out from under a live shell leaves',\n ' a cwd that names nothing, and that state needs its own name and its own message, because the',\n ' remedy for \"you are in a subdirectory\" is a `cd` back into the very directory that is gone.',\n '3. **Is the WRONG AGENT standing here?** — the coordinator must not work inside a linked worktree.',\n ' Its governance is anchored to `$CLAUDE_PROJECT_DIR`, fixed at session start, which does NOT follow',\n ' a `cd`; a coordinator in a worktree therefore has its filesystem in one tree and its guards in',\n ' another, and every fault it is shown is measured against a tree it is not standing in. Work in a',\n ' worktree belongs to a **subagent bound to it**, which has both in one place.',\n '4. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where',\n ' they are constantly, and `cd` gives them two different ways to be wrong: a `cd` that stays INSIDE',\n ' the workspace PERSISTS to later calls (so the shell can be parked in a subdirectory left by an',\n ' unrelated command turns earlier), while a `cd` that LEAVES it is reset by the harness, which says',\n ' so — `Shell cwd was reset to <root>`. Neither can be assumed, which is why every remedy names the',\n ' root explicitly instead of telling the agent to `cd` first.',\n '',\n ...renderTwoLayerForceToRoot(),\n '## Preamble — resolve the target first (Bash only)',\n '',\n '`EffectiveTreeResolver.resolve()` computes `effectiveCwd`: the directory the command actually runs in,',\n 'which is the shell\\'s cwd unless the command leads with `cd <dir> &&`. **K is classified from',\n '`effectiveCwd`, not from the shell\\'s cwd** — so \"a foreign repo that `cd`s into ours\" is not a cell,',\n 'it is simply `pw` after resolution.',\n '',\n 'That holds because K is resolved by ASKING GIT about `effectiveCwd`, not by testing whether the path',\n 'is lexically under the governed root. It has to be said that way round: the resolver used to do the',\n 'path test, and a linked worktree under `.claude/worktrees/**` therefore resolved `f` — every bash',\n 'guard exempt in the one sandbox agents are told to work in. A sentence in this file asserted the',\n 'opposite as fact for several releases, which is how it went unnoticed.',\n '',\n 'Only a LEADING run of `cd`/`pushd` counts. A *trailing* `… && cd <exempt-tree>` must never',\n 'retroactively pull a command out of scope — that would smuggle a root-level `git push` past the',\n 'guards. Quoting is handled by `ShellSegmentScan`, so `echo \"cd sub && git push\"` is one opaque',\n 'segment and its quoted `cd` is never picked up.',\n '',\n ...renderFilterSection(),\n ];\n}\n\n// `excludePaths` — a FILTER over the rule list, not a dimension of the table. Its own function because\n// renderHead is at the 70-line method cap, and because this section is one self-contained argument.\n// webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module\nfunction renderFilterSection(): string[] {\n return [\n '## Filter — not a dimension (all tools)',\n '',\n '`filterByExcludedPaths` drops every rule excluded for this path: the **target path** for',\n 'Read/Write/Edit, `effectiveCwd` for Bash. An empty rule list means allow. This is a filter, not a row:',\n '\"exempt\" is what emerges when the list empties.',\n '',\n '`excludePaths` is **ONE glob list** (canonical: `\"excludePaths\": [\"repositories/**\"]`). The',\n '`{ rules: [...], guards: [...] }` object is **retired and rejected**, with the union it must become',\n 'named in the error. `wp-install-ai-hooks` migrates it in place.',\n '',\n 'This used to be a tolerated fallback, justified here by \"rejecting it would block every Bash/Edit',\n 'including the edit that would fix it.\" **That was never true**, and the fallback it licensed is why',\n 'consumer configs — this repo\\'s own included — sat on the dead shape for releases. A Write/Edit whose',\n 'target is `webpieces.config.json` is an unconditional **PASS** (see the L0 table above), and',\n '`pnpm install` has an installer bypass, so an invalid config can always be repaired from inside the',\n 'block. Config rejection is self-recoverable by construction; see `retired-config-keys.ts` for the',\n 'policy and the reasoning.',\n '',\n ];\n}\n\n// The legend, the table itself (ROW DATA), and the note on the two structural blocks.\n// webpieces-disable no-function-outside-class -- second section of renderL1Doc's string, beside it in this module\nfunction renderTable(): string[] {\n return [\n '## Legend',\n '',\n '| col | dimension | values |',\n '|---|---|---|',\n '| **K** | tree kind of the resolved target, from git\\'s own dirs | `f` foreign repo (a DIFFERENT `--git-common-dir`) · `m` the directory does not exist · `o` outside any repo · `w` a LINKED worktree of ours (`--git-dir` ≠ `--git-common-dir`), wherever it sits on disk · `pw` ours (primary **or** worktree) |',\n '| **A** | who is calling | `c` the coordinator · `s` a subagent (or a caller that cannot tell) |',\n '| **R** | command is provably read-only inspection | `n` · `y` |',\n '| **G** | command invokes git/gh | `n` · `y` |',\n '| **P** | position of the resolved target | `root` · `sub` |',\n '',\n 'All of them are **Bash only**. Read/Write/Edit resolve their own target (`input.filePath`) and have no',\n 'dimensions — the filter is all that applies to them. **The Read tool is never blocked by L1.**',\n '',\n 'A linked worktree is deliberately **not** foreign: it is the same project, so the guards run against',\n 'THAT tree\\'s branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row 3 below',\n 'is the ONE place they separate, and it turns on **A**, not on the tree.',\n '',\n 'PLACEMENT IS NOT IDENTITY. A worktree checked out INSIDE the repo — `<repo>/.claude/worktrees/agent-XXXX`,',\n 'which is where Claude Code puts every agent worktree — is `w` exactly like a sibling `../feature-dir` one.',\n 'K comes from git\\'s own dirs (`--git-common-dir` is identical for every checkout of one repo,',\n '`--git-dir` differs only in a linked worktree), never from whether the path sits under the governed root.',\n 'It used to short-circuit on that path test, so an in-repo worktree read as `f` — every bash guard exempt,',\n 'and row 3 unreachable, for the only layout the harness actually produces. A nested clone under',\n '`repositories/**` still reads `f`, because its shared git dir is its own.',\n '',\n '`A` comes from `agent_id`/`agent_type` in the PreToolUse payload, which Claude Code sends **only inside',\n 'a subagent**. Absent = the coordinator. A caller that cannot read the payload (the openclaw adapter,',\n 'library consumers) resolves to `s` — fail open, never guess someone into a block.',\n '',\n '`R` is `ReadOnlyInspectionScan` — the same paranoid \"provably inert\" test the unloadable-config escape',\n 'hatch uses (allowlisted viewers/searchers only, no redirects, no `sed -i`).',\n '',\n '## Table',\n '',\n '| # | K | A | R | G | P | act | why |',\n '|---|---|---|---|---|---|---|---|',\n // Row 0 is the PRE-STAGE (`misplacedCdBlock`). It decides from command TEXT before a tree is\n // resolved, so it cannot be classified over the five dimensions rows 1-6 share — but it IS an\n // L1 block, and an L1 block the table did not describe is exactly the drift this table exists\n // to prevent. It is numbered 0, not 7, because it does not sit in the first-match scan; and it\n // is PRINTED because `row=0` in the L1 log has to join to something.\n `| ${L1_PRESTAGE_ROW} | – | – | – | – | – | 4 block | a \\`cd\\` that is not leading + literal, judged before any tree is resolved |`,\n ...L1_ROWS.map(tableRow),\n '',\n 'Rows 3, 5 and 7 are the structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',\n 'so they can never be reordered by accident — row 7 (the directory is gone) first, then row 3, then',\n 'force-to-root. Row 7 is printed LAST above only because row numbers are stable across releases and',\n 'renumbering 1-6 would invalidate every `row=` in the logs; `m` matches no other row, so its position',\n 'in the scan is immaterial. All three sit after the L0',\n 'allowlist, after the `f` check, and after the `excludePaths` filter and the config-sync check. So a',\n 'cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0\\'s invariant, and row 3 does',\n 'not weaken it.',\n '',\n ];\n}\n\n// The use-case table (ROW DATA, in the doc's own numbering) and the two notes that follow it.\n// webpieces-disable no-function-outside-class -- third section of renderL1Doc's string, beside it in this module\nfunction renderUseCases(): string[] {\n return [\n '## L1 use cases',\n '',\n 'Same row shape as L0: the **Fix** is literal or it is not a fix. `<root>` is the absolute workspace',\n 'root — the messages name it explicitly rather than telling you to `cd` first, for the reason in the',\n 'section head (neither the shell\\'s cwd nor a `cd`\\'s persistence can be assumed).',\n '',\n '| # | what you SEE (exact symptom) | state | verdict | Fix |',\n '|---|---|---|---|---|',\n ...allL1UseCases().map(useCaseRow),\n '',\n 'Row 8 is the one that changed. It used to be ALLOWED, because the predicate was',\n '`shellAtRoot || cdsToRoot` — two variables OR\\'d, so the same destination got opposite verdicts',\n 'depending on where the shell happened to start. It is now one variable, `effectiveCwd === root`.',\n '',\n 'Row 12 is the incident that produced table row 3. The coordinator ran `git worktree add`, `cd`\\'d in,',\n 'and worked there. An L0 version-drift fault then fired against the PRIMARY (pin `0.4.545` vs',\n '`node_modules` `0.4.526`) and prescribed `pnpm install` — which ran in the WORKTREE, internally',\n 'consistent at `0.4.526`/`0.4.526`, so it succeeded, changed nothing in the measured tree, and the guard',\n 're-denied. Five identical installs later the agent had invented a theory about the harness stripping',\n 'its `cd` and handed the problem to the human. Note what row 15 says: the fix is NOT to deny that',\n 'install. It is to make the split state unreachable, so the wrong-tree install is never plausible.',\n '',\n ];\n}\n\n// The known gap and the code anchors — prose, and the one section that must never be summarised away:\n// three code comments point at it.\n// webpieces-disable no-function-outside-class -- last section of renderL1Doc's string, beside it in this module\nfunction renderTail(): string[] {\n return [\n '## Not done — `o` is not exempt yet',\n '',\n 'Row 2 hands `\\'outside\\'` down to L2 rather than exempting it. `\\'outside\\'` is produced at',\n '`effective-tree.ts` (git has no answer for the directory) carrying `governedRoot`, and **no code branches on it**, so a',\n 'command in no git repo is judged against the governed repo\\'s branch and staleness state. That is a',\n 'wrong verdict, and `exempt` is the right action.',\n '',\n '**It must not ship alone.** Jurisdiction comes from the shell cwd, not from what the command touches,',\n 'so exempting `o` opens a bypass an agent reaches by typing `cd /tmp &&`:',\n '',\n '| command | today | with `o → exempt` alone |',\n '|---|---|---|',\n '| `cd /tmp && ls` | judged against the repo | exempt — **correct** |',\n '| `cd /tmp && git -C $REPO commit` | L2 guards fire | exempt — **every L2 guard bypassed** |',\n '| `cd /tmp && rm -rf $REPO/packages/http/src` | judged | exempt — **unguarded** |',\n '',\n 'The two cases only separate once jurisdiction is judged on **what the command touches** (explicit',\n '`git -C` / `--work-tree`, then path arguments, then the `cd`, then the shell cwd), with the fail-safe',\n 'rule that **any** resolved target inside `governedRoot` means `pw`. Ship the two together, or neither.',\n '',\n 'Tracked in `backlog/bug-bash-guards-judge-the-shell-cwd-not-the-paths-the-command-touches.md` and',\n '`backlog/bug-outside-tree-kind-is-never-consumed-so-a-non-git-dir-is-judged-against-the-governed-repo.md`.',\n 'That resolver has three consumers — L1\\'s K, L2\\'s scope dimension, and `excludePaths` on the Bash path',\n '— which is why the backlog says **fix once**.',\n '',\n '---',\n '',\n '',\n '## Code anchors',\n '',\n '| section | file | symbol |',\n '|---|---|---|',\n '| resolver, K | `ai-hook-rules/src/core/effective-tree.ts` | `EffectiveTreeResolver`, `TreeKind` |',\n '| the two structural blocks, in order | `ai-hook-rules/src/core/runner.ts` | `l1LocationBlock` |',\n '| coordinator-in-worktree (row 3), A, R | `ai-hook-rules/src/core/coordinator-worktree.ts` | `CoordinatorWorktreeGuard`, `AgentIdentity` |',\n '| force-to-root (row 5) | `ai-hook-rules/src/core/force-to-root.ts` | `ForceToRootGuard` |',\n '| the directory is gone (row 7) | `ai-hook-rules/src/core/missing-directory.ts` | `MissingDirectoryGuard` |',\n '| the filter | `ai-hook-rules/src/core/runner.ts` | `filterByExcludedPaths` |',\n '| `excludePaths` shape | `rules-config/src/exclude-hook-paths.ts`, `validate-config.ts`, `retired-config-keys.ts` | `ExcludePaths`, `validateExcludePaths` |',\n '',\n ];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"l1-doc.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { L1Row, L1UseCase, L1_ROWS, L1_PRESTAGE_ROW, allL1UseCases } from './l1-rows';\n\n// ---------------------------------------------------------------------------\n// guards/L1-location.md, rendered from L1_ROWS.\n//\n// Same arrangement as l0-matrix.renderGuardMatrixDoc(): one join('\\n') of literal markdown lines with\n// the ROW DATA interpolated from the array the guard consults. Everything that is not row data — every\n// prose section — is a literal line here, because that is the half a generator cannot own.\n//\n// A unit test (l1-matrix.spec.ts) locks guards/L1-location.md byte-identical to renderL1Doc(), and\n// `pnpm guards:generate` rewrites the file. So the table in the doc IS the array, not a description of\n// it. This module, like l1-rows.ts, has no runtime imports outside this pair so the generator can load\n// it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/** A dimension cell: the wildcard renders bare, every value renders as code. */\n// webpieces-disable no-function-outside-class -- pure cell formatter for renderL1Doc below, in this render module\nfunction cell(value: string): string {\n return value === '-' ? '-' : `\\`${value}\\``;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL1Doc below, in this render module\nfunction tableRow(row: L1Row): string {\n const dims = [row.k, row.a, row.r, row.g, row.p].map(cell).join(' | ');\n // Row 6 has no `why` — an EMPTY cell is `| |`, not `| |`. Two spaces would render the same in a\n // browser and fail the byte-lock, which is the whole point of locking bytes rather than markdown.\n const why = row.why === '' ? ' ' : ` ${row.why} `;\n return `| ${row.num} | ${dims} | ${row.action.label} |${why}|`;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL1Doc below, in this render module\nfunction useCaseRow(useCase: L1UseCase): string {\n return `| ${useCase.num} | ${useCase.symptom} | ${useCase.state} | ${useCase.verdict} | ${useCase.fix} |`;\n}\n\n/**\n * Render guards/L1-location.md.\n *\n * Split into three consecutive halves purely to stay inside the method-line budget — the join order is\n * what makes them 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 L1_ROWS, beside the array it reads\nexport function renderL1Doc(): string {\n return [\n ...renderHead(),\n ...renderTable(),\n ...renderUseCases(),\n ...renderTail(),\n ].join('\\n');\n}\n\n\n// The three questions L1 answers, the preamble and the filter — all prose, none of it row data.\n// webpieces-disable no-function-outside-class -- first section of renderL1Doc's string, beside it in this module\nfunction renderHead(): string[] {\n return [\n '# L1 — location',\n '',\n '**Goal: is this call ours to judge, is this tree governed by the release it asks for, and is git being run from the root?**',\n '',\n '**Config key: `location-guard` (proposed).** Force-to-root and trinary-version-skew both have **no',\n 'config key today** and cannot be disabled; `excludePaths` is a top-level block, not a `hookGuards`',\n 'entry.',\n '',\n '',\n '**Code:** `packages/tooling/ai-hook-rules/src/core/effective-tree.ts` (`EffectiveTreeResolver`,',\n '`TreeKind`) · `packages/tooling/ai-hook-rules/src/core/runner.ts` (`l1LocationBlock`,',\n '`filterByExcludedPaths`, the `foreign` check) · `.../force-to-root.ts` (`ForceToRootGuard`) ·',\n '`packages/tooling/ai-hook-rules/src/core/missing-directory.ts` (`MissingDirectoryGuard`) ·',\n '`packages/tooling/ai-hook-rules/src/core/version-sync.ts` (`VersionSyncGuard`,',\n '`WebpiecesVersions`).',\n '',\n 'L1 answers four questions, and they are genuinely separate:',\n '',\n '1. **Do we govern this at all?** — the escape hatches, for other repos and non-governed paths.',\n ' Answered by asking GIT (`--git-common-dir`), never by path math: see the legend under **K**.',\n '2. **Does the directory still EXIST?** — row 7. A worktree reaped out from under a live shell leaves',\n ' a cwd that names nothing, and that state needs its own name and its own message, because the',\n ' remedy for \"you are in a subdirectory\" is a `cd` back into the very directory that is gone.',\n '3. **Is this tree governed by a release it did not ask for?** — row 8. The guard hooks are registered',\n ' ABSOLUTE, so the MAIN tree governs every tree. That is not new — a linked worktree has no',\n ' `node_modules`, so the shim\\'s upward walk always ran the main tree\\'s binary. It matters when the',\n ' two trees PIN different `@webpieces` versions: the worktree is then linted, validated and built by',\n ' a release its own manifest does not ask for. Asked of the PATH acted on, never of who is asking —',\n ' agent identity was measured untrustworthy for tree detection (a worktree-isolated agent whose tree',\n ' is auto-reaped at a turn boundary silently resumes on the primary clone).',\n '4. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where',\n ' they are constantly, and `cd` gives them two different ways to be wrong: a `cd` that stays INSIDE',\n ' the workspace PERSISTS to later calls (so the shell can be parked in a subdirectory left by an',\n ' unrelated command turns earlier), while a `cd` that LEAVES it is reset by the harness, which says',\n ' so — `Shell cwd was reset to <root>`. Neither can be assumed, which is why every remedy names the',\n ' root explicitly instead of telling the agent to `cd` first.',\n '',\n '## Preamble — resolve the target first (Bash only)',\n '',\n '`EffectiveTreeResolver.resolve()` computes `effectiveCwd`: the directory the command actually runs in,',\n 'which is the shell\\'s cwd unless the command leads with `cd <dir> &&`. **K is classified from',\n '`effectiveCwd`, not from the shell\\'s cwd** — so \"a foreign repo that `cd`s into ours\" is not a cell,',\n 'it is simply `pw` after resolution.',\n '',\n 'That holds because K is resolved by ASKING GIT about `effectiveCwd`, not by testing whether the path',\n 'is lexically under the governed root. It has to be said that way round: the resolver used to do the',\n 'path test, and a linked worktree under `.claude/worktrees/**` therefore resolved `f` — every bash',\n 'guard exempt in the one sandbox agents are told to work in. A sentence in this file asserted the',\n 'opposite as fact for several releases, which is how it went unnoticed.',\n '',\n 'Only a LEADING run of `cd`/`pushd` counts. A *trailing* `… && cd <exempt-tree>` must never',\n 'retroactively pull a command out of scope — that would smuggle a root-level `git push` past the',\n 'guards. Quoting is handled by `ShellSegmentScan`, so `echo \"cd sub && git push\"` is one opaque',\n 'segment and its quoted `cd` is never picked up.',\n '',\n ...renderFilterSection(),\n ];\n}\n\n// `excludePaths` — a FILTER over the rule list, not a dimension of the table. Its own function because\n// renderHead is at the 70-line method cap, and because this section is one self-contained argument.\n// webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module\nfunction renderFilterSection(): string[] {\n return [\n '## Filter — not a dimension (all tools)',\n '',\n '`filterByExcludedPaths` drops every rule excluded for this path: the **target path** for',\n 'Read/Write/Edit, `effectiveCwd` for Bash. An empty rule list means allow. This is a filter, not a row:',\n '\"exempt\" is what emerges when the list empties.',\n '',\n '`excludePaths` is **ONE glob list** (canonical: `\"excludePaths\": [\"repositories/**\"]`). The',\n '`{ rules: [...], guards: [...] }` object is **retired and rejected**, with the union it must become',\n 'named in the error. `wp-install-ai-hooks` migrates it in place.',\n '',\n 'This used to be a tolerated fallback, justified here by \"rejecting it would block every Bash/Edit',\n 'including the edit that would fix it.\" **That was never true**, and the fallback it licensed is why',\n 'consumer configs — this repo\\'s own included — sat on the dead shape for releases. A Write/Edit whose',\n 'target is `webpieces.config.json` is an unconditional **PASS** (see the L0 table above), and',\n '`pnpm install` has an installer bypass, so an invalid config can always be repaired from inside the',\n 'block. Config rejection is self-recoverable by construction; see `retired-config-keys.ts` for the',\n 'policy and the reasoning.',\n '',\n ];\n}\n\n// The legend, the table itself (ROW DATA), and the note on the two structural blocks.\n// webpieces-disable no-function-outside-class -- second section of renderL1Doc's string, beside it in this module\nfunction renderTable(): string[] {\n return [\n '## Legend',\n '',\n '| col | dimension | values |',\n '|---|---|---|',\n '| **K** | tree kind of the resolved target, from git\\'s own dirs | `f` foreign repo (a DIFFERENT `--git-common-dir`) · `m` the directory does not exist · `o` outside any repo · `w` a LINKED worktree of ours (`--git-dir` ≠ `--git-common-dir`), wherever it sits on disk · `pw` ours (primary **or** worktree) |',\n '| **V** | do the `@webpieces` versions agree between this worktree and the MAIN tree | `n` skewed · `y` in sync |',\n '| **R** | command is provably read-only inspection | `n` · `y` |',\n '| **G** | command invokes git/gh | `n` · `y` |',\n '| **P** | position of the resolved target | `root` · `sub` |',\n '',\n 'All of them are **Bash only**. Read/Write/Edit resolve their own target (`input.filePath`) and have no',\n 'dimensions — the filter is all that applies to them. **The Read tool is never blocked by L1.**',\n '',\n 'A linked worktree is deliberately **not** foreign: it is the same project, so the guards run against',\n 'THAT tree\\'s branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row 8 below',\n 'is the ONE place they separate, and it turns on **V** — the versions, read off the tree itself.',\n '',\n 'PLACEMENT IS NOT IDENTITY. A worktree checked out INSIDE the repo — `<repo>/.claude/worktrees/agent-XXXX`,',\n 'which is where Claude Code puts every agent worktree — is `w` exactly like a sibling `../feature-dir` one.',\n 'K comes from git\\'s own dirs (`--git-common-dir` is identical for every checkout of one repo,',\n '`--git-dir` differs only in a linked worktree), never from whether the path sits under the governed root.',\n 'It used to short-circuit on that path test, so an in-repo worktree read as `f` — every bash guard exempt,',\n 'and row 8 unreachable, for the only layout the harness actually produces. A nested clone under',\n '`repositories/**` still reads `f`, because its shared git dir is its own.',\n '',\n '`V` comes from reading manifests off disk — the MAIN tree\\'s `pnpm-workspace.yaml` catalog pin, its',\n 'installed `node_modules` version, this worktree\\'s pin, and this worktree\\'s own installed version when',\n 'it has one (which it does the moment anyone runs `pnpm add` there). Three always, a fourth when',\n 'present. Anything unreadable is NO OPINION, never skew: a guard that cannot measure must not block.',\n 'It is deliberately NOT read from `agent_id`/`agent_type` — the dimension this replaced was, and a',\n 'worktree-isolated agent was measured resuming on the primary clone after its tree was reaped, so who',\n 'is asking cannot be trusted to say which tree is being acted on.',\n '',\n '`R` is `ReadOnlyInspectionScan` — the same paranoid \"provably inert\" test the unloadable-config escape',\n 'hatch uses (allowlisted viewers/searchers only, no redirects, no `sed -i`).',\n '',\n '## Table',\n '',\n '| # | K | V | R | G | P | act | why |',\n '|---|---|---|---|---|---|---|---|',\n // Row 0 is the PRE-STAGE (`misplacedCdBlock`). It decides from command TEXT before a tree is\n // resolved, so it cannot be classified over the five dimensions rows 1-6 share — but it IS an\n // L1 block, and an L1 block the table did not describe is exactly the drift this table exists\n // to prevent. It is numbered 0, not 7, because it does not sit in the first-match scan; and it\n // is PRINTED because `row=0` in the L1 log has to join to something.\n `| ${L1_PRESTAGE_ROW} | – | – | – | – | – | 4 block | a \\`cd\\` that is not leading + literal, judged before any tree is resolved |`,\n ...L1_ROWS.map(tableRow),\n '',\n 'Rows 3, 5 and 7 are the structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',\n 'so they can never be reordered by accident — row 7 (the directory is gone) first, then row 8, then',\n 'force-to-root. Row 7 is printed LAST above only because row numbers are stable across releases and',\n 'renumbering 1-6 would invalidate every `row=` in the logs; `m` matches no other row, so its position',\n 'in the scan is immaterial. All three sit after the L0',\n 'allowlist, after the `f` check, and after the `excludePaths` filter and the config-sync check. So a',\n 'cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0\\'s invariant, and row 8 does',\n 'not weaken it.',\n '',\n ];\n}\n\n// The use-case table (ROW DATA, in the doc's own numbering) and the two notes that follow it.\n// webpieces-disable no-function-outside-class -- third section of renderL1Doc's string, beside it in this module\nfunction renderUseCases(): string[] {\n return [\n '## L1 use cases',\n '',\n 'Same row shape as L0: the **Fix** is literal or it is not a fix. `<root>` is the absolute workspace',\n 'root — the messages name it explicitly rather than telling you to `cd` first, for the reason in the',\n 'section head (neither the shell\\'s cwd nor a `cd`\\'s persistence can be assumed).',\n '',\n '| # | what you SEE (exact symptom) | state | verdict | Fix |',\n '|---|---|---|---|---|',\n ...allL1UseCases().map(useCaseRow),\n '',\n 'Row 8 is the one that changed. It used to be ALLOWED, because the predicate was',\n '`shellAtRoot || cdsToRoot` — two variables OR\\'d, so the same destination got opposite verdicts',\n 'depending on where the shell happened to start. It is now one variable, `effectiveCwd === root`.',\n '',\n 'Row 12 is the incident that produced table row 8, and it is a VERSION SKEW incident — which is why',\n 'the row that replaced it measures versions rather than agent identity. The coordinator ran',\n '`git worktree add`, `cd`\\'d in,',\n 'and worked there. An L0 version-drift fault then fired against the PRIMARY (pin `0.4.545` vs',\n '`node_modules` `0.4.526`) and prescribed `pnpm install` — which ran in the WORKTREE, internally',\n 'consistent at `0.4.526`/`0.4.526`, so it succeeded, changed nothing in the measured tree, and the guard',\n 're-denied. Five identical installs later the agent had invented a theory about the harness stripping',\n 'its `cd` and handed the problem to the human. Note what row 15 says: the fix is NOT to deny that',\n 'install. It is to make the split state unreachable, so the wrong-tree install is never plausible.',\n '',\n ];\n}\n\n// The known gap and the code anchors — prose, and the one section that must never be summarised away:\n// three code comments point at it.\n// webpieces-disable no-function-outside-class -- last section of renderL1Doc's string, beside it in this module\nfunction renderTail(): string[] {\n return [\n '## Not done — `o` is not exempt yet',\n '',\n 'Row 2 hands `\\'outside\\'` down to L2 rather than exempting it. `\\'outside\\'` is produced at',\n '`effective-tree.ts` (git has no answer for the directory) carrying `governedRoot`, and **no code branches on it**, so a',\n 'command in no git repo is judged against the governed repo\\'s branch and staleness state. That is a',\n 'wrong verdict, and `exempt` is the right action.',\n '',\n '**It must not ship alone.** Jurisdiction comes from the shell cwd, not from what the command touches,',\n 'so exempting `o` opens a bypass an agent reaches by typing `cd /tmp &&`:',\n '',\n '| command | today | with `o → exempt` alone |',\n '|---|---|---|',\n '| `cd /tmp && ls` | judged against the repo | exempt — **correct** |',\n '| `cd /tmp && git -C $REPO commit` | L2 guards fire | exempt — **every L2 guard bypassed** |',\n '| `cd /tmp && rm -rf $REPO/packages/http/src` | judged | exempt — **unguarded** |',\n '',\n 'The two cases only separate once jurisdiction is judged on **what the command touches** (explicit',\n '`git -C` / `--work-tree`, then path arguments, then the `cd`, then the shell cwd), with the fail-safe',\n 'rule that **any** resolved target inside `governedRoot` means `pw`. Ship the two together, or neither.',\n '',\n 'Tracked in `backlog/bug-bash-guards-judge-the-shell-cwd-not-the-paths-the-command-touches.md` and',\n '`backlog/bug-outside-tree-kind-is-never-consumed-so-a-non-git-dir-is-judged-against-the-governed-repo.md`.',\n 'That resolver has three consumers — L1\\'s K, L2\\'s scope dimension, and `excludePaths` on the Bash path',\n '— which is why the backlog says **fix once**.',\n '',\n '---',\n '',\n '',\n '## Code anchors',\n '',\n '| section | file | symbol |',\n '|---|---|---|',\n '| resolver, K | `ai-hook-rules/src/core/effective-tree.ts` | `EffectiveTreeResolver`, `TreeKind` |',\n '| the two structural blocks, in order | `ai-hook-rules/src/core/runner.ts` | `l1LocationBlock` |',\n '| trinary-version-skew (row 8), V, R | `ai-hook-rules/src/core/version-sync.ts` | `VersionSyncGuard`, `WebpiecesVersions` |',\n '| force-to-root (row 5) | `ai-hook-rules/src/core/force-to-root.ts` | `ForceToRootGuard` |',\n '| the directory is gone (row 7) | `ai-hook-rules/src/core/missing-directory.ts` | `MissingDirectoryGuard` |',\n '| the filter | `ai-hook-rules/src/core/runner.ts` | `filterByExcludedPaths` |',\n '| `excludePaths` shape | `rules-config/src/exclude-hook-paths.ts`, `validate-config.ts`, `retired-config-keys.ts` | `ExcludePaths`, `validateExcludePaths` |',\n '',\n ];\n}\n"]}
|
package/src/core/l1-rows.d.ts
CHANGED
|
@@ -10,8 +10,16 @@ export type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';
|
|
|
10
10
|
export type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';
|
|
11
11
|
/** R and G are yes/no, written `y`/`n` in the doc, with `-` for "does not matter". */
|
|
12
12
|
export type L1Flag = 'y' | 'n' | '-';
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* V is the same one boolean wearing the doc's own letters: `n` the webpieces versions do NOT agree
|
|
15
|
+
* between this worktree and the main tree, `y` they do.
|
|
16
|
+
*
|
|
17
|
+
* This dimension used to be A (`c` coordinator / `s` subagent). It was replaced rather than removed
|
|
18
|
+
* because agent identity was measured untrustworthy as a proxy for "which tree am I in" — a
|
|
19
|
+
* worktree-isolated agent auto-reaped at a turn boundary silently resumes on the primary clone. A
|
|
20
|
+
* version read off the PATH being acted on cannot lie in that way.
|
|
21
|
+
*/
|
|
22
|
+
export type L1VersionSync = 'y' | 'n' | '-';
|
|
15
23
|
/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */
|
|
16
24
|
export type L1ActionKind = 'exempt' | 'down' | 'block';
|
|
17
25
|
/**
|
|
@@ -19,7 +27,7 @@ export type L1ActionKind = 'exempt' | 'down' | 'block';
|
|
|
19
27
|
* load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,
|
|
20
28
|
* so deleting a row from the array removes the block.
|
|
21
29
|
*/
|
|
22
|
-
export type L1BlockId = '
|
|
30
|
+
export type L1BlockId = 'trinary-version-skew' | 'force-to-root' | 'missing-directory';
|
|
23
31
|
/**
|
|
24
32
|
* The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a
|
|
25
33
|
* tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use
|
|
@@ -36,16 +44,16 @@ export declare const L1_PRESTAGE_ROW = "0";
|
|
|
36
44
|
/**
|
|
37
45
|
* One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
|
|
38
46
|
*
|
|
39
|
-
* The dimensions are exactly the doc's legend: K (tree kind of the resolved target),
|
|
47
|
+
* The dimensions are exactly the doc's legend: K (tree kind of the resolved target), V (webpieces versions in sync or
|
|
40
48
|
* subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).
|
|
41
49
|
*/
|
|
42
50
|
export declare class L1Classification {
|
|
43
51
|
readonly kind: L1Kind;
|
|
44
|
-
readonly
|
|
52
|
+
readonly versionsSkewed: boolean;
|
|
45
53
|
readonly readOnly: boolean;
|
|
46
54
|
readonly git: boolean;
|
|
47
55
|
readonly atRoot: boolean;
|
|
48
|
-
constructor(kind: L1Kind,
|
|
56
|
+
constructor(kind: L1Kind, versionsSkewed: boolean, readOnly: boolean, git: boolean, atRoot: boolean);
|
|
49
57
|
/**
|
|
50
58
|
* The classification the RUNNER enforces on, built from the resolved tree and the caller.
|
|
51
59
|
*
|
|
@@ -57,7 +65,7 @@ export declare class L1Classification {
|
|
|
57
65
|
* so the two ship together or neither does. Mapping it to `p` here is what preserves today's
|
|
58
66
|
* behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.
|
|
59
67
|
*/
|
|
60
|
-
static forEnforcement(treeKind: TreeKind,
|
|
68
|
+
static forEnforcement(treeKind: TreeKind, versionsSkewed: boolean, readOnly: boolean, git: boolean, atRoot: boolean): L1Classification;
|
|
61
69
|
}
|
|
62
70
|
/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */
|
|
63
71
|
export declare class L1Action {
|
|
@@ -109,7 +117,7 @@ export declare class L1UseCase {
|
|
|
109
117
|
export declare class L1Row {
|
|
110
118
|
readonly num: number;
|
|
111
119
|
readonly k: L1KindMatch;
|
|
112
|
-
readonly a:
|
|
120
|
+
readonly a: L1VersionSync;
|
|
113
121
|
readonly r: L1Flag;
|
|
114
122
|
readonly g: L1Flag;
|
|
115
123
|
readonly p: 'root' | 'sub' | '-';
|
|
@@ -119,7 +127,7 @@ export declare class L1Row {
|
|
|
119
127
|
readonly cure: L1Cure | null;
|
|
120
128
|
readonly blockId: L1BlockId | null;
|
|
121
129
|
readonly useCases: readonly L1UseCase[];
|
|
122
|
-
constructor(num: number, k: L1KindMatch, a:
|
|
130
|
+
constructor(num: number, k: L1KindMatch, a: L1VersionSync, r: L1Flag, g: L1Flag, p: 'root' | 'sub' | '-', action: L1Action,
|
|
123
131
|
/** The `why` cell, verbatim. */
|
|
124
132
|
why: string, cure: L1Cure | null, blockId: L1BlockId | null, useCases: readonly L1UseCase[]);
|
|
125
133
|
matches(c: L1Classification): boolean;
|