@webpieces/ai-hook-rules 0.4.615 → 0.4.617

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.
@@ -59,7 +59,7 @@ function renderTwoLayerForceToRoot() {
59
59
  '| | judged by | what it judges | for | verdict |',
60
60
  '|---|---|---|---|---|',
61
61
  '| **L-1** | `.claude/webpieces/guarantee-root.sh` (POSIX sh, before any binary) | the `cd` **destination** of the command | **Bash** | ALLOW unless the destination is inside `$CLAUDE_PROJECT_DIR` and holds no `.git` — i.e. **sticky AND unguarded** |',
62
- '| **L1** | `gitFromSubdirBlock` (`runner.ts`) | the post-`cd` `effectiveCwd` | **git / gh only** | BLOCK unless it is THE root |',
62
+ '| **L1** | `ForceToRootGuard` (`force-to-root.ts`) | the post-`cd` `effectiveCwd` | **git / gh only** | BLOCK unless it is THE root |',
63
63
  '',
64
64
  'They ask different questions. L-1 asks *"can the relative guard hooks launch there?"* — because the',
65
65
  'guard hooks are registered relative, and a hook that cannot resolve exits 127, which the harness treats',
@@ -71,6 +71,25 @@ function renderTwoLayerForceToRoot() {
71
71
  'A denied `cd` never executes — PreToolUse denies the whole tool call before the shell moves — so there',
72
72
  'is no bad state to recover from and L-1 needs no cure command on any allowlist.',
73
73
  '',
74
+ '### The seam between them — where the worktree bypass actually lived',
75
+ '',
76
+ 'Read the two rows above together. L-1 treats **"the destination holds a `.git`"** as **"L1 will govern',
77
+ 'this"** — that is the whole reason it allows a `cd` into a nested clone. L1 then treated **"it has its',
78
+ 'own git toplevel"** as **"someone else\'s repo, hands off"** and exempted it. Each rule is defensible',
79
+ 'alone; COMPOSED, they left a hole exactly the shape of a linked worktree — L-1 deferred to L1, and L1',
80
+ 'deferred to nobody. Every bash guard was silently skipped inside `.claude/worktrees/**`, which is the',
81
+ 'sandbox agents are told to work in; a live `git push --dry-run` that was blocked from the primary clone',
82
+ 'executed from the worktree.',
83
+ '',
84
+ 'Resolving K from git\'s dirs closes it. State the seam anyway, because it is a SPEC-level invariant and',
85
+ 'not a bug in either rule: **L-1\'s `.git` test is a proxy for "governed elsewhere", and that proxy is',
86
+ 'sound only while L1 actually governs every tree whose `.git` belongs to this repo.** Change either side',
87
+ 'and check the other.',
88
+ '',
89
+ 'The same hole did NOT exist for Read/Write/Edit, which is why it stayed hidden: file tools are judged on',
90
+ 'the TARGET PATH, not the cwd (use case 4), so writes into a worktree were guarded the whole time while',
91
+ 'Bash in that same directory was exempt.',
92
+ '',
74
93
  ];
75
94
  }
76
95
  // The three questions L1 answers, the preamble and the filter — all prose, none of it row data.
