@webpieces/ai-hook-rules 0.4.625 → 0.4.627
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/package.json +2 -2
- package/src/bin/hook-registration.d.ts +4 -3
- package/src/bin/hook-registration.js +4 -3
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/shim-deny-reason.js +49 -8
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim.js +1 -1
- package/src/bin/shim.js.map +1 -1
- package/src/core/effective-tree.d.ts +32 -3
- package/src/core/effective-tree.js +34 -15
- package/src/core/effective-tree.js.map +1 -1
- package/src/core/l1-doc.js +6 -5
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l1-rows.js +2 -2
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/version-sync.d.ts +19 -0
- package/src/core/version-sync.js +38 -12
- package/src/core/version-sync.js.map +1 -1
- package/templates/ai-hook.sh +1 -1
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\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"]}
|
|
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. A worktree MAY have its own',\n ' `node_modules` (nx, vitest and the eslint plugin all execute there and load from it), and when it',\n ' has none the shim\\'s upward walk runs the main tree\\'s binary. Either way the rule is the same and',\n ' holds whichever registration form — absolute or relative — is live in the consumer: the two trees',\n ' must PIN the same `@webpieces`, or the worktree is linted, validated and built by a release its',\n ' 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.js
CHANGED
|
@@ -198,8 +198,8 @@ exports.L1_ROWS = [
|
|
|
198
198
|
// unconstructible). The NUMBER is never reused: row numbers are identity here — they are printed in
|
|
199
199
|
// denies, logged as `row=`, and cited in guards/L1-location.md — so renumbering would silently
|
|
200
200
|
// re-point every historical reference. Its replacement is row 8.
|
|
201
|
-
new L1Row(8, 'w', 'n', 'n', '-', '-', exports.ACT_BLOCK, 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it', new L1Cure('align the pins (same git hash -> same tracked pin ->
|
|
202
|
-
new L1UseCase(12, 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked', '`w` / `n` / `n` — row 8', 'BLOCK_AI_CURE', 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then
|
|
201
|
+
new L1Row(8, 'w', 'n', 'n', '-', '-', exports.ACT_BLOCK, 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it', new L1Cure('align the pins (same git hash -> same tracked pin -> install in each tree), work in the main tree, or use a separate clone', '@webpieces version SKEW', false), 'trinary-version-skew', [
|
|
202
|
+
new L1UseCase(12, 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked', '`w` / `n` / `n` — row 8', 'BLOCK_AI_CURE', 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then `pnpm install` in each tree that has a node_modules — the pin is tracked, so the same commit gives both trees the same version<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a DIFFERENT version, use a separate CLONE — a clone gets its own governance. That is the answer to "I need a different version", never to "I need to install here": a worktree MAY have its own node_modules (nx, vitest and the eslint plugin all load from it), it just may not hold a different @webpieces version<br>Do NOT: lower the MAIN tree\'s pin to match — that downgrades every tree, including this session\'s own governor', new L1Classification('w', true, false, false, false)),
|
|
203
203
|
new L1UseCase(16, 'a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX`', '`w` / `n` / `n` — row 8; in-repo placement is still `w`', 'BLOCK_AI_CURE', 'A subagent CANNOT fix this alone — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Report to the coordinator: "my worktree is on X, the main tree is on Y — one of us must move"<br>Do NOT: expect exemption because it sits under the repo — K is git\'s `--git-common-dir` answer, not a path test', new L1Classification('w', true, false, false, false)),
|
|
204
204
|
]),
|
|
205
205
|
new L1Row(4, 'pw', '-', '-', 'n', '-', exports.ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [
|
package/src/core/l1-rows.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers four questions: do we govern this call at all, does the directory still EXIST, is the\n// WRONG AGENT standing here, and is the agent stranded away from the root? Drawn as a decision matrix\n// that is SEVEN ordered rows over five dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/**\n * V is the same one boolean wearing the doc's own letters: `n` the webpieces versions do NOT agree\n * between this worktree and the main tree, `y` they do.\n *\n * This dimension used to be A (`c` coordinator / `s` subagent). It was replaced rather than removed\n * because agent identity was measured untrustworthy as a proxy for \"which tree am I in\" — a\n * worktree-isolated agent auto-reaped at a turn boundary silently resumes on the primary clone. A\n * version read off the PATH being acted on cannot lie in that way.\n */\nexport type L1VersionSync = 'y' | 'n' | '-';\n\n/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */\nexport type L1ActionKind = 'exempt' | 'down' | 'block';\n\n/**\n * WHICH structural block a blocking row dispatches to. This is the field that makes the array\n * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,\n * so deleting a row from the array removes the block.\n */\nexport type L1BlockId = 'trinary-version-skew' | 'force-to-root' | 'missing-directory';\n\n/**\n * The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a\n * tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use\n * (asking L1_ROWS to classify it would need the very resolution its answer determines).\n *\n * ZERO rather than a seventh row, deliberately. It has to appear in the table — an L1 block the\n * generated doc did not describe is precisely the drift the table exists to prevent, and it was\n * carrying a `KNOWN GAP` comment saying so. But numbering it 7 would assert it sits in the same\n * first-match scan as the others, which is the one thing that is not true about it. Row 0 says\n * \"decided before the scan\" in the number itself. `renderL1Doc()` PRINTS this row above the six, so\n * `row=0` in the L1 log joins to a line the reader can actually find.\n */\nexport const L1_PRESTAGE_ROW = '0';\n\n/**\n * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.\n *\n * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), V (webpieces versions in sync or\n * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).\n */\nexport class L1Classification {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape\n constructor(\n readonly kind: L1Kind,\n readonly versionsSkewed: boolean,\n readonly readOnly: boolean,\n readonly git: boolean,\n readonly atRoot: boolean,\n ) {}\n\n /**\n * The classification the RUNNER enforces on, built from the resolved tree and the caller.\n *\n * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by\n * effective-tree.ts (git has no answer for the directory) and consumed NOWHERE, so a command in no git repo is\n * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what\n * SHOULD happen and is deliberately unreachable from here until the \"Not done\" fix in\n * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,\n * so the two ship together or neither does. Mapping it to `p` here is what preserves today's\n * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.\n */\n // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to\n // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject\n static forEnforcement(\n treeKind: TreeKind,\n versionsSkewed: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f'\n : treeKind === 'missing' ? 'm'\n : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, versionsSkewed, readOnly, git, atRoot);\n }\n}\n\n/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L1Action {\n constructor(readonly label: string, readonly kind: L1ActionKind) {}\n}\n\nexport const ACT_EXEMPT = new L1Action('2 exempt', 'exempt');\nexport const ACT_DOWN = new L1Action('→ L2', 'down');\nexport const ACT_BLOCK = new L1Action('4 block', 'block');\n\n/**\n * The CURE a blocking row prescribes.\n *\n * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,\n * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — \"spawn a\n * subagent bound to the worktree\" — which no allowlist can accept and no reclassification can model,\n * so it declares `runnable: false` and is asserted only on the deny text.\n */\nexport class L1Cure {\n constructor(\n /** How the doc's `why` column spells it. */\n readonly summary: string,\n /** A substring that MUST appear in the deny text the guard emits for this row. */\n readonly denyMention: string,\n readonly runnable: boolean,\n ) {}\n}\n\n/**\n * One row of the \"L1 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed\n * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement\n * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither\n * of which is a row) can carry `null` there.\n */\nexport class L1UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly classification: L1Classification | null = null,\n ) {}\n}\n\n/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */\nexport class L1Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases\n constructor(\n readonly num: number,\n readonly k: L1KindMatch,\n readonly a: L1VersionSync,\n readonly r: L1Flag,\n readonly g: L1Flag,\n readonly p: 'root' | 'sub' | '-',\n readonly action: L1Action,\n /** The `why` cell, verbatim. */\n readonly why: string,\n readonly cure: L1Cure | null,\n readonly blockId: L1BlockId | null,\n readonly useCases: readonly L1UseCase[],\n ) {}\n\n matches(c: L1Classification): boolean {\n if (!this.kindMatches(c.kind)) return false;\n if (this.a !== '-' && (this.a === 'n') !== c.versionsSkewed) return false;\n if (!flagMatches(this.r, c.readOnly)) return false;\n if (!flagMatches(this.g, c.git)) return false;\n return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');\n }\n\n private kindMatches(kind: L1Kind): boolean {\n if (this.k === '-') return true;\n if (this.k === 'pw') return kind === 'p' || kind === 'w';\n return this.k === kind;\n }\n}\n\n// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the\n// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)\n// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module\nfunction flagMatches(cell: L1Flag, value: boolean): boolean {\n if (cell === '-') return true;\n return (cell === 'y') === value;\n}\n\n/**\n * THE seven L1 rows, in first-match-wins order.\n *\n * Rows 3, 5 and 7 are the structural blocks and they run as ONE step (runner.l1LocationBlock) so they\n * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. \"L1 has no\n * objection\" — which is why only those three carry a blockId.\n *\n * Row 7 (`m`, the vanished directory) sits LAST only because row numbers are stable across releases —\n * they are printed in the doc and logged as `row=`, so renumbering rows 1-6 to slot it in front would\n * silently invalidate every existing reference. Position costs nothing here: `m` is matched by no other\n * row, so first-match reaches it wherever it sits.\n */\nexport const L1_ROWS: readonly L1Row[] = [\n new L1Row(1, 'f', '-', '-', '-', '-', ACT_EXEMPT, 'different git repo — hands off', null, null, [\n new L1UseCase(1,\n '`cd repositories/vendored && git commit` goes through untouched',\n '`f` / `y` / - — row 1',\n 'ALLOW_EXEMPT',\n 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off',\n new L1Classification('f', false, false, true, false)),\n ]),\n new L1Row(2, 'o', '-', '-', '-', '-', ACT_DOWN, 'see \"Not done\" below', null, null, []),\n // ROW 3 IS RETIRED — it was coordinator-in-worktree, deleted with CoordinatorWorktreeGuard when the\n // guard hooks went ABSOLUTE (one governor, so the filesystem/governance split it policed became\n // unconstructible). The NUMBER is never reused: row numbers are identity here — they are printed in\n // denies, logged as `row=`, and cited in guards/L1-location.md — so renumbering would silently\n // re-point every historical reference. Its replacement is row 8.\n new L1Row(8, 'w', 'n', 'n', '-', '-', ACT_BLOCK,\n 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it',\n new L1Cure('align the pins (same git hash -> same tracked pin -> one install in the main tree), work in the main tree, or use a separate clone',\n '@webpieces version SKEW', false),\n 'trinary-version-skew', [\n new L1UseCase(12,\n 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked',\n '`w` / `n` / `n` — row 8',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then ONE `pnpm install` in the MAIN tree — the pin is tracked, so the same commit gives the same version and a worktree needs no install of its own<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a different version, use a separate CLONE — a clone gets its own node_modules and its own governance; a worktree borrows the main tree\\'s and cannot<br>Do NOT: lower the MAIN tree\\'s pin to match — that downgrades every tree, including this session\\'s own governor',\n new L1Classification('w', true, false, false, false)),\n new L1UseCase(16,\n 'a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX`',\n '`w` / `n` / `n` — row 8; in-repo placement is still `w`',\n 'BLOCK_AI_CURE',\n 'A subagent CANNOT fix this alone — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Report to the coordinator: \"my worktree is on X, the main tree is on Y — one of us must move\"<br>Do NOT: expect exemption because it sits under the repo — K is git\\'s `--git-common-dir` answer, not a path test',\n new L1Classification('w', true, false, false, false)),\n ]),\n new L1Row(4, 'pw', '-', '-', 'n', '-', ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [\n new L1UseCase(5,\n '`ls` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — force-to-root has no jurisdiction over non-git commands',\n new L1Classification('p', false, true, false, false)),\n new L1UseCase(6,\n '`pnpm test` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — deliberately untouched, so package-local test runs stay natural',\n new L1Classification('p', false, false, false, false)),\n new L1UseCase(10,\n '`echo \"cd sub && git push\"` passes',\n '`pw` / `n` / `root` — row 4',\n 'ALLOW (handed to L2)',\n 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape',\n new L1Classification('p', false, false, false, true)),\n new L1UseCase(13,\n 'the same command from a **subagent** runs normally',\n '`w` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — a subagent pinned to a worktree is the correct pattern',\n new L1Classification('w', false, false, false, false)),\n new L1UseCase(14,\n 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`',\n '`w` / `n` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you',\n new L1Classification('w', true, true, false, false)),\n ]),\n new L1Row(5, 'pw', '-', '-', 'y', 'sub', ACT_BLOCK, '`cd <root> && <original>`',\n new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true),\n 'force-to-root', [\n new L1UseCase(7,\n '`git status` from `packages/http/` is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(8,\n '`cd packages/http && git status` **typed from the root** is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(11,\n '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one',\n '`pw` / `y` / `sub` — row 5; force-to-root runs first',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\\'s real answer ← costs one extra turn by design; still blocked',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(17,\n 'the printed cure REPLACES your `cd`, it does not stack in front of it',\n '`pw` / `y` / `sub` — row 5, on the cure itself',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed line VERBATIM — `cd <root> && <the work>`, with your own leading `cd` dropped<br>Do NOT: paste `cd <root> && cd <subdir> && <work>`; `effectiveCwd` resolves the leading `cd`s left to right, so that lands in `<subdir>` again and re-fires this exact block',\n new L1Classification('p', false, false, true, false)),\n ]),\n new L1Row(6, 'pw', '-', '-', 'y', 'root', ACT_DOWN, '', null, null, [\n new L1UseCase(9,\n '`cd <root> && git status` passes from anywhere',\n '`pw` / `y` / `root` — row 6',\n 'ALLOW (handed to L2)',\n 'none — this IS the prescribed cure',\n new L1Classification('p', false, false, true, true)),\n ]),\n new L1Row(7, 'm', '-', '-', '-', '-', ACT_BLOCK, 'the directory is GONE — nothing can run there',\n new L1Cure('`cd <root> && <the work>`, never back through the dead path',\n 'no longer exists', true),\n 'missing-directory', [\n new L1UseCase(18,\n 'every command from a worktree another agent REAPED mid-session is blocked',\n '`m` — row 7',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed `cd <root> && <the work>` line — it does NOT route back through the dead path<br>Do NOT: re-`cd` into the worktree, or `git worktree add` it back expecting your uncommitted work; that work is gone',\n new L1Classification('m', false, false, true, false)),\n new L1UseCase(19,\n 'the same block for a NON-git command there — `m` does not care about G',\n '`m` — row 7; K alone decides it',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): the same printed line. A vanished cwd is not a git question — nothing at all can run in a directory that does not exist',\n new L1Classification('m', false, false, false, false)),\n ]),\n];\n\n/**\n * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0\n * allowlist that runs ahead of L1 (15).\n *\n * They are use cases of L1 all the same — \"exempt\" is what emerges when the filter empties the rule\n * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the\n * doc's one numbered table. They carry no classification because no row classifies them.\n */\nexport const L1_UNROWED_USE_CASES: readonly L1UseCase[] = [\n new L1UseCase(2,\n 'Edit `repositories/vendored/foo.ts` allowed even on stale main',\n 'filter — the path is in `excludePaths`',\n 'ALLOW_EXEMPT',\n 'none needed'),\n new L1UseCase(3,\n 'Edit `packages/http/foo.ts` blocked on stale main',\n 'filter keeps the rules → L2 fires',\n 'BLOCK (at L2)',\n 'that is L2\\'s write-on-main verdict, not L1\\'s — follow the L2 message'),\n new L1UseCase(4,\n 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`',\n 'filter, on the TARGET path',\n '→ L2',\n 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to \"fix\" it'),\n new L1UseCase(15,\n '`cd <worktree> && pnpm install` still runs while row 8 is live — it is the CURE',\n 'L0 allowlist, ahead of L1',\n 'ALLOW',\n 'none — a cure must stay reachable from every tree'),\n];\n\n/** Every use case, in the doc's numbering — the order the table is rendered and read in. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module\nexport function allL1UseCases(): readonly L1UseCase[] {\n const all = [...L1_ROWS.flatMap((row: L1Row): readonly L1UseCase[] => row.useCases), ...L1_UNROWED_USE_CASES];\n return all.sort((a: L1UseCase, b: L1UseCase): number => a.num - b.num);\n}\n\n/**\n * FIRST MATCH WINS — the one lookup the guard and the tests share.\n *\n * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a\n * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly\n * that, which is why this returns L1Row rather than L1Row | null.\n */\n// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads\nexport function firstMatchingL1Row(c: L1Classification): L1Row {\n const row = L1_ROWS.find((r: L1Row): boolean => r.matches(c));\n if (row === undefined) throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);\n return row;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers four questions: do we govern this call at all, does the directory still EXIST, is the\n// WRONG AGENT standing here, and is the agent stranded away from the root? Drawn as a decision matrix\n// that is SEVEN ordered rows over five dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/**\n * V is the same one boolean wearing the doc's own letters: `n` the webpieces versions do NOT agree\n * between this worktree and the main tree, `y` they do.\n *\n * This dimension used to be A (`c` coordinator / `s` subagent). It was replaced rather than removed\n * because agent identity was measured untrustworthy as a proxy for \"which tree am I in\" — a\n * worktree-isolated agent auto-reaped at a turn boundary silently resumes on the primary clone. A\n * version read off the PATH being acted on cannot lie in that way.\n */\nexport type L1VersionSync = 'y' | 'n' | '-';\n\n/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */\nexport type L1ActionKind = 'exempt' | 'down' | 'block';\n\n/**\n * WHICH structural block a blocking row dispatches to. This is the field that makes the array\n * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,\n * so deleting a row from the array removes the block.\n */\nexport type L1BlockId = 'trinary-version-skew' | 'force-to-root' | 'missing-directory';\n\n/**\n * The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a\n * tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use\n * (asking L1_ROWS to classify it would need the very resolution its answer determines).\n *\n * ZERO rather than a seventh row, deliberately. It has to appear in the table — an L1 block the\n * generated doc did not describe is precisely the drift the table exists to prevent, and it was\n * carrying a `KNOWN GAP` comment saying so. But numbering it 7 would assert it sits in the same\n * first-match scan as the others, which is the one thing that is not true about it. Row 0 says\n * \"decided before the scan\" in the number itself. `renderL1Doc()` PRINTS this row above the six, so\n * `row=0` in the L1 log joins to a line the reader can actually find.\n */\nexport const L1_PRESTAGE_ROW = '0';\n\n/**\n * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.\n *\n * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), V (webpieces versions in sync or\n * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).\n */\nexport class L1Classification {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape\n constructor(\n readonly kind: L1Kind,\n readonly versionsSkewed: boolean,\n readonly readOnly: boolean,\n readonly git: boolean,\n readonly atRoot: boolean,\n ) {}\n\n /**\n * The classification the RUNNER enforces on, built from the resolved tree and the caller.\n *\n * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by\n * effective-tree.ts (git has no answer for the directory) and consumed NOWHERE, so a command in no git repo is\n * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what\n * SHOULD happen and is deliberately unreachable from here until the \"Not done\" fix in\n * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,\n * so the two ship together or neither does. Mapping it to `p` here is what preserves today's\n * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.\n */\n // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to\n // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject\n static forEnforcement(\n treeKind: TreeKind,\n versionsSkewed: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f'\n : treeKind === 'missing' ? 'm'\n : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, versionsSkewed, readOnly, git, atRoot);\n }\n}\n\n/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L1Action {\n constructor(readonly label: string, readonly kind: L1ActionKind) {}\n}\n\nexport const ACT_EXEMPT = new L1Action('2 exempt', 'exempt');\nexport const ACT_DOWN = new L1Action('→ L2', 'down');\nexport const ACT_BLOCK = new L1Action('4 block', 'block');\n\n/**\n * The CURE a blocking row prescribes.\n *\n * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,\n * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — \"spawn a\n * subagent bound to the worktree\" — which no allowlist can accept and no reclassification can model,\n * so it declares `runnable: false` and is asserted only on the deny text.\n */\nexport class L1Cure {\n constructor(\n /** How the doc's `why` column spells it. */\n readonly summary: string,\n /** A substring that MUST appear in the deny text the guard emits for this row. */\n readonly denyMention: string,\n readonly runnable: boolean,\n ) {}\n}\n\n/**\n * One row of the \"L1 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed\n * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement\n * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither\n * of which is a row) can carry `null` there.\n */\nexport class L1UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly classification: L1Classification | null = null,\n ) {}\n}\n\n/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */\nexport class L1Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases\n constructor(\n readonly num: number,\n readonly k: L1KindMatch,\n readonly a: L1VersionSync,\n readonly r: L1Flag,\n readonly g: L1Flag,\n readonly p: 'root' | 'sub' | '-',\n readonly action: L1Action,\n /** The `why` cell, verbatim. */\n readonly why: string,\n readonly cure: L1Cure | null,\n readonly blockId: L1BlockId | null,\n readonly useCases: readonly L1UseCase[],\n ) {}\n\n matches(c: L1Classification): boolean {\n if (!this.kindMatches(c.kind)) return false;\n if (this.a !== '-' && (this.a === 'n') !== c.versionsSkewed) return false;\n if (!flagMatches(this.r, c.readOnly)) return false;\n if (!flagMatches(this.g, c.git)) return false;\n return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');\n }\n\n private kindMatches(kind: L1Kind): boolean {\n if (this.k === '-') return true;\n if (this.k === 'pw') return kind === 'p' || kind === 'w';\n return this.k === kind;\n }\n}\n\n// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the\n// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)\n// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module\nfunction flagMatches(cell: L1Flag, value: boolean): boolean {\n if (cell === '-') return true;\n return (cell === 'y') === value;\n}\n\n/**\n * THE seven L1 rows, in first-match-wins order.\n *\n * Rows 3, 5 and 7 are the structural blocks and they run as ONE step (runner.l1LocationBlock) so they\n * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. \"L1 has no\n * objection\" — which is why only those three carry a blockId.\n *\n * Row 7 (`m`, the vanished directory) sits LAST only because row numbers are stable across releases —\n * they are printed in the doc and logged as `row=`, so renumbering rows 1-6 to slot it in front would\n * silently invalidate every existing reference. Position costs nothing here: `m` is matched by no other\n * row, so first-match reaches it wherever it sits.\n */\nexport const L1_ROWS: readonly L1Row[] = [\n new L1Row(1, 'f', '-', '-', '-', '-', ACT_EXEMPT, 'different git repo — hands off', null, null, [\n new L1UseCase(1,\n '`cd repositories/vendored && git commit` goes through untouched',\n '`f` / `y` / - — row 1',\n 'ALLOW_EXEMPT',\n 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off',\n new L1Classification('f', false, false, true, false)),\n ]),\n new L1Row(2, 'o', '-', '-', '-', '-', ACT_DOWN, 'see \"Not done\" below', null, null, []),\n // ROW 3 IS RETIRED — it was coordinator-in-worktree, deleted with CoordinatorWorktreeGuard when the\n // guard hooks went ABSOLUTE (one governor, so the filesystem/governance split it policed became\n // unconstructible). The NUMBER is never reused: row numbers are identity here — they are printed in\n // denies, logged as `row=`, and cited in guards/L1-location.md — so renumbering would silently\n // re-point every historical reference. Its replacement is row 8.\n new L1Row(8, 'w', 'n', 'n', '-', '-', ACT_BLOCK,\n 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it',\n new L1Cure('align the pins (same git hash -> same tracked pin -> install in each tree), work in the main tree, or use a separate clone',\n '@webpieces version SKEW', false),\n 'trinary-version-skew', [\n new L1UseCase(12,\n 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked',\n '`w` / `n` / `n` — row 8',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then `pnpm install` in each tree that has a node_modules — the pin is tracked, so the same commit gives both trees the same version<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a DIFFERENT version, use a separate CLONE — a clone gets its own governance. That is the answer to \"I need a different version\", never to \"I need to install here\": a worktree MAY have its own node_modules (nx, vitest and the eslint plugin all load from it), it just may not hold a different @webpieces version<br>Do NOT: lower the MAIN tree\\'s pin to match — that downgrades every tree, including this session\\'s own governor',\n new L1Classification('w', true, false, false, false)),\n new L1UseCase(16,\n 'a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX`',\n '`w` / `n` / `n` — row 8; in-repo placement is still `w`',\n 'BLOCK_AI_CURE',\n 'A subagent CANNOT fix this alone — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Report to the coordinator: \"my worktree is on X, the main tree is on Y — one of us must move\"<br>Do NOT: expect exemption because it sits under the repo — K is git\\'s `--git-common-dir` answer, not a path test',\n new L1Classification('w', true, false, false, false)),\n ]),\n new L1Row(4, 'pw', '-', '-', 'n', '-', ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [\n new L1UseCase(5,\n '`ls` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — force-to-root has no jurisdiction over non-git commands',\n new L1Classification('p', false, true, false, false)),\n new L1UseCase(6,\n '`pnpm test` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — deliberately untouched, so package-local test runs stay natural',\n new L1Classification('p', false, false, false, false)),\n new L1UseCase(10,\n '`echo \"cd sub && git push\"` passes',\n '`pw` / `n` / `root` — row 4',\n 'ALLOW (handed to L2)',\n 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape',\n new L1Classification('p', false, false, false, true)),\n new L1UseCase(13,\n 'the same command from a **subagent** runs normally',\n '`w` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — a subagent pinned to a worktree is the correct pattern',\n new L1Classification('w', false, false, false, false)),\n new L1UseCase(14,\n 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`',\n '`w` / `n` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you',\n new L1Classification('w', true, true, false, false)),\n ]),\n new L1Row(5, 'pw', '-', '-', 'y', 'sub', ACT_BLOCK, '`cd <root> && <original>`',\n new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true),\n 'force-to-root', [\n new L1UseCase(7,\n '`git status` from `packages/http/` is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(8,\n '`cd packages/http && git status` **typed from the root** is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(11,\n '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one',\n '`pw` / `y` / `sub` — row 5; force-to-root runs first',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\\'s real answer ← costs one extra turn by design; still blocked',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(17,\n 'the printed cure REPLACES your `cd`, it does not stack in front of it',\n '`pw` / `y` / `sub` — row 5, on the cure itself',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed line VERBATIM — `cd <root> && <the work>`, with your own leading `cd` dropped<br>Do NOT: paste `cd <root> && cd <subdir> && <work>`; `effectiveCwd` resolves the leading `cd`s left to right, so that lands in `<subdir>` again and re-fires this exact block',\n new L1Classification('p', false, false, true, false)),\n ]),\n new L1Row(6, 'pw', '-', '-', 'y', 'root', ACT_DOWN, '', null, null, [\n new L1UseCase(9,\n '`cd <root> && git status` passes from anywhere',\n '`pw` / `y` / `root` — row 6',\n 'ALLOW (handed to L2)',\n 'none — this IS the prescribed cure',\n new L1Classification('p', false, false, true, true)),\n ]),\n new L1Row(7, 'm', '-', '-', '-', '-', ACT_BLOCK, 'the directory is GONE — nothing can run there',\n new L1Cure('`cd <root> && <the work>`, never back through the dead path',\n 'no longer exists', true),\n 'missing-directory', [\n new L1UseCase(18,\n 'every command from a worktree another agent REAPED mid-session is blocked',\n '`m` — row 7',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed `cd <root> && <the work>` line — it does NOT route back through the dead path<br>Do NOT: re-`cd` into the worktree, or `git worktree add` it back expecting your uncommitted work; that work is gone',\n new L1Classification('m', false, false, true, false)),\n new L1UseCase(19,\n 'the same block for a NON-git command there — `m` does not care about G',\n '`m` — row 7; K alone decides it',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): the same printed line. A vanished cwd is not a git question — nothing at all can run in a directory that does not exist',\n new L1Classification('m', false, false, false, false)),\n ]),\n];\n\n/**\n * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0\n * allowlist that runs ahead of L1 (15).\n *\n * They are use cases of L1 all the same — \"exempt\" is what emerges when the filter empties the rule\n * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the\n * doc's one numbered table. They carry no classification because no row classifies them.\n */\nexport const L1_UNROWED_USE_CASES: readonly L1UseCase[] = [\n new L1UseCase(2,\n 'Edit `repositories/vendored/foo.ts` allowed even on stale main',\n 'filter — the path is in `excludePaths`',\n 'ALLOW_EXEMPT',\n 'none needed'),\n new L1UseCase(3,\n 'Edit `packages/http/foo.ts` blocked on stale main',\n 'filter keeps the rules → L2 fires',\n 'BLOCK (at L2)',\n 'that is L2\\'s write-on-main verdict, not L1\\'s — follow the L2 message'),\n new L1UseCase(4,\n 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`',\n 'filter, on the TARGET path',\n '→ L2',\n 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to \"fix\" it'),\n new L1UseCase(15,\n '`cd <worktree> && pnpm install` still runs while row 8 is live — it is the CURE',\n 'L0 allowlist, ahead of L1',\n 'ALLOW',\n 'none — a cure must stay reachable from every tree'),\n];\n\n/** Every use case, in the doc's numbering — the order the table is rendered and read in. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module\nexport function allL1UseCases(): readonly L1UseCase[] {\n const all = [...L1_ROWS.flatMap((row: L1Row): readonly L1UseCase[] => row.useCases), ...L1_UNROWED_USE_CASES];\n return all.sort((a: L1UseCase, b: L1UseCase): number => a.num - b.num);\n}\n\n/**\n * FIRST MATCH WINS — the one lookup the guard and the tests share.\n *\n * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a\n * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly\n * that, which is why this returns L1Row rather than L1Row | null.\n */\n// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads\nexport function firstMatchingL1Row(c: L1Classification): L1Row {\n const row = L1_ROWS.find((r: L1Row): boolean => r.matches(c));\n if (row === undefined) throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);\n return row;\n}\n"]}
|
|
@@ -31,6 +31,15 @@ import { VersionQuartet } from './webpieces-versions';
|
|
|
31
31
|
* 2. EDIT THE MANIFESTS — `pnpm-workspace.yaml` / `package.json` edits are carved out in the runner
|
|
32
32
|
* the same way `webpieces.config.json` already is, so the cure is typable from inside the block.
|
|
33
33
|
* Reads and read-only inspection are never blocked either, so an agent can always look before it fixes.
|
|
34
|
+
*
|
|
35
|
+
* ─── The MAIN tree is `tree.mainRoot`, never `tree.governedRoot` ───────────────────────────────────
|
|
36
|
+
* The two differ for exactly the reader this guard is for. `governedRoot` is walked up from the payload
|
|
37
|
+
* cwd to the nearest `webpieces.config.json`, and that file is TRACKED — a linked worktree has its own.
|
|
38
|
+
* So for an agent resident in a worktree `governedRoot` IS the worktree, and comparing it against
|
|
39
|
+
* `tree.root` compared the tree with ITSELF: trivially in sync, guard silent. `mainRoot` is git's
|
|
40
|
+
* `<git-common-dir>/..`, i.e. the clone whose `node_modules` actually supplies the judging binary, and
|
|
41
|
+
* it is the same answer from every checkout. Measured 2026-08-10: a worktree on 0.4.624 with its own
|
|
42
|
+
* install, a main clone on 0.4.616, and not one word from this guard.
|
|
34
43
|
*/
|
|
35
44
|
export declare class VersionSyncGuard {
|
|
36
45
|
private readonly inspection;
|
|
@@ -59,6 +68,16 @@ export declare class VersionSyncGuard {
|
|
|
59
68
|
* because those are the operations that would be judged by the wrong release.
|
|
60
69
|
*/
|
|
61
70
|
private isCureOrLook;
|
|
71
|
+
/**
|
|
72
|
+
* Is there a cross-tree comparison to make at all? TWO cheap conditions, no file read behind either:
|
|
73
|
+
*
|
|
74
|
+
* • K is `worktree` — git's `--git-dir ≠ --git-common-dir`, so a repo with no linked worktrees can
|
|
75
|
+
* never reach the manifests. (In the primary clone this is also structural escape #1: "do the
|
|
76
|
+
* work in the main tree" needs no allowlist entry to keep working.)
|
|
77
|
+
* • the two roots are DIFFERENT directories — a tree compared with itself is not a skew, it is the
|
|
78
|
+
* single-tree pin-vs-install question the L0 drift guard already owns.
|
|
79
|
+
*/
|
|
80
|
+
private applies;
|
|
62
81
|
/** Public so the runner can log all four versions on ALLOW as well as on BLOCK (audit, not just deny). */
|
|
63
82
|
quartetFor(tree: EffectiveTree): VersionQuartet;
|
|
64
83
|
private report;
|
package/src/core/version-sync.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.VersionSyncGuard = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const path = tslib_1.__importStar(require("path"));
|
|
4
6
|
const read_only_inspection_1 = require("./read-only-inspection");
|
|
5
7
|
const webpieces_versions_1 = require("./webpieces-versions");
|
|
6
8
|
/**
|
|
@@ -34,6 +36,15 @@ const webpieces_versions_1 = require("./webpieces-versions");
|
|
|
34
36
|
* 2. EDIT THE MANIFESTS — `pnpm-workspace.yaml` / `package.json` edits are carved out in the runner
|
|
35
37
|
* the same way `webpieces.config.json` already is, so the cure is typable from inside the block.
|
|
36
38
|
* Reads and read-only inspection are never blocked either, so an agent can always look before it fixes.
|
|
39
|
+
*
|
|
40
|
+
* ─── The MAIN tree is `tree.mainRoot`, never `tree.governedRoot` ───────────────────────────────────
|
|
41
|
+
* The two differ for exactly the reader this guard is for. `governedRoot` is walked up from the payload
|
|
42
|
+
* cwd to the nearest `webpieces.config.json`, and that file is TRACKED — a linked worktree has its own.
|
|
43
|
+
* So for an agent resident in a worktree `governedRoot` IS the worktree, and comparing it against
|
|
44
|
+
* `tree.root` compared the tree with ITSELF: trivially in sync, guard silent. `mainRoot` is git's
|
|
45
|
+
* `<git-common-dir>/..`, i.e. the clone whose `node_modules` actually supplies the judging binary, and
|
|
46
|
+
* it is the same answer from every checkout. Measured 2026-08-10: a worktree on 0.4.624 with its own
|
|
47
|
+
* install, a main clone on 0.4.616, and not one word from this guard.
|
|
37
48
|
*/
|
|
38
49
|
class VersionSyncGuard {
|
|
39
50
|
inspection = new read_only_inspection_1.ReadOnlyInspectionScan();
|
|
@@ -44,13 +55,13 @@ class VersionSyncGuard {
|
|
|
44
55
|
* lands in the audit log even when nothing blocks.
|
|
45
56
|
*/
|
|
46
57
|
skewed(tree) {
|
|
47
|
-
if (tree
|
|
58
|
+
if (!this.applies(tree))
|
|
48
59
|
return false;
|
|
49
60
|
return !this.quartetFor(tree).inSync;
|
|
50
61
|
}
|
|
51
62
|
/** The deny report, or null to allow. */
|
|
52
63
|
block(command, tree) {
|
|
53
|
-
if (tree
|
|
64
|
+
if (!this.applies(tree))
|
|
54
65
|
return null;
|
|
55
66
|
if (this.inspection.isReadOnlyInspection(command))
|
|
56
67
|
return null;
|
|
@@ -87,9 +98,22 @@ class VersionSyncGuard {
|
|
|
87
98
|
}
|
|
88
99
|
return (head === 'pnpm' || head === 'npm') && (sub === 'install' || sub === 'i');
|
|
89
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Is there a cross-tree comparison to make at all? TWO cheap conditions, no file read behind either:
|
|
103
|
+
*
|
|
104
|
+
* • K is `worktree` — git's `--git-dir ≠ --git-common-dir`, so a repo with no linked worktrees can
|
|
105
|
+
* never reach the manifests. (In the primary clone this is also structural escape #1: "do the
|
|
106
|
+
* work in the main tree" needs no allowlist entry to keep working.)
|
|
107
|
+
* • the two roots are DIFFERENT directories — a tree compared with itself is not a skew, it is the
|
|
108
|
+
* single-tree pin-vs-install question the L0 drift guard already owns.
|
|
109
|
+
*/
|
|
110
|
+
applies(tree) {
|
|
111
|
+
return tree.kind === 'worktree'
|
|
112
|
+
&& path.resolve(tree.mainRoot) !== path.resolve(tree.root);
|
|
113
|
+
}
|
|
90
114
|
/** Public so the runner can log all four versions on ALLOW as well as on BLOCK (audit, not just deny). */
|
|
91
115
|
quartetFor(tree) {
|
|
92
|
-
return this.versions.quartet(tree.
|
|
116
|
+
return this.versions.quartet(tree.mainRoot, tree.root);
|
|
93
117
|
}
|
|
94
118
|
// Short on purpose — L0 ran a deliberate message diet and these blocks regress into a wall of text
|
|
95
119
|
// if each one argues its case. State the skew, show every version WITH its file, give the git cure
|
|
@@ -100,15 +124,17 @@ class VersionSyncGuard {
|
|
|
100
124
|
'',
|
|
101
125
|
...this.versionLines(tree, quartet),
|
|
102
126
|
'',
|
|
103
|
-
`
|
|
104
|
-
`
|
|
127
|
+
` Whichever tree's hooks are live, one of these two releases lints, validates and builds`,
|
|
128
|
+
` this worktree — and it may be the one this manifest does not ask for.`,
|
|
105
129
|
'',
|
|
106
130
|
` FIX (usually just git — the pin is TRACKED, so the same commit gives the same version):`,
|
|
107
|
-
` 1. \`git -C ${tree.
|
|
108
|
-
` then
|
|
131
|
+
` 1. \`git -C ${tree.mainRoot} pull\` and \`git -C ${tree.root} pull\` onto the same main,`,
|
|
132
|
+
` then \`pnpm install\` in each tree that has a node_modules. A worktree MAY have its`,
|
|
133
|
+
` own; what it may not have is a DIFFERENT @webpieces version from the main tree.`,
|
|
109
134
|
` 2. Or work in the MAIN tree instead — it is never blocked by this guard.`,
|
|
110
|
-
` 3. Or, if this tree genuinely needs a
|
|
111
|
-
` worktree: a clone gets its own
|
|
135
|
+
` 3. Or, if this tree genuinely needs a DIFFERENT version, use a separate CLONE, not a`,
|
|
136
|
+
` worktree: a clone gets its own governance. (This is the answer to "I need a different`,
|
|
137
|
+
` version", never to "I need to install here" — installing here is fine.)`,
|
|
112
138
|
'',
|
|
113
139
|
` STILL ALLOWED HERE: every Read, read-only inspection, \`pnpm install\`, \`git pull\`/\`fetch\`,`,
|
|
114
140
|
` and edits to pnpm-workspace.yaml / package.json / webpieces.config.json.`,
|
|
@@ -123,8 +149,8 @@ class VersionSyncGuard {
|
|
|
123
149
|
// "this one is absent" from "I forgot to look".
|
|
124
150
|
versionLines(tree, quartet) {
|
|
125
151
|
const lines = [
|
|
126
|
-
` main pin ${this.show(quartet.main.pinned)} ${tree.
|
|
127
|
-
` main installed ${this.show(quartet.main.installed)} ${tree.
|
|
152
|
+
` main pin ${this.show(quartet.main.pinned)} ${tree.mainRoot}/pnpm-workspace.yaml`,
|
|
153
|
+
` main installed ${this.show(quartet.main.installed)} ${tree.mainRoot}/node_modules/${webpieces_versions_1.UMBRELLA_PACKAGE}`,
|
|
128
154
|
` ^ the binary judging this very call`,
|
|
129
155
|
` this worktree ${this.show(quartet.worktree.pinned)} ${tree.root}/pnpm-workspace.yaml`,
|
|
130
156
|
];
|
|
@@ -132,7 +158,7 @@ class VersionSyncGuard {
|
|
|
132
158
|
lines.push(` its installed ${this.show(quartet.worktree.installed)} ${tree.root}/node_modules/${webpieces_versions_1.UMBRELLA_PACKAGE}`);
|
|
133
159
|
lines.push(' ^ what nx, vitest and eslint load IN this tree');
|
|
134
160
|
}
|
|
135
|
-
const others = this.versions.otherWorktrees(tree.
|
|
161
|
+
const others = this.versions.otherWorktrees(tree.mainRoot, tree.root);
|
|
136
162
|
if (others.length > 0) {
|
|
137
163
|
lines.push(` NOTE ${others.length} other worktree(s) exist and are governed the same way — if they are`);
|
|
138
164
|
lines.push(' skewed too, their agents are already mis-governed. Consider clones, or');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-sync.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/version-sync.ts"],"names":[],"mappings":"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","sourcesContent":["import { EffectiveTree } from './effective-tree';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { UMBRELLA_PACKAGE, VersionQuartet, WebpiecesVersions } from './webpieces-versions';\n\n/**\n * L1 row 8 — a tree may not be worked in while its `@webpieces` version disagrees with the MAIN tree's.\n *\n * ─── WHY THIS EXISTS, and what it replaces ─────────────────────────────────────────────────────────\n * The guard hooks are registered ABSOLUTE (`$CLAUDE_PROJECT_DIR/...`), so the MAIN tree governs every\n * tree. That is not a new imposition — it is what was always happening, because a linked worktree has no\n * `node_modules` and ai-hook.sh's upward walk already executed the main tree's binary. The design now\n * says so out loud, which makes ONE case newly important: a worktree whose branch pins a DIFFERENT\n * release is being linted, validated and built by a release it never asked for.\n *\n * This guard makes that case LOUD instead of silent. It replaces `CoordinatorWorktreeGuard`, and the\n * replacement is strictly better on the axis that matters: the old guard keyed off WHO was asking\n * (coordinator vs subagent), and agent identity was measured untrustworthy — a worktree-isolated agent\n * whose tree is auto-reaped at a turn boundary silently resumes with its cwd on the primary clone\n * (reproduced twice, 2026-08-10). This guard keys off the PATH the command acts on, which cannot lie.\n *\n * ─── IT EXISTS TO STOP A LOOP, not to enforce tidiness ─────────────────────────────────────────────\n * A main/worktree manifest mismatch is exactly the shape that produced the founding incident: an agent\n * is shown a fault measured against one tree, runs the prescribed cure in another, the cure succeeds,\n * nothing the guard measures changes, and the guard re-denies. Five identical no-op `pnpm install`s and\n * a fabricated theory about the harness later, a human had to untangle it. Firing EARLY, with a message\n * that names all the versions and all their files, is the whole point. Any future proposal to soften\n * this to a warning must answer: what stops the five-install loop instead?\n *\n * ─── Never a deadlock ──────────────────────────────────────────────────────────────────────────────\n * Two structurally independent escapes, and neither depends on an allowlist regex staying in step:\n * 1. WORK IN THE MAIN TREE — a main-tree-targeted command cannot classify as `worktree`, so it never\n * reaches this guard at all. No allowlist entry can break it because none is involved.\n * 2. EDIT THE MANIFESTS — `pnpm-workspace.yaml` / `package.json` edits are carved out in the runner\n * the same way `webpieces.config.json` already is, so the cure is typable from inside the block.\n * Reads and read-only inspection are never blocked either, so an agent can always look before it fixes.\n */\nexport class VersionSyncGuard {\n private readonly inspection = new ReadOnlyInspectionScan();\n private readonly versions = new WebpiecesVersions();\n\n /**\n * True when this tree is a linked worktree whose webpieces version disagrees with the main tree's.\n * This is the `V` dimension of the L1 matrix; the runner asks it for EVERY Bash call so the answer\n * lands in the audit log even when nothing blocks.\n */\n skewed(tree: EffectiveTree): boolean {\n if (tree.kind !== 'worktree') return false;\n return !this.quartetFor(tree).inSync;\n }\n\n /** The deny report, or null to allow. */\n block(command: string, tree: EffectiveTree): string | null {\n if (tree.kind !== 'worktree') return null;\n if (this.inspection.isReadOnlyInspection(command)) return null;\n if (this.isCureOrLook(command)) return null;\n const quartet = this.quartetFor(tree);\n if (quartet.inSync) return null;\n return this.report(tree, quartet);\n }\n\n /**\n * Commands that must pass EVEN WHILE THIS GUARD IS BLOCKING, because they are how you get unblocked\n * — or how you look at the tree first.\n *\n * `ReadOnlyInspectionScan` deliberately excludes git and gh OUTRIGHT (\"the guards exist to police\n * git, and read-only git is not a line worth drawing while flying blind\"), which is right for the\n * guards that police git but WRONG here: this guard's own prescribed cure is `git pull` in both\n * trees. Without this carve-out the guard would deny the exact command it tells the reader to run —\n * the single failure shape this repo has been burned by most often, and the reason the deny text is\n * allowed to promise \"STILL ALLOWED HERE: ... pnpm install, git pull/fetch\".\n *\n * Deliberately NARROW: fetching, pulling and installing cannot make a skew worse, and every one of\n * them moves the tree toward agreement. Anything that BUILDS, TESTS or COMMITS is still blocked,\n * because those are the operations that would be judged by the wrong release.\n */\n private isCureOrLook(command: string): boolean {\n const words = command.trim().split(/\\s+/);\n const head = words[0] ?? '';\n const sub = words[1] ?? '';\n if (head === 'git' || head === 'gh') {\n // `git -C <dir> <sub>` names its own directory; take the first non-flag word after it.\n const subcommand = sub === '-C' ? (words[3] ?? '') : sub;\n return ['pull', 'fetch', 'status', 'log', 'diff', 'show', 'branch', 'rev-parse', 'worktree'].includes(subcommand);\n }\n return (head === 'pnpm' || head === 'npm') && (sub === 'install' || sub === 'i');\n }\n\n /** Public so the runner can log all four versions on ALLOW as well as on BLOCK (audit, not just deny). */\n quartetFor(tree: EffectiveTree): VersionQuartet {\n return this.versions.quartet(tree.governedRoot, tree.root);\n }\n\n // Short on purpose — L0 ran a deliberate message diet and these blocks regress into a wall of text\n // if each one argues its case. State the skew, show every version WITH its file, give the git cure\n // first, then the two structural escapes, then what is still allowed.\n private report(tree: EffectiveTree, quartet: VersionQuartet): string {\n return [\n `❌ @webpieces version SKEW — this worktree and the main tree disagree, so work here is blocked.`,\n '',\n ...this.versionLines(tree, quartet),\n '',\n ` The guard hooks are ABSOLUTE, so the MAIN tree governs every tree. This worktree would be`,\n ` linted, validated and built by a release its own manifest does not ask for.`,\n '',\n ` FIX (usually just git — the pin is TRACKED, so the same commit gives the same version):`,\n ` 1. \\`git -C ${tree.governedRoot} pull\\` and \\`git -C ${tree.root} pull\\` onto the same main,`,\n ` then ONE \\`cd '${tree.governedRoot}' && pnpm install\\`. A worktree needs no install of its own.`,\n ` 2. Or work in the MAIN tree instead — it is never blocked by this guard.`,\n ` 3. Or, if this tree genuinely needs a different version, use a separate CLONE, not a`,\n ` worktree: a clone gets its own node_modules and its own governance; a worktree cannot.`,\n '',\n ` STILL ALLOWED HERE: every Read, read-only inspection, \\`pnpm install\\`, \\`git pull\\`/\\`fetch\\`,`,\n ` and edits to pnpm-workspace.yaml / package.json / webpieces.config.json.`,\n ` Do NOT lower the MAIN tree's pin to match — that downgrades every tree, including this`,\n ` session's own governor. If you are a SUBAGENT, you cannot fix the main tree: report to your`,\n ` coordinator that one of you must move to the other's version.`,\n ].join('\\n');\n }\n\n // Every version WITH the file it came from. An agent that is told \"they disagree\" without being told\n // WHICH FILE to edit re-derives it by grepping, which is exactly the turn-burning this guard exists\n // to prevent. Unreadable legs are printed as `-` rather than omitted, so the reader can tell\n // \"this one is absent\" from \"I forgot to look\".\n private versionLines(tree: EffectiveTree, quartet: VersionQuartet): readonly string[] {\n const lines = [\n ` main pin ${this.show(quartet.main.pinned)} ${tree.governedRoot}/pnpm-workspace.yaml`,\n ` main installed ${this.show(quartet.main.installed)} ${tree.governedRoot}/node_modules/${UMBRELLA_PACKAGE}`,\n ` ^ the binary judging this very call`,\n ` this worktree ${this.show(quartet.worktree.pinned)} ${tree.root}/pnpm-workspace.yaml`,\n ];\n if (quartet.worktree.installed !== null) {\n lines.push(` its installed ${this.show(quartet.worktree.installed)} ${tree.root}/node_modules/${UMBRELLA_PACKAGE}`);\n lines.push(' ^ what nx, vitest and eslint load IN this tree');\n }\n const others = this.versions.otherWorktrees(tree.governedRoot, tree.root);\n if (others.length > 0) {\n lines.push(` NOTE ${others.length} other worktree(s) exist and are governed the same way — if they are`);\n lines.push(' skewed too, their agents are already mis-governed. Consider clones, or');\n lines.push(' serializing the work in the main tree.');\n }\n return lines;\n }\n\n private show(version: string | null): string {\n return (version ?? '-').padEnd(10);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"version-sync.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/version-sync.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { EffectiveTree } from './effective-tree';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { UMBRELLA_PACKAGE, VersionQuartet, WebpiecesVersions } from './webpieces-versions';\n\n/**\n * L1 row 8 — a tree may not be worked in while its `@webpieces` version disagrees with the MAIN tree's.\n *\n * ─── WHY THIS EXISTS, and what it replaces ─────────────────────────────────────────────────────────\n * The guard hooks are registered ABSOLUTE (`$CLAUDE_PROJECT_DIR/...`), so the MAIN tree governs every\n * tree. That is not a new imposition — it is what was always happening, because a linked worktree has no\n * `node_modules` and ai-hook.sh's upward walk already executed the main tree's binary. The design now\n * says so out loud, which makes ONE case newly important: a worktree whose branch pins a DIFFERENT\n * release is being linted, validated and built by a release it never asked for.\n *\n * This guard makes that case LOUD instead of silent. It replaces `CoordinatorWorktreeGuard`, and the\n * replacement is strictly better on the axis that matters: the old guard keyed off WHO was asking\n * (coordinator vs subagent), and agent identity was measured untrustworthy — a worktree-isolated agent\n * whose tree is auto-reaped at a turn boundary silently resumes with its cwd on the primary clone\n * (reproduced twice, 2026-08-10). This guard keys off the PATH the command acts on, which cannot lie.\n *\n * ─── IT EXISTS TO STOP A LOOP, not to enforce tidiness ─────────────────────────────────────────────\n * A main/worktree manifest mismatch is exactly the shape that produced the founding incident: an agent\n * is shown a fault measured against one tree, runs the prescribed cure in another, the cure succeeds,\n * nothing the guard measures changes, and the guard re-denies. Five identical no-op `pnpm install`s and\n * a fabricated theory about the harness later, a human had to untangle it. Firing EARLY, with a message\n * that names all the versions and all their files, is the whole point. Any future proposal to soften\n * this to a warning must answer: what stops the five-install loop instead?\n *\n * ─── Never a deadlock ──────────────────────────────────────────────────────────────────────────────\n * Two structurally independent escapes, and neither depends on an allowlist regex staying in step:\n * 1. WORK IN THE MAIN TREE — a main-tree-targeted command cannot classify as `worktree`, so it never\n * reaches this guard at all. No allowlist entry can break it because none is involved.\n * 2. EDIT THE MANIFESTS — `pnpm-workspace.yaml` / `package.json` edits are carved out in the runner\n * the same way `webpieces.config.json` already is, so the cure is typable from inside the block.\n * Reads and read-only inspection are never blocked either, so an agent can always look before it fixes.\n *\n * ─── The MAIN tree is `tree.mainRoot`, never `tree.governedRoot` ───────────────────────────────────\n * The two differ for exactly the reader this guard is for. `governedRoot` is walked up from the payload\n * cwd to the nearest `webpieces.config.json`, and that file is TRACKED — a linked worktree has its own.\n * So for an agent resident in a worktree `governedRoot` IS the worktree, and comparing it against\n * `tree.root` compared the tree with ITSELF: trivially in sync, guard silent. `mainRoot` is git's\n * `<git-common-dir>/..`, i.e. the clone whose `node_modules` actually supplies the judging binary, and\n * it is the same answer from every checkout. Measured 2026-08-10: a worktree on 0.4.624 with its own\n * install, a main clone on 0.4.616, and not one word from this guard.\n */\nexport class VersionSyncGuard {\n private readonly inspection = new ReadOnlyInspectionScan();\n private readonly versions = new WebpiecesVersions();\n\n /**\n * True when this tree is a linked worktree whose webpieces version disagrees with the main tree's.\n * This is the `V` dimension of the L1 matrix; the runner asks it for EVERY Bash call so the answer\n * lands in the audit log even when nothing blocks.\n */\n skewed(tree: EffectiveTree): boolean {\n if (!this.applies(tree)) return false;\n return !this.quartetFor(tree).inSync;\n }\n\n /** The deny report, or null to allow. */\n block(command: string, tree: EffectiveTree): string | null {\n if (!this.applies(tree)) return null;\n if (this.inspection.isReadOnlyInspection(command)) return null;\n if (this.isCureOrLook(command)) return null;\n const quartet = this.quartetFor(tree);\n if (quartet.inSync) return null;\n return this.report(tree, quartet);\n }\n\n /**\n * Commands that must pass EVEN WHILE THIS GUARD IS BLOCKING, because they are how you get unblocked\n * — or how you look at the tree first.\n *\n * `ReadOnlyInspectionScan` deliberately excludes git and gh OUTRIGHT (\"the guards exist to police\n * git, and read-only git is not a line worth drawing while flying blind\"), which is right for the\n * guards that police git but WRONG here: this guard's own prescribed cure is `git pull` in both\n * trees. Without this carve-out the guard would deny the exact command it tells the reader to run —\n * the single failure shape this repo has been burned by most often, and the reason the deny text is\n * allowed to promise \"STILL ALLOWED HERE: ... pnpm install, git pull/fetch\".\n *\n * Deliberately NARROW: fetching, pulling and installing cannot make a skew worse, and every one of\n * them moves the tree toward agreement. Anything that BUILDS, TESTS or COMMITS is still blocked,\n * because those are the operations that would be judged by the wrong release.\n */\n private isCureOrLook(command: string): boolean {\n const words = command.trim().split(/\\s+/);\n const head = words[0] ?? '';\n const sub = words[1] ?? '';\n if (head === 'git' || head === 'gh') {\n // `git -C <dir> <sub>` names its own directory; take the first non-flag word after it.\n const subcommand = sub === '-C' ? (words[3] ?? '') : sub;\n return ['pull', 'fetch', 'status', 'log', 'diff', 'show', 'branch', 'rev-parse', 'worktree'].includes(subcommand);\n }\n return (head === 'pnpm' || head === 'npm') && (sub === 'install' || sub === 'i');\n }\n\n /**\n * Is there a cross-tree comparison to make at all? TWO cheap conditions, no file read behind either:\n *\n * • K is `worktree` — git's `--git-dir ≠ --git-common-dir`, so a repo with no linked worktrees can\n * never reach the manifests. (In the primary clone this is also structural escape #1: \"do the\n * work in the main tree\" needs no allowlist entry to keep working.)\n * • the two roots are DIFFERENT directories — a tree compared with itself is not a skew, it is the\n * single-tree pin-vs-install question the L0 drift guard already owns.\n */\n private applies(tree: EffectiveTree): boolean {\n return tree.kind === 'worktree'\n && path.resolve(tree.mainRoot) !== path.resolve(tree.root);\n }\n\n /** Public so the runner can log all four versions on ALLOW as well as on BLOCK (audit, not just deny). */\n quartetFor(tree: EffectiveTree): VersionQuartet {\n return this.versions.quartet(tree.mainRoot, tree.root);\n }\n\n // Short on purpose — L0 ran a deliberate message diet and these blocks regress into a wall of text\n // if each one argues its case. State the skew, show every version WITH its file, give the git cure\n // first, then the two structural escapes, then what is still allowed.\n private report(tree: EffectiveTree, quartet: VersionQuartet): string {\n return [\n `❌ @webpieces version SKEW — this worktree and the main tree disagree, so work here is blocked.`,\n '',\n ...this.versionLines(tree, quartet),\n '',\n ` Whichever tree's hooks are live, one of these two releases lints, validates and builds`,\n ` this worktree — and it may be the one this manifest does not ask for.`,\n '',\n ` FIX (usually just git — the pin is TRACKED, so the same commit gives the same version):`,\n ` 1. \\`git -C ${tree.mainRoot} pull\\` and \\`git -C ${tree.root} pull\\` onto the same main,`,\n ` then \\`pnpm install\\` in each tree that has a node_modules. A worktree MAY have its`,\n ` own; what it may not have is a DIFFERENT @webpieces version from the main tree.`,\n ` 2. Or work in the MAIN tree instead — it is never blocked by this guard.`,\n ` 3. Or, if this tree genuinely needs a DIFFERENT version, use a separate CLONE, not a`,\n ` worktree: a clone gets its own governance. (This is the answer to \"I need a different`,\n ` version\", never to \"I need to install here\" — installing here is fine.)`,\n '',\n ` STILL ALLOWED HERE: every Read, read-only inspection, \\`pnpm install\\`, \\`git pull\\`/\\`fetch\\`,`,\n ` and edits to pnpm-workspace.yaml / package.json / webpieces.config.json.`,\n ` Do NOT lower the MAIN tree's pin to match — that downgrades every tree, including this`,\n ` session's own governor. If you are a SUBAGENT, you cannot fix the main tree: report to your`,\n ` coordinator that one of you must move to the other's version.`,\n ].join('\\n');\n }\n\n // Every version WITH the file it came from. An agent that is told \"they disagree\" without being told\n // WHICH FILE to edit re-derives it by grepping, which is exactly the turn-burning this guard exists\n // to prevent. Unreadable legs are printed as `-` rather than omitted, so the reader can tell\n // \"this one is absent\" from \"I forgot to look\".\n private versionLines(tree: EffectiveTree, quartet: VersionQuartet): readonly string[] {\n const lines = [\n ` main pin ${this.show(quartet.main.pinned)} ${tree.mainRoot}/pnpm-workspace.yaml`,\n ` main installed ${this.show(quartet.main.installed)} ${tree.mainRoot}/node_modules/${UMBRELLA_PACKAGE}`,\n ` ^ the binary judging this very call`,\n ` this worktree ${this.show(quartet.worktree.pinned)} ${tree.root}/pnpm-workspace.yaml`,\n ];\n if (quartet.worktree.installed !== null) {\n lines.push(` its installed ${this.show(quartet.worktree.installed)} ${tree.root}/node_modules/${UMBRELLA_PACKAGE}`);\n lines.push(' ^ what nx, vitest and eslint load IN this tree');\n }\n const others = this.versions.otherWorktrees(tree.mainRoot, tree.root);\n if (others.length > 0) {\n lines.push(` NOTE ${others.length} other worktree(s) exist and are governed the same way — if they are`);\n lines.push(' skewed too, their agents are already mis-governed. Consider clones, or');\n lines.push(' serializing the work in the main tree.');\n }\n return lines;\n }\n\n private show(version: string | null): string {\n return (version ?? '-').padEnd(10);\n }\n}\n"]}
|
package/templates/ai-hook.sh
CHANGED
|
@@ -113,7 +113,7 @@ WP_INSTALL_CMD="pnpm install"
|
|
|
113
113
|
WP_BORROW_NOTE=""
|
|
114
114
|
if [ "$BIN_ROOT" != "$ROOT" ]; then
|
|
115
115
|
WP_INSTALL_CMD="cd $ROOT && pnpm install"
|
|
116
|
-
WP_BORROW_NOTE=" NOTE: this tree ($ROOT) has NO node_modules of its own, so the guard binary was inherited from $BIN_ROOT by walking up.
|
|
116
|
+
WP_BORROW_NOTE=" NOTE: this tree ($ROOT) has NO node_modules of its own, so the guard binary was inherited from $BIN_ROOT by walking up. TWO cures are real and they fix DIFFERENT things: A makes THIS tree work now, B stops the two trees disagreeing. A - run the command above HERE. That is legitimate and it does work; a worktree NEEDS its own node_modules anyway (nx, vitest and the eslint plugin all execute in this tree and load from it). B - get $BIN_ROOT onto the same @webpieces version: put both trees on the same git hash (the pin is tracked) and run ONE 'pnpm install' there. If you are a SUBAGENT you cannot reach that tree, so ESCALATE - ask the coordinator to run 'pnpm install' in the main tree so both trees are on the same @webpieces version. The rule is NOT no-install-here: it is that this tree's @webpieces must EQUAL $BIN_ROOT's, because doing only A leaves two trees on two releases (the trinary-version-skew guard then BLOCKS rather than letting it pass unnoticed). Adding an ordinary third-party dependency here changes none of that - only a differing @webpieces version does. If this tree genuinely needs a DIFFERENT version, use a separate clone rather than a worktree."
|
|
117
117
|
fi
|
|
118
118
|
# Read the tool payload ONCE, up front. The shim no longer exec's the bin (see RUN_BIN_SH), so it must
|
|
119
119
|
# forward stdin to the bin itself — and it needs the payload again on the fail-closed path below.
|