@@ -88,19 +107,24 @@ function renderHead() {
88
107
  '',
89
108
  '**Code:** `packages/tooling/ai-hook-rules/src/core/effective-tree.ts` (`EffectiveTreeResolver`,',
90
109
  '`TreeKind`) · `packages/tooling/ai-hook-rules/src/core/runner.ts` (`l1LocationBlock`,',
91
- '`gitFromSubdirBlock`, `filterByExcludedPaths`, the `foreign` check) ·',
110
+ '`filterByExcludedPaths`, the `foreign` check) · `.../force-to-root.ts` (`ForceToRootGuard`) ·',
111
+ '`packages/tooling/ai-hook-rules/src/core/missing-directory.ts` (`MissingDirectoryGuard`) ·',
92
112
  '`packages/tooling/ai-hook-rules/src/core/coordinator-worktree.ts` (`CoordinatorWorktreeGuard`,',
93
113
  '`AgentIdentity`).',
94
114
  '',
95
- 'L1 answers three questions, and they are genuinely separate:',
115
+ 'L1 answers four questions, and they are genuinely separate:',
96
116
  '',
97
117
  '1. **Do we govern this at all?** — the escape hatches, for other repos and non-governed paths.',
98
- '2. **Is the WRONG AGENT standing here?** the coordinator must not work inside a linked worktree.',
118
+ ' Answered by asking GIT (`--git-common-dir`), never by path math: see the legend under **K**.',
119
+ '2. **Does the directory still EXIST?** — row 7. A worktree reaped out from under a live shell leaves',
120
+ ' a cwd that names nothing, and that state needs its own name and its own message, because the',
121
+ ' remedy for "you are in a subdirectory" is a `cd` back into the very directory that is gone.',
122
+ '3. **Is the WRONG AGENT standing here?** — the coordinator must not work inside a linked worktree.',
99
123
  ' Its governance is anchored to `$CLAUDE_PROJECT_DIR`, fixed at session start, which does NOT follow',
100
124
  ' a `cd`; a coordinator in a worktree therefore has its filesystem in one tree and its guards in',
101
125
  ' another, and every fault it is shown is measured against a tree it is not standing in. Work in a',
102
126
  ' worktree belongs to a **subagent bound to it**, which has both in one place.',
103
- '3. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where',
127
+ '4. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where',
104
128
  ' they are constantly, and `cd` gives them two different ways to be wrong: a `cd` that stays INSIDE',
105
129
  ' the workspace PERSISTS to later calls (so the shell can be parked in a subdirectory left by an',
106
130
  ' unrelated command turns earlier), while a `cd` that LEAVES it is reset by the harness, which says',
@@ -115,11 +139,25 @@ function renderHead() {
115
139
  '`effectiveCwd`, not from the shell\'s cwd** — so "a foreign repo that `cd`s into ours" is not a cell,',
116
140
  'it is simply `pw` after resolution.',
117
141
  '',
142
+ 'That holds because K is resolved by ASKING GIT about `effectiveCwd`, not by testing whether the path',
143
+ 'is lexically under the governed root. It has to be said that way round: the resolver used to do the',
144
+ 'path test, and a linked worktree under `.claude/worktrees/**` therefore resolved `f` — every bash',
145
+ 'guard exempt in the one sandbox agents are told to work in. A sentence in this file asserted the',
146
+ 'opposite as fact for several releases, which is how it went unnoticed.',
147
+ '',
118
148
  'Only a LEADING run of `cd`/`pushd` counts. A *trailing* `… && cd <exempt-tree>` must never',
119
149
  'retroactively pull a command out of scope — that would smuggle a root-level `git push` past the',
120
150
  'guards. Quoting is handled by `ShellSegmentScan`, so `echo "cd sub && git push"` is one opaque',
121
151
  'segment and its quoted `cd` is never picked up.',
122
152
  '',
153
+ ...renderFilterSection(),
154
+ ];
155
+ }
156
+ // `excludePaths` — a FILTER over the rule list, not a dimension of the table. Its own function because
157
+ // renderHead is at the 70-line method cap, and because this section is one self-contained argument.
158
+ // webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module
159
+ function renderFilterSection() {
160
+ return [
123
161
  '## Filter — not a dimension (all tools)',
124
162
  '',
125
163
  '`filterByExcludedPaths` drops every rule excluded for this path: the **target path** for',
@@ -148,7 +186,7 @@ function renderTable() {
148
186
  '',
149
187
  '| col | dimension | values |',
150
188
  '|---|---|---|',
151
- '| **K** | tree kind of the resolved target | `f` foreign repo · `o` outside any repo · `w` a LINKED worktree of ours · `pw` ours (primary **or** worktree) |',
189
+ '| **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) |',
152
190
  '| **A** | who is calling | `c` the coordinator · `s` a subagent (or a caller that cannot tell) |',
153
191
  '| **R** | command is provably read-only inspection | `n` · `y` |',
154
192
  '| **G** | command invokes git/gh | `n` · `y` |',
@@ -161,6 +199,14 @@ function renderTable() {
161
199
  'THAT tree\'s branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row 3 below',
162
200
  'is the ONE place they separate, and it turns on **A**, not on the tree.',
163
201
  '',
202
+ 'PLACEMENT IS NOT IDENTITY. A worktree checked out INSIDE the repo — `<repo>/.claude/worktrees/agent-XXXX`,',
203
+ 'which is where Claude Code puts every agent worktree — is `w` exactly like a sibling `../feature-dir` one.',
204
+ 'K comes from git\'s own dirs (`--git-common-dir` is identical for every checkout of one repo,',
205
+ '`--git-dir` differs only in a linked worktree), never from whether the path sits under the governed root.',
206
+ 'It used to short-circuit on that path test, so an in-repo worktree read as `f` — every bash guard exempt,',
207
+ 'and row 3 unreachable, for the only layout the harness actually produces. A nested clone under',
208
+ '`repositories/**` still reads `f`, because its shared git dir is its own.',
209
+ '',
164
210
  '`A` comes from `agent_id`/`agent_type` in the PreToolUse payload, which Claude Code sends **only inside',
165
211
  'a subagent**. Absent = the coordinator. A caller that cannot read the payload (the openclaw adapter,',
166
212
  'library consumers) resolves to `s` — fail open, never guess someone into a block.',
@@ -180,8 +226,11 @@ function renderTable() {
180
226
  `| ${l1_rows_1.L1_PRESTAGE_ROW} | – | – | – | – | – | 4 block | a \`cd\` that is not leading + literal, judged before any tree is resolved |`,
181
227
  ...l1_rows_1.L1_ROWS.map(tableRow),
182
228
  '',
183
- 'Rows 3 and 5 are the two structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',
184
- 'so they can never be reordered by accident — row 3 first, then force-to-root. Both sit after the L0',
229
+ 'Rows 3, 5 and 7 are the structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',
230
+ 'so they can never be reordered by accident — row 7 (the directory is gone) first, then row 3, then',
231
+ 'force-to-root. Row 7 is printed LAST above only because row numbers are stable across releases and',
232
+ 'renumbering 1-6 would invalidate every `row=` in the logs; `m` matches no other row, so its position',
233
+ 'in the scan is immaterial. All three sit after the L0',
185
234
  'allowlist, after the `f` check, and after the `excludePaths` filter and the config-sync check. So a',
186
235
  'cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0\'s invariant, and row 3 does',
187
236
  'not weaken it.',
@@ -224,7 +273,7 @@ function renderTail() {
224
273
  '## Not done — `o` is not exempt yet',
225
274
  '',
226
275
  'Row 2 hands `\'outside\'` down to L2 rather than exempting it. `\'outside\'` is produced at',
227
- '`effective-tree.ts` (`gitRoot === null`) carrying `governedRoot`, and **no code branches on it**, so a',
276
+ '`effective-tree.ts` (git has no answer for the directory) carrying `governedRoot`, and **no code branches on it**, so a',
228
277
  'command in no git repo is judged against the governed repo\'s branch and staleness state. That is a',
229
278
  'wrong verdict, and `exempt` is the right action.',
230
279
  '',
@@ -256,7 +305,8 @@ function renderTail() {
256
305
  '| resolver, K | `ai-hook-rules/src/core/effective-tree.ts` | `EffectiveTreeResolver`, `TreeKind` |',
257
306
  '| the two structural blocks, in order | `ai-hook-rules/src/core/runner.ts` | `l1LocationBlock` |',
258
307
  '| coordinator-in-worktree (row 3), A, R | `ai-hook-rules/src/core/coordinator-worktree.ts` | `CoordinatorWorktreeGuard`, `AgentIdentity` |',
259
- '| force-to-root (row 5) | `ai-hook-rules/src/core/runner.ts` | `gitFromSubdirBlock` |',
308
+ '| force-to-root (row 5) | `ai-hook-rules/src/core/force-to-root.ts` | `ForceToRootGuard` |',
309
+ '| the directory is gone (row 7) | `ai-hook-rules/src/core/missing-directory.ts` | `MissingDirectoryGuard` |',
260
310
  '| the filter | `ai-hook-rules/src/core/runner.ts` | `filterByExcludedPaths` |',
261
311
  '| `excludePaths` shape | `rules-config/src/exclude-hook-paths.ts`, `validate-config.ts`, `retired-config-keys.ts` | `ExcludePaths`, `validateExcludePaths` |',
262
312
  '',
@@ -1 +1 @@
1
- {"version":3,"file":"l1-doc.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { L1Row, L1UseCase, L1_ROWS, L1_PRESTAGE_ROW, allL1UseCases } from './l1-rows';\n\n// ---------------------------------------------------------------------------\n// guards/L1-location.md, rendered from L1_ROWS.\n//\n// Same arrangement as l0-matrix.renderGuardMatrixDoc(): one join('\\n') of literal markdown lines with\n// the ROW DATA interpolated from the array the guard consults. Everything that is not row data — every\n// prose section — is a literal line here, because that is the half a generator cannot own.\n//\n// A unit test (l1-matrix.spec.ts) locks guards/L1-location.md byte-identical to renderL1Doc(), and\n// `pnpm guards:generate` rewrites the file. So the table in the doc IS the array, not a description of\n// it. This module, like l1-rows.ts, has no runtime imports outside this pair so the generator can load\n// it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/** A dimension cell: the wildcard renders bare, every value renders as code. */\n// webpieces-disable no-function-outside-class -- pure cell formatter for renderL1Doc below, in this render module\nfunction cell(value: string): string {\n return value === '-' ? '-' : `\\`${value}\\``;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL1Doc below, in this render module\nfunction tableRow(row: L1Row): string {\n const dims = [row.k, row.a, row.r, row.g, row.p].map(cell).join(' | ');\n // Row 6 has no `why` — an EMPTY cell is `| |`, not `| |`. Two spaces would render the same in a\n // browser and fail the byte-lock, which is the whole point of locking bytes rather than markdown.\n const why = row.why === '' ? ' ' : ` ${row.why} `;\n return `| ${row.num} | ${dims} | ${row.action.label} |${why}|`;\n}\n\n// webpieces-disable no-function-outside-class -- pure row formatter for renderL1Doc below, in this render module\nfunction useCaseRow(useCase: L1UseCase): string {\n return `| ${useCase.num} | ${useCase.symptom} | ${useCase.state} | ${useCase.verdict} | ${useCase.fix} |`;\n}\n\n/**\n * Render guards/L1-location.md.\n *\n * Split into three consecutive halves purely to stay inside the method-line budget — the join order is\n * what makes them one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over L1_ROWS, beside the array it reads\nexport function renderL1Doc(): string {\n return [\n ...renderHead(),\n ...renderTable(),\n ...renderUseCases(),\n ...renderTail(),\n ].join('\\n');\n}\n\n// Why L-1's guarantee-root and L1's force-to-root are NOT the same rule. Its own function because\n// renderHead is at the 80-line method cap, and because this section is one self-contained argument.\n// webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module\nfunction renderTwoLayerForceToRoot(): string[] {\n return [\n '## Force-to-root is TWO rules, in two layers — do not collapse them',\n '',\n 'Collapsing them is how the `shellAtRoot` bug happened the first time, and the two now live in different',\n 'layers, so it is worth stating plainly:',\n '',\n '| | judged by | what it judges | for | verdict |',\n '|---|---|---|---|---|',\n '| **L-1** | `.claude/webpieces/guarantee-root.sh` (POSIX sh, before any binary) | the `cd` **destination** of the command | **Bash** | ALLOW unless the destination is inside `$CLAUDE_PROJECT_DIR` and holds no `.git` — i.e. **sticky AND unguarded** |',\n '| **L1** | `gitFromSubdirBlock` (`runner.ts`) | the post-`cd` `effectiveCwd` | **git / gh only** | BLOCK unless it is THE root |',\n '',\n 'They ask different questions. L-1 asks *\"can the relative guard hooks launch there?\"* — because the',\n 'guard hooks are registered relative, and a hook that cannot resolve exits 127, which the harness treats',\n 'as a non-blocking error and lets the call proceed UNGUARDED. It therefore ALLOWS a `cd` into a foreign',\n 'nested clone (its own `.git`) and a `cd` outside the project (the harness resets the cwd next call),',\n 'neither of which L1 would tolerate for a `git` command. L1 asks *\"is this git command being run from the',\n 'one root it is meant to run from?\"*, which is a narrower question about a narrower set of commands.',\n '',\n 'A denied `cd` never executes — PreToolUse denies the whole tool call before the shell moves — so there',\n 'is no bad state to recover from and L-1 needs no cure command on any allowlist.',\n '',\n ];\n}\n\n// The three questions L1 answers, the preamble and the filter — all prose, none of it row data.\n// webpieces-disable no-function-outside-class -- first section of renderL1Doc's string, beside it in this module\nfunction renderHead(): string[] {\n return [\n '# L1 — location',\n '',\n '**Goal: is this call ours to judge, is the right AGENT making it, and is git being run from the root?**',\n '',\n '**Config key: `location-guard` (proposed).** Force-to-root and coordinator-in-worktree both have **no',\n 'config key today** and cannot be disabled; `excludePaths` is a top-level block, not a `hookGuards`',\n 'entry.',\n '',\n '',\n '**Code:** `packages/tooling/ai-hook-rules/src/core/effective-tree.ts` (`EffectiveTreeResolver`,',\n '`TreeKind`) · `packages/tooling/ai-hook-rules/src/core/runner.ts` (`l1LocationBlock`,',\n '`gitFromSubdirBlock`, `filterByExcludedPaths`, the `foreign` check) ·',\n '`packages/tooling/ai-hook-rules/src/core/coordinator-worktree.ts` (`CoordinatorWorktreeGuard`,',\n '`AgentIdentity`).',\n '',\n 'L1 answers three 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 '2. **Is the WRONG AGENT standing here?** — the coordinator must not work inside a linked worktree.',\n ' Its governance is anchored to `$CLAUDE_PROJECT_DIR`, fixed at session start, which does NOT follow',\n ' a `cd`; a coordinator in a worktree therefore has its filesystem in one tree and its guards in',\n ' another, and every fault it is shown is measured against a tree it is not standing in. Work in a',\n ' worktree belongs to a **subagent bound to it**, which has both in one place.',\n '3. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where',\n ' they are constantly, and `cd` gives them two different ways to be wrong: a `cd` that stays INSIDE',\n ' the workspace PERSISTS to later calls (so the shell can be parked in a subdirectory left by an',\n ' unrelated command turns earlier), while a `cd` that LEAVES it is reset by the harness, which says',\n ' so — `Shell cwd was reset to <root>`. Neither can be assumed, which is why every remedy names the',\n ' root explicitly instead of telling the agent to `cd` first.',\n '',\n ...renderTwoLayerForceToRoot(),\n '## Preamble — resolve the target first (Bash only)',\n '',\n '`EffectiveTreeResolver.resolve()` computes `effectiveCwd`: the directory the command actually runs in,',\n 'which is the shell\\'s cwd unless the command leads with `cd <dir> &&`. **K is classified from',\n '`effectiveCwd`, not from the shell\\'s cwd** — so \"a foreign repo that `cd`s into ours\" is not a cell,',\n 'it is simply `pw` after resolution.',\n '',\n '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 '## 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 | `f` foreign repo · `o` outside any repo · `w` a LINKED worktree of ours · `pw` ours (primary **or** worktree) |',\n '| **A** | who is calling | `c` the coordinator · `s` a subagent (or a caller that cannot tell) |',\n '| **R** | command is provably read-only inspection | `n` · `y` |',\n '| **G** | command invokes git/gh | `n` · `y` |',\n '| **P** | position of the resolved target | `root` · `sub` |',\n '',\n 'All of them are **Bash only**. Read/Write/Edit resolve their own target (`input.filePath`) and have no',\n 'dimensions — the filter is all that applies to them. **The Read tool is never blocked by L1.**',\n '',\n 'A linked worktree is deliberately **not** foreign: it is the same project, so the guards run against',\n 'THAT tree\\'s branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row 3 below',\n 'is the ONE place they separate, and it turns on **A**, not on the tree.',\n '',\n '`A` comes from `agent_id`/`agent_type` in the PreToolUse payload, which Claude Code sends **only inside',\n 'a subagent**. Absent = the coordinator. A caller that cannot read the payload (the openclaw adapter,',\n 'library consumers) resolves to `s` — fail open, never guess someone into a block.',\n '',\n '`R` is `ReadOnlyInspectionScan` — the same paranoid \"provably inert\" test the unloadable-config escape',\n 'hatch uses (allowlisted viewers/searchers only, no redirects, no `sed -i`).',\n '',\n '## Table',\n '',\n '| # | K | A | R | G | P | act | why |',\n '|---|---|---|---|---|---|---|---|',\n // Row 0 is the PRE-STAGE (`misplacedCdBlock`). It decides from command TEXT before a tree is\n // resolved, so it cannot be classified over the five dimensions rows 1-6 share — but it IS an\n // L1 block, and an L1 block the table did not describe is exactly the drift this table exists\n // to prevent. It is numbered 0, not 7, because it does not sit in the first-match scan; and it\n // is PRINTED because `row=0` in the L1 log has to join to something.\n `| ${L1_PRESTAGE_ROW} | – | – | – | – | – | 4 block | a \\`cd\\` that is not leading + literal, judged before any tree is resolved |`,\n ...L1_ROWS.map(tableRow),\n '',\n 'Rows 3 and 5 are the two structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',\n 'so they can never be reordered by accident — row 3 first, then force-to-root. Both sit after the L0',\n 'allowlist, after the `f` check, and after the `excludePaths` filter and the config-sync check. So a',\n 'cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0\\'s invariant, and row 3 does',\n 'not weaken it.',\n '',\n ];\n}\n\n// The use-case table (ROW DATA, in the doc's own numbering) and the two notes that follow it.\n// webpieces-disable no-function-outside-class -- third section of renderL1Doc's string, beside it in this module\nfunction renderUseCases(): string[] {\n return [\n '## L1 use cases',\n '',\n 'Same row shape as L0: the **Fix** is literal or it is not a fix. `<root>` is the absolute workspace',\n 'root — the messages name it explicitly rather than telling you to `cd` first, for the reason in the',\n 'section head (neither the shell\\'s cwd nor a `cd`\\'s persistence can be assumed).',\n '',\n '| # | what you SEE (exact symptom) | state | verdict | Fix |',\n '|---|---|---|---|---|',\n ...allL1UseCases().map(useCaseRow),\n '',\n 'Row 8 is the one that changed. It used to be ALLOWED, because the predicate was',\n '`shellAtRoot || cdsToRoot` — two variables OR\\'d, so the same destination got opposite verdicts',\n 'depending on where the shell happened to start. It is now one variable, `effectiveCwd === root`.',\n '',\n 'Row 12 is the incident that produced table row 3. The coordinator ran `git worktree add`, `cd`\\'d in,',\n 'and worked there. An L0 version-drift fault then fired against the PRIMARY (pin `0.4.545` vs',\n '`node_modules` `0.4.526`) and prescribed `pnpm install` — which ran in the WORKTREE, internally',\n 'consistent at `0.4.526`/`0.4.526`, so it succeeded, changed nothing in the measured tree, and the guard',\n 're-denied. Five identical installs later the agent had invented a theory about the harness stripping',\n 'its `cd` and handed the problem to the human. Note what row 15 says: the fix is NOT to deny that',\n 'install. It is to make the split state unreachable, so the wrong-tree install is never plausible.',\n '',\n ];\n}\n\n// The known gap and the code anchors — prose, and the one section that must never be summarised away:\n// three code comments point at it.\n// webpieces-disable no-function-outside-class -- last section of renderL1Doc's string, beside it in this module\nfunction renderTail(): string[] {\n return [\n '## Not done — `o` is not exempt yet',\n '',\n 'Row 2 hands `\\'outside\\'` down to L2 rather than exempting it. `\\'outside\\'` is produced at',\n '`effective-tree.ts` (`gitRoot === null`) carrying `governedRoot`, and **no code branches on it**, so a',\n 'command in no git repo is judged against the governed repo\\'s branch and staleness state. That is a',\n 'wrong verdict, and `exempt` is the right action.',\n '',\n '**It must not ship alone.** Jurisdiction comes from the shell cwd, not from what the command touches,',\n 'so exempting `o` opens a bypass an agent reaches by typing `cd /tmp &&`:',\n '',\n '| command | today | with `o → exempt` alone |',\n '|---|---|---|',\n '| `cd /tmp && ls` | judged against the repo | exempt — **correct** |',\n '| `cd /tmp && git -C $REPO commit` | L2 guards fire | exempt — **every L2 guard bypassed** |',\n '| `cd /tmp && rm -rf $REPO/packages/http/src` | judged | exempt — **unguarded** |',\n '',\n 'The two cases only separate once jurisdiction is judged on **what the command touches** (explicit',\n '`git -C` / `--work-tree`, then path arguments, then the `cd`, then the shell cwd), with the fail-safe',\n 'rule that **any** resolved target inside `governedRoot` means `pw`. Ship the two together, or neither.',\n '',\n 'Tracked in `backlog/bug-bash-guards-judge-the-shell-cwd-not-the-paths-the-command-touches.md` and',\n '`backlog/bug-outside-tree-kind-is-never-consumed-so-a-non-git-dir-is-judged-against-the-governed-repo.md`.',\n 'That resolver has three consumers — L1\\'s K, L2\\'s scope dimension, and `excludePaths` on the Bash path',\n '— which is why the backlog says **fix once**.',\n '',\n '---',\n '',\n '',\n '## Code anchors',\n '',\n '| section | file | symbol |',\n '|---|---|---|',\n '| resolver, K | `ai-hook-rules/src/core/effective-tree.ts` | `EffectiveTreeResolver`, `TreeKind` |',\n '| the two structural blocks, in order | `ai-hook-rules/src/core/runner.ts` | `l1LocationBlock` |',\n '| coordinator-in-worktree (row 3), A, R | `ai-hook-rules/src/core/coordinator-worktree.ts` | `CoordinatorWorktreeGuard`, `AgentIdentity` |',\n '| force-to-root (row 5) | `ai-hook-rules/src/core/runner.ts` | `gitFromSubdirBlock` |',\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// Why L-1's guarantee-root and L1's force-to-root are NOT the same rule. Its own function because\n// renderHead is at the 80-line method cap, and because this section is one self-contained argument.\n// webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module\nfunction renderTwoLayerForceToRoot(): string[] {\n return [\n '## Force-to-root is TWO rules, in two layers — do not collapse them',\n '',\n 'Collapsing them is how the `shellAtRoot` bug happened the first time, and the two now live in different',\n 'layers, so it is worth stating plainly:',\n '',\n '| | judged by | what it judges | for | verdict |',\n '|---|---|---|---|---|',\n '| **L-1** | `.claude/webpieces/guarantee-root.sh` (POSIX sh, before any binary) | the `cd` **destination** of the command | **Bash** | ALLOW unless the destination is inside `$CLAUDE_PROJECT_DIR` and holds no `.git` — i.e. **sticky AND unguarded** |',\n '| **L1** | `ForceToRootGuard` (`force-to-root.ts`) | the post-`cd` `effectiveCwd` | **git / gh only** | BLOCK unless it is THE root |',\n '',\n 'They ask different questions. L-1 asks *\"can the relative guard hooks launch there?\"* — because the',\n 'guard hooks are registered relative, and a hook that cannot resolve exits 127, which the harness treats',\n 'as a non-blocking error and lets the call proceed UNGUARDED. It therefore ALLOWS a `cd` into a foreign',\n 'nested clone (its own `.git`) and a `cd` outside the project (the harness resets the cwd next call),',\n 'neither of which L1 would tolerate for a `git` command. L1 asks *\"is this git command being run from the',\n 'one root it is meant to run from?\"*, which is a narrower question about a narrower set of commands.',\n '',\n 'A denied `cd` never executes — PreToolUse denies the whole tool call before the shell moves — so there',\n 'is no bad state to recover from and L-1 needs no cure command on any allowlist.',\n '',\n '### The seam between them — where the worktree bypass actually lived',\n '',\n 'Read the two rows above together. L-1 treats **\"the destination holds a `.git`\"** as **\"L1 will govern',\n 'this\"** — that is the whole reason it allows a `cd` into a nested clone. L1 then treated **\"it has its',\n 'own git toplevel\"** as **\"someone else\\'s repo, hands off\"** and exempted it. Each rule is defensible',\n 'alone; COMPOSED, they left a hole exactly the shape of a linked worktree — L-1 deferred to L1, and L1',\n 'deferred to nobody. Every bash guard was silently skipped inside `.claude/worktrees/**`, which is the',\n 'sandbox agents are told to work in; a live `git push --dry-run` that was blocked from the primary clone',\n 'executed from the worktree.',\n '',\n 'Resolving K from git\\'s dirs closes it. State the seam anyway, because it is a SPEC-level invariant and',\n 'not a bug in either rule: **L-1\\'s `.git` test is a proxy for \"governed elsewhere\", and that proxy is',\n 'sound only while L1 actually governs every tree whose `.git` belongs to this repo.** Change either side',\n 'and check the other.',\n '',\n 'The same hole did NOT exist for Read/Write/Edit, which is why it stayed hidden: file tools are judged on',\n 'the TARGET PATH, not the cwd (use case 4), so writes into a worktree were guarded the whole time while',\n 'Bash in that same directory was exempt.',\n '',\n ];\n}\n\n// The three questions L1 answers, the preamble and the filter — all prose, none of it row data.\n// webpieces-disable no-function-outside-class -- first section of renderL1Doc's string, beside it in this module\nfunction renderHead(): string[] {\n return [\n '# L1 — location',\n '',\n '**Goal: is this call ours to judge, is the right AGENT making it, and is git being run from the root?**',\n '',\n '**Config key: `location-guard` (proposed).** Force-to-root and coordinator-in-worktree both have **no',\n 'config key today** and cannot be disabled; `excludePaths` is a top-level block, not a `hookGuards`',\n 'entry.',\n '',\n '',\n '**Code:** `packages/tooling/ai-hook-rules/src/core/effective-tree.ts` (`EffectiveTreeResolver`,',\n '`TreeKind`) · `packages/tooling/ai-hook-rules/src/core/runner.ts` (`l1LocationBlock`,',\n '`filterByExcludedPaths`, the `foreign` check) · `.../force-to-root.ts` (`ForceToRootGuard`) ·',\n '`packages/tooling/ai-hook-rules/src/core/missing-directory.ts` (`MissingDirectoryGuard`) ·',\n '`packages/tooling/ai-hook-rules/src/core/coordinator-worktree.ts` (`CoordinatorWorktreeGuard`,',\n '`AgentIdentity`).',\n '',\n 'L1 answers four questions, and they are genuinely separate:',\n '',\n '1. **Do we govern this at all?** — the escape hatches, for other repos and non-governed paths.',\n ' Answered by asking GIT (`--git-common-dir`), never by path math: see the legend under **K**.',\n '2. **Does the directory still EXIST?** — row 7. A worktree reaped out from under a live shell leaves',\n ' a cwd that names nothing, and that state needs its own name and its own message, because the',\n ' remedy for \"you are in a subdirectory\" is a `cd` back into the very directory that is gone.',\n '3. **Is the WRONG AGENT standing here?** — the coordinator must not work inside a linked worktree.',\n ' Its governance is anchored to `$CLAUDE_PROJECT_DIR`, fixed at session start, which does NOT follow',\n ' a `cd`; a coordinator in a worktree therefore has its filesystem in one tree and its guards in',\n ' another, and every fault it is shown is measured against a tree it is not standing in. Work in a',\n ' worktree belongs to a **subagent bound to it**, which has both in one place.',\n '4. **Is the agent stranded away from the root?** — force-to-root, git/gh only. Agents forget where',\n ' they are constantly, and `cd` gives them two different ways to be wrong: a `cd` that stays INSIDE',\n ' the workspace PERSISTS to later calls (so the shell can be parked in a subdirectory left by an',\n ' unrelated command turns earlier), while a `cd` that LEAVES it is reset by the harness, which says',\n ' so — `Shell cwd was reset to <root>`. Neither can be assumed, which is why every remedy names the',\n ' root explicitly instead of telling the agent to `cd` first.',\n '',\n ...renderTwoLayerForceToRoot(),\n '## Preamble — resolve the target first (Bash only)',\n '',\n '`EffectiveTreeResolver.resolve()` computes `effectiveCwd`: the directory the command actually runs in,',\n 'which is the shell\\'s cwd unless the command leads with `cd <dir> &&`. **K is classified from',\n '`effectiveCwd`, not from the shell\\'s cwd** — so \"a foreign repo that `cd`s into ours\" is not a cell,',\n 'it is simply `pw` after resolution.',\n '',\n 'That holds because K is resolved by ASKING GIT about `effectiveCwd`, not by testing whether the path',\n 'is lexically under the governed root. It has to be said that way round: the resolver used to do the',\n 'path test, and a linked worktree under `.claude/worktrees/**` therefore resolved `f` — every bash',\n 'guard exempt in the one sandbox agents are told to work in. A sentence in this file asserted the',\n 'opposite as fact for several releases, which is how it went unnoticed.',\n '',\n 'Only a LEADING run of `cd`/`pushd` counts. A *trailing* `… && cd <exempt-tree>` must never',\n 'retroactively pull a command out of scope — that would smuggle a root-level `git push` past the',\n 'guards. Quoting is handled by `ShellSegmentScan`, so `echo \"cd sub && git push\"` is one opaque',\n 'segment and its quoted `cd` is never picked up.',\n '',\n ...renderFilterSection(),\n ];\n}\n\n// `excludePaths` — a FILTER over the rule list, not a dimension of the table. Its own function because\n// renderHead is at the 70-line method cap, and because this section is one self-contained argument.\n// webpieces-disable no-function-outside-class -- prose section of renderL1Doc's string, beside it in this module\nfunction renderFilterSection(): string[] {\n return [\n '## Filter — not a dimension (all tools)',\n '',\n '`filterByExcludedPaths` drops every rule excluded for this path: the **target path** for',\n 'Read/Write/Edit, `effectiveCwd` for Bash. An empty rule list means allow. This is a filter, not a row:',\n '\"exempt\" is what emerges when the list empties.',\n '',\n '`excludePaths` is **ONE glob list** (canonical: `\"excludePaths\": [\"repositories/**\"]`). The',\n '`{ rules: [...], guards: [...] }` object is **retired and rejected**, with the union it must become',\n 'named in the error. `wp-install-ai-hooks` migrates it in place.',\n '',\n 'This used to be a tolerated fallback, justified here by \"rejecting it would block every Bash/Edit',\n 'including the edit that would fix it.\" **That was never true**, and the fallback it licensed is why',\n 'consumer configs — this repo\\'s own included — sat on the dead shape for releases. A Write/Edit whose',\n 'target is `webpieces.config.json` is an unconditional **PASS** (see the L0 table above), and',\n '`pnpm install` has an installer bypass, so an invalid config can always be repaired from inside the',\n 'block. Config rejection is self-recoverable by construction; see `retired-config-keys.ts` for the',\n 'policy and the reasoning.',\n '',\n ];\n}\n\n// The legend, the table itself (ROW DATA), and the note on the two structural blocks.\n// webpieces-disable no-function-outside-class -- second section of renderL1Doc's string, beside it in this module\nfunction renderTable(): string[] {\n return [\n '## Legend',\n '',\n '| col | dimension | values |',\n '|---|---|---|',\n '| **K** | tree kind of the resolved target, from git\\'s own dirs | `f` foreign repo (a DIFFERENT `--git-common-dir`) · `m` the directory does not exist · `o` outside any repo · `w` a LINKED worktree of ours (`--git-dir` ≠ `--git-common-dir`), wherever it sits on disk · `pw` ours (primary **or** worktree) |',\n '| **A** | who is calling | `c` the coordinator · `s` a subagent (or a caller that cannot tell) |',\n '| **R** | command is provably read-only inspection | `n` · `y` |',\n '| **G** | command invokes git/gh | `n` · `y` |',\n '| **P** | position of the resolved target | `root` · `sub` |',\n '',\n 'All of them are **Bash only**. Read/Write/Edit resolve their own target (`input.filePath`) and have no',\n 'dimensions — the filter is all that applies to them. **The Read tool is never blocked by L1.**',\n '',\n 'A linked worktree is deliberately **not** foreign: it is the same project, so the guards run against',\n 'THAT tree\\'s branch and cache. Every rule-scoped guard treats `p` and `w` alike, hence `pw`; row 3 below',\n 'is the ONE place they separate, and it turns on **A**, not on the tree.',\n '',\n 'PLACEMENT IS NOT IDENTITY. A worktree checked out INSIDE the repo — `<repo>/.claude/worktrees/agent-XXXX`,',\n 'which is where Claude Code puts every agent worktree — is `w` exactly like a sibling `../feature-dir` one.',\n 'K comes from git\\'s own dirs (`--git-common-dir` is identical for every checkout of one repo,',\n '`--git-dir` differs only in a linked worktree), never from whether the path sits under the governed root.',\n 'It used to short-circuit on that path test, so an in-repo worktree read as `f` — every bash guard exempt,',\n 'and row 3 unreachable, for the only layout the harness actually produces. A nested clone under',\n '`repositories/**` still reads `f`, because its shared git dir is its own.',\n '',\n '`A` comes from `agent_id`/`agent_type` in the PreToolUse payload, which Claude Code sends **only inside',\n 'a subagent**. Absent = the coordinator. A caller that cannot read the payload (the openclaw adapter,',\n 'library consumers) resolves to `s` — fail open, never guess someone into a block.',\n '',\n '`R` is `ReadOnlyInspectionScan` — the same paranoid \"provably inert\" test the unloadable-config escape',\n 'hatch uses (allowlisted viewers/searchers only, no redirects, no `sed -i`).',\n '',\n '## Table',\n '',\n '| # | K | A | R | G | P | act | why |',\n '|---|---|---|---|---|---|---|---|',\n // Row 0 is the PRE-STAGE (`misplacedCdBlock`). It decides from command TEXT before a tree is\n // resolved, so it cannot be classified over the five dimensions rows 1-6 share — but it IS an\n // L1 block, and an L1 block the table did not describe is exactly the drift this table exists\n // to prevent. It is numbered 0, not 7, because it does not sit in the first-match scan; and it\n // is PRINTED because `row=0` in the L1 log has to join to something.\n `| ${L1_PRESTAGE_ROW} | – | – | – | – | – | 4 block | a \\`cd\\` that is not leading + literal, judged before any tree is resolved |`,\n ...L1_ROWS.map(tableRow),\n '',\n 'Rows 3, 5 and 7 are the structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',\n 'so they can never be reordered by accident — row 7 (the directory is gone) first, then row 3, then',\n 'force-to-root. Row 7 is printed LAST above only because row numbers are stable across releases and',\n 'renumbering 1-6 would invalidate every `row=` in the logs; `m` matches no other row, so its position',\n 'in the scan is immaterial. All three sit after the L0',\n 'allowlist, after the `f` check, and after the `excludePaths` filter and the config-sync check. So a',\n 'cure (`cd <worktree> && pnpm install`) still reaches any tree: that is L0\\'s invariant, and row 3 does',\n 'not weaken it.',\n '',\n ];\n}\n\n// The use-case table (ROW DATA, in the doc's own numbering) and the two notes that follow it.\n// webpieces-disable no-function-outside-class -- third section of renderL1Doc's string, beside it in this module\nfunction renderUseCases(): string[] {\n return [\n '## L1 use cases',\n '',\n 'Same row shape as L0: the **Fix** is literal or it is not a fix. `<root>` is the absolute workspace',\n 'root — the messages name it explicitly rather than telling you to `cd` first, for the reason in the',\n 'section head (neither the shell\\'s cwd nor a `cd`\\'s persistence can be assumed).',\n '',\n '| # | what you SEE (exact symptom) | state | verdict | Fix |',\n '|---|---|---|---|---|',\n ...allL1UseCases().map(useCaseRow),\n '',\n 'Row 8 is the one that changed. It used to be ALLOWED, because the predicate was',\n '`shellAtRoot || cdsToRoot` — two variables OR\\'d, so the same destination got opposite verdicts',\n 'depending on where the shell happened to start. It is now one variable, `effectiveCwd === root`.',\n '',\n 'Row 12 is the incident that produced table row 3. The coordinator ran `git worktree add`, `cd`\\'d in,',\n 'and worked there. An L0 version-drift fault then fired against the PRIMARY (pin `0.4.545` vs',\n '`node_modules` `0.4.526`) and prescribed `pnpm install` — which ran in the WORKTREE, internally',\n 'consistent at `0.4.526`/`0.4.526`, so it succeeded, changed nothing in the measured tree, and the guard',\n 're-denied. Five identical installs later the agent had invented a theory about the harness stripping',\n 'its `cd` and handed the problem to the human. Note what row 15 says: the fix is NOT to deny that',\n 'install. It is to make the split state unreachable, so the wrong-tree install is never plausible.',\n '',\n ];\n}\n\n// The known gap and the code anchors — prose, and the one section that must never be summarised away:\n// three code comments point at it.\n// webpieces-disable no-function-outside-class -- last section of renderL1Doc's string, beside it in this module\nfunction renderTail(): string[] {\n return [\n '## Not done — `o` is not exempt yet',\n '',\n 'Row 2 hands `\\'outside\\'` down to L2 rather than exempting it. `\\'outside\\'` is produced at',\n '`effective-tree.ts` (git has no answer for the directory) carrying `governedRoot`, and **no code branches on it**, so a',\n 'command in no git repo is judged against the governed repo\\'s branch and staleness state. That is a',\n 'wrong verdict, and `exempt` is the right action.',\n '',\n '**It must not ship alone.** Jurisdiction comes from the shell cwd, not from what the command touches,',\n 'so exempting `o` opens a bypass an agent reaches by typing `cd /tmp &&`:',\n '',\n '| command | today | with `o → exempt` alone |',\n '|---|---|---|',\n '| `cd /tmp && ls` | judged against the repo | exempt — **correct** |',\n '| `cd /tmp && git -C $REPO commit` | L2 guards fire | exempt — **every L2 guard bypassed** |',\n '| `cd /tmp && rm -rf $REPO/packages/http/src` | judged | exempt — **unguarded** |',\n '',\n 'The two cases only separate once jurisdiction is judged on **what the command touches** (explicit',\n '`git -C` / `--work-tree`, then path arguments, then the `cd`, then the shell cwd), with the fail-safe',\n 'rule that **any** resolved target inside `governedRoot` means `pw`. Ship the two together, or neither.',\n '',\n 'Tracked in `backlog/bug-bash-guards-judge-the-shell-cwd-not-the-paths-the-command-touches.md` and',\n '`backlog/bug-outside-tree-kind-is-never-consumed-so-a-non-git-dir-is-judged-against-the-governed-repo.md`.',\n 'That resolver has three consumers — L1\\'s K, L2\\'s scope dimension, and `excludePaths` on the Bash path',\n '— which is why the backlog says **fix once**.',\n '',\n '---',\n '',\n '',\n '## Code anchors',\n '',\n '| section | file | symbol |',\n '|---|---|---|',\n '| resolver, K | `ai-hook-rules/src/core/effective-tree.ts` | `EffectiveTreeResolver`, `TreeKind` |',\n '| the two structural blocks, in order | `ai-hook-rules/src/core/runner.ts` | `l1LocationBlock` |',\n '| coordinator-in-worktree (row 3), A, R | `ai-hook-rules/src/core/coordinator-worktree.ts` | `CoordinatorWorktreeGuard`, `AgentIdentity` |',\n '| force-to-root (row 5) | `ai-hook-rules/src/core/force-to-root.ts` | `ForceToRootGuard` |',\n '| the directory is gone (row 7) | `ai-hook-rules/src/core/missing-directory.ts` | `MissingDirectoryGuard` |',\n '| the filter | `ai-hook-rules/src/core/runner.ts` | `filterByExcludedPaths` |',\n '| `excludePaths` shape | `rules-config/src/exclude-hook-paths.ts`, `validate-config.ts`, `retired-config-keys.ts` | `ExcludePaths`, `validateExcludePaths` |',\n '',\n ];\n}\n"]}
@@ -5,9 +5,9 @@ import type { TreeKind } from './effective-tree';
5
5
  * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the
6
6
  * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.
7
7
  */
8
- export type L1Kind = 'f' | 'o' | 'p' | 'w';
8
+ export type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';
9
9
  /** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */
10
- export type L1KindMatch = 'f' | 'o' | 'w' | 'pw' | '-';
10
+ export type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';
11
11
  /** R and G are yes/no, written `y`/`n` in the doc, with `-` for "does not matter". */
12
12
  export type L1Flag = 'y' | 'n' | '-';
13
13
  /** A is the same one boolean wearing the doc's own letters: `c` the coordinator, `s` a subagent. */
@@ -19,7 +19,7 @@ export type L1ActionKind = 'exempt' | 'down' | 'block';
19
19
  * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,
20
20
  * so deleting a row from the array removes the block.
21
21
  */
22
- export type L1BlockId = 'coordinator-in-worktree' | 'force-to-root';
22
+ export type L1BlockId = 'coordinator-in-worktree' | 'force-to-root' | 'missing-directory';
23
23
  /**
24
24
  * The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a
25
25
  * tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use
@@ -50,7 +50,7 @@ export declare class L1Classification {
50
50
  * The classification the RUNNER enforces on, built from the resolved tree and the caller.
51
51
  *
52
52
  * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by
53
- * effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is
53
+ * effective-tree.ts (git has no answer for the directory) and consumed NOWHERE, so a command in no git repo is
54
54
  * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what
55
55
  * SHOULD happen and is deliberately unreachable from here until the "Not done" fix in
56
56
  * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,
@@ -126,11 +126,16 @@ export declare class L1Row {
126
126
  private kindMatches;
127
127
  }
128
128
  /**
129
- * THE six L1 rows, in first-match-wins order.
129
+ * THE seven L1 rows, in first-match-wins order.
130
130
  *
131
- * Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they
131
+ * Rows 3, 5 and 7 are the structural blocks and they run as ONE step (runner.l1LocationBlock) so they
132
132
  * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. "L1 has no
133
- * objection" — which is why only rows 3 and 5 carry a blockId.
133
+ * objection" — which is why only those three carry a blockId.
134
+ *
135
+ * Row 7 (`m`, the vanished directory) sits LAST only because row numbers are stable across releases —
136
+ * they are printed in the doc and logged as `row=`, so renumbering rows 1-6 to slot it in front would
137
+ * silently invalidate every existing reference. Position costs nothing here: `m` is matched by no other
138
+ * row, so first-match reaches it wherever it sits.
134
139
  */
135
140
  export declare const L1_ROWS: readonly L1Row[];
136
141
  /**
@@ -40,7 +40,7 @@ class L1Classification {
40
40
  * The classification the RUNNER enforces on, built from the resolved tree and the caller.
41
41
  *
42
42
  * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by
43
- * effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is
43
+ * effective-tree.ts (git has no answer for the directory) and consumed NOWHERE, so a command in no git repo is
44
44
  * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what
45
45
  * SHOULD happen and is deliberately unreachable from here until the "Not done" fix in
46
46
  * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,
@@ -50,7 +50,9 @@ class L1Classification {
50
50
  // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to
51
51
  // 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
52
52
  static forEnforcement(treeKind, coordinator, readOnly, git, atRoot) {
53
- const kind = treeKind === 'foreign' ? 'f' : treeKind === 'worktree' ? 'w' : 'p';
53
+ const kind = treeKind === 'foreign' ? 'f'
54
+ : treeKind === 'missing' ? 'm'
55
+ : treeKind === 'worktree' ? 'w' : 'p';
54
56
  return new L1Classification(kind, coordinator, readOnly, git, atRoot);
55
57
  }
56
58
  }
@@ -175,11 +177,16 @@ function flagMatches(cell, value) {
175
177
  return (cell === 'y') === value;
176
178
  }
177
179
  /**
178
- * THE six L1 rows, in first-match-wins order.
180
+ * THE seven L1 rows, in first-match-wins order.
179
181
  *
180
- * Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they
182
+ * Rows 3, 5 and 7 are the structural blocks and they run as ONE step (runner.l1LocationBlock) so they
181
183
  * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. "L1 has no
182
- * objection" — which is why only rows 3 and 5 carry a blockId.
184
+ * objection" — which is why only those three carry a blockId.
185
+ *
186
+ * Row 7 (`m`, the vanished directory) sits LAST only because row numbers are stable across releases —
187
+ * they are printed in the doc and logged as `row=`, so renumbering rows 1-6 to slot it in front would
188
+ * silently invalidate every existing reference. Position costs nothing here: `m` is matched by no other
189
+ * row, so first-match reaches it wherever it sits.
183
190
  */
184
191
  exports.L1_ROWS = [
185
192
  new L1Row(1, 'f', '-', '-', '-', '-', exports.ACT_EXEMPT, 'different git repo — hands off', null, null, [
@@ -188,6 +195,7 @@ exports.L1_ROWS = [
188
195
  new L1Row(2, 'o', '-', '-', '-', '-', exports.ACT_DOWN, 'see "Not done" below', null, null, []),
189
196
  new L1Row(3, 'w', 'c', 'n', '-', '-', exports.ACT_BLOCK, 'the coordinator\'s guards do not follow its `cd` — delegate to a subagent bound to the worktree', new L1Cure('delegate to a subagent bound to the worktree', 'Spawn a subagent bound to that worktree', false), 'coordinator-in-worktree', [
190
197
  new L1UseCase(12, 'you are the **coordinator**, you ran `git worktree add ../wt`, and `cd ../wt && pnpm build` is blocked', '`w` / `c` / `n` — row 3', 'BLOCK_AI_CURE', 'Option 1 (preferred): spawn a subagent bound to `<worktree>` — the Agent tool with worktree isolation, or have the subagent call `EnterWorktree` with `path: <worktree>` (it accepts a worktree you already created)<br>Do NOT: re-type the command, or conclude the harness ate your `cd` — it did not; your GUARDS did not follow it', new L1Classification('w', true, false, false, false)),
198
+ new L1UseCase(16, 'the same block for `cd .claude/worktrees/agent-XXXX && <work>` — the harness\'s OWN worktree layout', '`w` / `c` / `n` — row 3; in-repo placement is still `w`', 'BLOCK_AI_CURE', 'Option 1 (preferred): same as case 12 — spawn a subagent bound to that worktree<br>Do NOT: expect it to be exempt because it sits under the repo — K is git\'s `--git-common-dir` answer, not a path test. This case used to read `f` (every guard silently off); if you are looking at an OLD release that is the difference', new L1Classification('w', true, false, false, false)),
191
199
  ]),
192
200
  new L1Row(4, 'pw', '-', '-', 'n', '-', exports.ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [
193
201
  new L1UseCase(5, '`ls` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', 'ALLOW (handed to L2)', 'none — force-to-root has no jurisdiction over non-git commands', new L1Classification('p', false, true, false, false)),
@@ -200,10 +208,15 @@ exports.L1_ROWS = [
200
208
  new L1UseCase(7, '`git status` from `packages/http/` is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK_AI_CURE', 'Option 1 (preferred): `cd <root> && git status`', new L1Classification('p', false, false, true, false)),
201
209
  new L1UseCase(8, '`cd packages/http && git status` **typed from the root** is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK_AI_CURE', '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', new L1Classification('p', false, false, true, false)),
202
210
  new L1UseCase(11, '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one', '`pw` / `y` / `sub` — row 5; force-to-root runs first', 'BLOCK_AI_CURE', 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\'s real answer ← costs one extra turn by design; still blocked', new L1Classification('p', false, false, true, false)),
211
+ new L1UseCase(17, 'the printed cure REPLACES your `cd`, it does not stack in front of it', '`pw` / `y` / `sub` — row 5, on the cure itself', 'BLOCK_AI_CURE', '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', new L1Classification('p', false, false, true, false)),
203
212
  ]),
204
213
  new L1Row(6, 'pw', '-', '-', 'y', 'root', exports.ACT_DOWN, '', null, null, [
205
214
  new L1UseCase(9, '`cd <root> && git status` passes from anywhere', '`pw` / `y` / `root` — row 6', 'ALLOW (handed to L2)', 'none — this IS the prescribed cure', new L1Classification('p', false, false, true, true)),
206
215
  ]),
216
+ new L1Row(7, 'm', '-', '-', '-', '-', exports.ACT_BLOCK, 'the directory is GONE — nothing can run there', new L1Cure('`cd <root> && <the work>`, never back through the dead path', 'no longer exists', true), 'missing-directory', [
217
+ new L1UseCase(18, 'every command from a worktree another agent REAPED mid-session is blocked', '`m` — row 7', 'BLOCK_AI_CURE', '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', new L1Classification('m', false, false, true, false)),
218
+ new L1UseCase(19, 'the same block for a NON-git command there — `m` does not care about G', '`m` — row 7; K alone decides it', 'BLOCK_AI_CURE', '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', new L1Classification('m', false, false, false, false)),
219
+ ]),
207
220
  ];
208
221
  /**
209
222
  * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0
@@ -1 +1 @@
1
- {"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":";;;AAyTA,sCAGC;AAUD,gDAIC;AA7RD;;;;;;;;;;;GAWG;AACU,QAAA,eAAe,GAAG,GAAG,CAAC;AAEnC;;;;;GAKG;AACH,MAAa,gBAAgB;IAGZ;IACA;IACA;IACA;IACA;IANb,kGAAkG;IAClG,YACa,IAAY,EACZ,WAAoB,EACpB,QAAiB,EACjB,GAAY,EACZ,MAAe;QAJf,SAAI,GAAJ,IAAI,CAAQ;QACZ,gBAAW,GAAX,WAAW,CAAS;QACpB,aAAQ,GAAR,QAAQ,CAAS;QACjB,QAAG,GAAH,GAAG,CAAS;QACZ,WAAM,GAAN,MAAM,CAAS;IACzB,CAAC;IAEJ;;;;;;;;;;OAUG;IACH,oGAAoG;IACpG,kMAAkM;IAClM,MAAM,CAAC,cAAc,CACjB,QAAkB,EAClB,WAAoB,EACpB,QAAiB,EACjB,GAAY,EACZ,MAAe;QAEf,MAAM,IAAI,GAAW,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QACxF,OAAO,IAAI,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAC1E,CAAC;CACJ;AAjCD,4CAiCC;AAED,kGAAkG;AAClG,MAAa,QAAQ;IACI;IAAwB;IAA7C,YAAqB,KAAa,EAAW,IAAkB;QAA1C,UAAK,GAAL,KAAK,CAAQ;QAAW,SAAI,GAAJ,IAAI,CAAc;IAAG,CAAC;CACtE;AAFD,4BAEC;AAEY,QAAA,UAAU,GAAG,IAAI,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;AAChD,QAAA,QAAQ,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACxC,QAAA,SAAS,GAAG,IAAI,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAa,MAAM;IAGF;IAEA;IACA;IALb;IACI,4CAA4C;IACnC,OAAe;IACxB,kFAAkF;IACzE,WAAmB,EACnB,QAAiB;QAHjB,YAAO,GAAP,OAAO,CAAQ;QAEf,gBAAW,GAAX,WAAW,CAAQ;QACnB,aAAQ,GAAR,QAAQ,CAAS;IAC3B,CAAC;CACP;AARD,wBAQC;AAED;;;;;;;GAOG;AACH,MAAa,SAAS;IAGL;IACA;IACA;IACA;IACA;IACA;IAPb,wHAAwH;IACxH,YACa,GAAW,EACX,OAAe,EACf,KAAa,EACb,OAAe,EACf,GAAW,EACX,iBAA0C,IAAI;QAL9C,QAAG,GAAH,GAAG,CAAQ;QACX,YAAO,GAAP,OAAO,CAAQ;QACf,UAAK,GAAL,KAAK,CAAQ;QACb,YAAO,GAAP,OAAO,CAAQ;QACf,QAAG,GAAH,GAAG,CAAQ;QACX,mBAAc,GAAd,cAAc,CAAgC;IACxD,CAAC;CACP;AAVD,8BAUC;AAED,0EAA0E;AAC1E,MAAa,KAAK;IAGD;IACA;IACA;IACA;IACA;IACA;IACA;IAEA;IACA;IACA;IACA;IAbb,oHAAoH;IACpH,YACa,GAAW,EACX,CAAc,EACd,CAAU,EACV,CAAS,EACT,CAAS,EACT,CAAuB,EACvB,MAAgB;IACzB,gCAAgC;IACvB,GAAW,EACX,IAAmB,EACnB,OAAyB,EACzB,QAA8B;QAX9B,QAAG,GAAH,GAAG,CAAQ;QACX,MAAC,GAAD,CAAC,CAAa;QACd,MAAC,GAAD,CAAC,CAAS;QACV,MAAC,GAAD,CAAC,CAAQ;QACT,MAAC,GAAD,CAAC,CAAQ;QACT,MAAC,GAAD,CAAC,CAAsB;QACvB,WAAM,GAAN,MAAM,CAAU;QAEhB,QAAG,GAAH,GAAG,CAAQ;QACX,SAAI,GAAJ,IAAI,CAAe;QACnB,YAAO,GAAP,OAAO,CAAkB;QACzB,aAAQ,GAAR,QAAQ,CAAsB;IACxC,CAAC;IAEJ,OAAO,CAAC,CAAmB;QACvB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAC5C,IAAI,IAAI,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,WAAW;YAAE,OAAO,KAAK,CAAC;QACvE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;QACnD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAC9C,OAAO,IAAI,CAAC,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACpE,CAAC;IAEO,WAAW,CAAC,IAAY;QAC5B,IAAI,IAAI,CAAC,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAChC,IAAI,IAAI,CAAC,CAAC,KAAK,IAAI;YAAE,OAAO,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC;QACzD,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC;IAC3B,CAAC;CACJ;AA9BD,sBA8BC;AAED,oGAAoG;AACpG,sGAAsG;AACtG,6GAA6G;AAC7G,SAAS,WAAW,CAAC,IAAY,EAAE,KAAc;IAC7C,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO,CAAC,IAAI,KAAK,GAAG,CAAC,KAAK,KAAK,CAAC;AACpC,CAAC;AAED;;;;;;GAMG;AACU,QAAA,OAAO,GAAqB;IACrC,IAAI,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,kBAAU,EAAE,gCAAgC,EAAE,IAAI,EAAE,IAAI,EAAE;QAC5F,IAAI,SAAS,CAAC,CAAC,EACX,iEAAiE,EACjE,uBAAuB,EACvB,cAAc,EACd,gHAAgH,EAChH,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;KAC5D,CAAC;IACF,IAAI,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAQ,EAAE,sBAAsB,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;IACvF,IAAI,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAS,EAC3C,iGAAiG,EACjG,IAAI,MAAM,CAAC,8CAA8C,EAAE,yCAAyC,EAAE,KAAK,CAAC,EAC5G,yBAAyB,EAAE;QACvB,IAAI,SAAS,CAAC,EAAE,EACZ,wGAAwG,EACxG,yBAAyB,EACzB,eAAe,EACf,wUAAwU,EACxU,IAAI,gBAAgB,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;KAC5D,CAAC;IACN,IAAI,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAQ,EAAE,mCAAmC,EAAE,IAAI,EAAE,IAAI,EAAE;QAC9F,IAAI,SAAS,CAAC,CAAC,EACX,0CAA0C,EAC1C,wBAAwB,EACxB,sBAAsB,EACtB,gEAAgE,EAChE,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QACzD,IAAI,SAAS,CAAC,CAAC,EACX,iDAAiD,EACjD,wBAAwB,EACxB,sBAAsB,EACtB,wEAAwE,EACxE,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC1D,IAAI,SAAS,CAAC,EAAE,EACZ,oCAAoC,EACpC,6BAA6B,EAC7B,sBAAsB,EACtB,2FAA2F,EAC3F,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACzD,IAAI,SAAS,CAAC,EAAE,EACZ,oDAAoD,EACpD,kCAAkC,EAClC,sBAAsB,EACtB,+DAA+D,EAC/D,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC1D,IAAI,SAAS,CAAC,EAAE,EACZ,kEAAkE,EAClE,wCAAwC,EACxC,sBAAsB,EACtB,wIAAwI,EACxI,IAAI,gBAAgB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;KAC3D,CAAC;IACF,IAAI,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,iBAAS,EAAE,2BAA2B,EAC3E,IAAI,MAAM,CAAC,2BAA2B,EAAE,wCAAwC,EAAE,IAAI,CAAC,EACvF,eAAe,EAAE;QACb,IAAI,SAAS,CAAC,CAAC,EACX,+CAA+C,EAC/C,4BAA4B,EAC5B,eAAe,EACf,iDAAiD,EACjD,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACzD,IAAI,SAAS,CAAC,CAAC,EACX,qEAAqE,EACrE,4BAA4B,EAC5B,eAAe,EACf,kLAAkL,EAClL,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACzD,IAAI,SAAS,CAAC,EAAE,EACZ,0FAA0F,EAC1F,sDAAsD,EACtD,eAAe,EACf,8IAA8I,EAC9I,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;KAC5D,CAAC;IACN,IAAI,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;QAChE,IAAI,SAAS,CAAC,CAAC,EACX,gDAAgD,EAChD,6BAA6B,EAC7B,sBAAsB,EACtB,oCAAoC,EACpC,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;KAC3D,CAAC;CACL,CAAC;AAEF;;;;;;;GAOG;AACU,QAAA,oBAAoB,GAAyB;IACtD,IAAI,SAAS,CAAC,CAAC,EACX,gEAAgE,EAChE,wCAAwC,EACxC,cAAc,EACd,aAAa,CAAC;IAClB,IAAI,SAAS,CAAC,CAAC,EACX,mDAAmD,EACnD,mCAAmC,EACnC,eAAe,EACf,wEAAwE,CAAC;IAC7E,IAAI,SAAS,CAAC,CAAC,EACX,uEAAuE,EACvE,4BAA4B,EAC5B,MAAM,EACN,+EAA+E,CAAC;IACpF,IAAI,SAAS,CAAC,EAAE,EACZ,mFAAmF,EACnF,2BAA2B,EAC3B,OAAO,EACP,mDAAmD,CAAC;CAC3D,CAAC;AAEF,4FAA4F;AAC5F,0HAA0H;AAC1H,SAAgB,aAAa;IACzB,MAAM,GAAG,GAAG,CAAC,GAAG,eAAO,CAAC,OAAO,CAAC,CAAC,GAAU,EAAwB,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,4BAAoB,CAAC,CAAC;IAC9G,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAY,EAAE,CAAY,EAAU,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,qGAAqG;AACrG,SAAgB,kBAAkB,CAAC,CAAmB;IAClD,MAAM,GAAG,GAAG,eAAO,CAAC,IAAI,CAAC,CAAC,CAAQ,EAAW,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,GAAG,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpG,OAAO,GAAG,CAAC;AACf,CAAC","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers three questions: do we govern this call at all, is the WRONG AGENT standing here, and is\n// the agent stranded away from the root? Drawn as a decision matrix that is SIX ordered rows over five\n// 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' | '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' | '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/** A is the same one boolean wearing the doc's own letters: `c` the coordinator, `s` a subagent. */\nexport type L1Agent = 'c' | 's' | '-';\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 = 'coordinator-in-worktree' | 'force-to-root';\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), A (coordinator 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 coordinator: 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 (`gitRoot === null`) 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 coordinator: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f' : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, coordinator, 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: L1Agent,\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 === 'c') !== c.coordinator) 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 six L1 rows, in first-match-wins order.\n *\n * Rows 3 and 5 are the two 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 rows 3 and 5 carry a blockId.\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 new L1Row(3, 'w', 'c', 'n', '-', '-', ACT_BLOCK,\n 'the coordinator\\'s guards do not follow its `cd` — delegate to a subagent bound to the worktree',\n new L1Cure('delegate to a subagent bound to the worktree', 'Spawn a subagent bound to that worktree', false),\n 'coordinator-in-worktree', [\n new L1UseCase(12,\n 'you are the **coordinator**, you ran `git worktree add ../wt`, and `cd ../wt && pnpm build` is blocked',\n '`w` / `c` / `n` — row 3',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): spawn a subagent bound to `<worktree>` — the Agent tool with worktree isolation, or have the subagent call `EnterWorktree` with `path: <worktree>` (it accepts a worktree you already created)<br>Do NOT: re-type the command, or conclude the harness ate your `cd` — it did not; your GUARDS did not follow it',\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` / `s` — row 3 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 'the coordinator\\'s `cd <worktree> && ls`/`cat`/`grep` still runs',\n '`w` / `c` / `y` — row 3 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 ]),\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];\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 'the coordinator\\'s `cd <worktree> && pnpm install` still runs while row 3 is live',\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/** A is the same one boolean wearing the doc's own letters: `c` the coordinator, `s` a subagent. */\nexport type L1Agent = 'c' | 's' | '-';\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 = 'coordinator-in-worktree' | '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), A (coordinator 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 coordinator: 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 coordinator: 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, coordinator, 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: L1Agent,\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 === 'c') !== c.coordinator) 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 new L1Row(3, 'w', 'c', 'n', '-', '-', ACT_BLOCK,\n 'the coordinator\\'s guards do not follow its `cd` — delegate to a subagent bound to the worktree',\n new L1Cure('delegate to a subagent bound to the worktree', 'Spawn a subagent bound to that worktree', false),\n 'coordinator-in-worktree', [\n new L1UseCase(12,\n 'you are the **coordinator**, you ran `git worktree add ../wt`, and `cd ../wt && pnpm build` is blocked',\n '`w` / `c` / `n` — row 3',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): spawn a subagent bound to `<worktree>` — the Agent tool with worktree isolation, or have the subagent call `EnterWorktree` with `path: <worktree>` (it accepts a worktree you already created)<br>Do NOT: re-type the command, or conclude the harness ate your `cd` — it did not; your GUARDS did not follow it',\n new L1Classification('w', true, false, false, false)),\n new L1UseCase(16,\n 'the same block for `cd .claude/worktrees/agent-XXXX && <work>` — the harness\\'s OWN worktree layout',\n '`w` / `c` / `n` — row 3; in-repo placement is still `w`',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): same as case 12 — spawn a subagent bound to that worktree<br>Do NOT: expect it to be exempt because it sits under the repo — K is git\\'s `--git-common-dir` answer, not a path test. This case used to read `f` (every guard silently off); if you are looking at an OLD release that is the difference',\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` / `s` — row 3 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 'the coordinator\\'s `cd <worktree> && ls`/`cat`/`grep` still runs',\n '`w` / `c` / `y` — row 3 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 'the coordinator\\'s `cd <worktree> && pnpm install` still runs while row 3 is live',\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"]}
@@ -0,0 +1,38 @@
1
+ import { EffectiveTree } from './effective-tree';
2
+ import { BlockedResult } from './types';
3
+ /**
4
+ * L1: the directory this command would run in DOES NOT EXIST.
5
+ *
6
+ * THE INCIDENT (reproduced from a transcript, 0.4.603). A subagent was launched with its cwd inside
7
+ * `<root>/.claude/worktrees/agent-a5931637c5bff6d6d`. Mid-session another agent's cleanup REAPED that
8
+ * worktree. The shell's cwd still named a path that was now gone.
9
+ *
10
+ * `classify()` asked git for the toplevel, got `null` — and `null` meant two completely different
11
+ * things: "this is not a git directory" and "this directory does not exist". Conflating them classified
12
+ * the dead path as the PRIMARY clone, so force-to-root fired and printed:
13
+ *
14
+ * ❌ Run git/gh commands from the repo root, not a subdirectory.
15
+ * Command runs in: <root>/.claude/worktrees/agent-a5931637c5bff6d6d
16
+ * Judged against: <root>
17
+ * cd '<root>' && cd <root>/.claude/worktrees/agent-… && git fetch …
18
+ *
19
+ * Three things wrong at once: the diagnosis (it is not a subdirectory, it is GONE), the tone (it reads
20
+ * as a scolding for a mistake nobody made), and the remedy (it ends inside the deleted directory, so it
21
+ * cannot satisfy the check — the retry re-fires with the prefix doubled, then tripled). Three rounds
22
+ * were burned before `git worktree list` made the real state visible.
23
+ *
24
+ * So the fix is a DISTINCT tree kind with its own message. Naming the state is most of the cure: an
25
+ * agent that is told the directory is gone stops trying to `cd` into it. The remedy deliberately routes
26
+ * through the governed root and drops the command's own leading `cd`, so it can never point back at the
27
+ * dead path — the same convergence property `remedyAtRoot` gives force-to-root.
28
+ *
29
+ * The lost-work line is not padding. A reaped worktree takes uncommitted changes with it, and an agent
30
+ * that silently retries at the root will otherwise report success on work that no longer exists.
31
+ */
32
+ export declare class MissingDirectoryGuard {
33
+ private readonly resolver;
34
+ /** The deny report, or null to allow. */
35
+ block(command: string, tree: EffectiveTree): string | null;
36
+ private report;
37
+ }
38
+ export declare function missingDirectoryBlock(command: string, tree: EffectiveTree): BlockedResult | null;