@webpieces/ai-hook-rules 0.4.622 → 0.4.624
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -19
- package/package.json +2 -2
- package/src/adapters/hook-core.js +13 -14
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/hook-registration.d.ts +160 -50
- package/src/bin/hook-registration.js +227 -96
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/managed-env.d.ts +46 -0
- package/src/bin/managed-env.js +50 -0
- package/src/bin/managed-env.js.map +1 -0
- package/src/bin/setup.d.ts +1 -2
- package/src/bin/setup.js +38 -46
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.js +12 -1
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +6 -0
- package/src/bin/shim-deny-reason.js +82 -0
- package/src/bin/shim-deny-reason.js.map +1 -0
- package/src/bin/shim.d.ts +0 -1
- package/src/bin/shim.js +3 -57
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.js +140 -30
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/decision-log.d.ts +3 -3
- package/src/core/decision-log.js +8 -8
- package/src/core/decision-log.js.map +1 -1
- package/src/core/effective-tree.d.ts +5 -2
- package/src/core/effective-tree.js +1 -1
- package/src/core/effective-tree.js.map +1 -1
- package/src/core/l0-matrix.js +15 -13
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l1-doc.js +29 -68
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l1-rows.d.ts +17 -9
- package/src/core/l1-rows.js +18 -13
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/log-stream.d.ts +6 -4
- package/src/core/log-stream.js +6 -4
- package/src/core/log-stream.js.map +1 -1
- package/src/core/log-streams.d.ts +13 -3
- package/src/core/log-streams.js +15 -5
- package/src/core/log-streams.js.map +1 -1
- package/src/core/runner.d.ts +1 -2
- package/src/core/runner.js +29 -24
- package/src/core/runner.js.map +1 -1
- package/src/core/version-sync.d.ts +67 -0
- package/src/core/version-sync.js +148 -0
- package/src/core/version-sync.js.map +1 -0
- package/src/core/webpieces-versions.d.ts +83 -0
- package/src/core/webpieces-versions.js +169 -0
- package/src/core/webpieces-versions.js.map +1 -0
- package/templates/ai-hook.sh +15 -4
- package/templates/claude-settings-hook.json +6 -3
- package/src/bin/guarantee-root.d.ts +0 -95
- package/src/bin/guarantee-root.js +0 -297
- package/src/bin/guarantee-root.js.map +0 -1
- package/src/core/coordinator-worktree.d.ts +0 -61
- package/src/core/coordinator-worktree.js +0 -94
- package/src/core/coordinator-worktree.js.map +0 -1
- package/templates/guarantee-root.sh +0 -113
package/src/core/l1-rows.js
CHANGED
|
@@ -19,19 +19,19 @@ exports.L1_PRESTAGE_ROW = '0';
|
|
|
19
19
|
/**
|
|
20
20
|
* One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
|
|
21
21
|
*
|
|
22
|
-
* The dimensions are exactly the doc's legend: K (tree kind of the resolved target),
|
|
22
|
+
* The dimensions are exactly the doc's legend: K (tree kind of the resolved target), V (webpieces versions in sync or
|
|
23
23
|
* subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).
|
|
24
24
|
*/
|
|
25
25
|
class L1Classification {
|
|
26
26
|
kind;
|
|
27
|
-
|
|
27
|
+
versionsSkewed;
|
|
28
28
|
readOnly;
|
|
29
29
|
git;
|
|
30
30
|
atRoot;
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape
|
|
32
|
-
constructor(kind,
|
|
32
|
+
constructor(kind, versionsSkewed, readOnly, git, atRoot) {
|
|
33
33
|
this.kind = kind;
|
|
34
|
-
this.
|
|
34
|
+
this.versionsSkewed = versionsSkewed;
|
|
35
35
|
this.readOnly = readOnly;
|
|
36
36
|
this.git = git;
|
|
37
37
|
this.atRoot = atRoot;
|
|
@@ -49,11 +49,11 @@ class L1Classification {
|
|
|
49
49
|
*/
|
|
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
|
-
static forEnforcement(treeKind,
|
|
52
|
+
static forEnforcement(treeKind, versionsSkewed, readOnly, git, atRoot) {
|
|
53
53
|
const kind = treeKind === 'foreign' ? 'f'
|
|
54
54
|
: treeKind === 'missing' ? 'm'
|
|
55
55
|
: treeKind === 'worktree' ? 'w' : 'p';
|
|
56
|
-
return new L1Classification(kind,
|
|
56
|
+
return new L1Classification(kind, versionsSkewed, readOnly, git, atRoot);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
exports.L1Classification = L1Classification;
|
|
@@ -151,7 +151,7 @@ class L1Row {
|
|
|
151
151
|
matches(c) {
|
|
152
152
|
if (!this.kindMatches(c.kind))
|
|
153
153
|
return false;
|
|
154
|
-
if (this.a !== '-' && (this.a === '
|
|
154
|
+
if (this.a !== '-' && (this.a === 'n') !== c.versionsSkewed)
|
|
155
155
|
return false;
|
|
156
156
|
if (!flagMatches(this.r, c.readOnly))
|
|
157
157
|
return false;
|
|
@@ -193,16 +193,21 @@ exports.L1_ROWS = [
|
|
|
193
193
|
new L1UseCase(1, '`cd repositories/vendored && git commit` goes through untouched', '`f` / `y` / - — row 1', 'ALLOW_EXEMPT', 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off', new L1Classification('f', false, false, true, false)),
|
|
194
194
|
]),
|
|
195
195
|
new L1Row(2, 'o', '-', '-', '-', '-', exports.ACT_DOWN, 'see "Not done" below', null, null, []),
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
// ROW 3 IS RETIRED — it was coordinator-in-worktree, deleted with CoordinatorWorktreeGuard when the
|
|
197
|
+
// guard hooks went ABSOLUTE (one governor, so the filesystem/governance split it policed became
|
|
198
|
+
// unconstructible). The NUMBER is never reused: row numbers are identity here — they are printed in
|
|
199
|
+
// denies, logged as `row=`, and cited in guards/L1-location.md — so renumbering would silently
|
|
200
|
+
// re-point every historical reference. Its replacement is row 8.
|
|
201
|
+
new L1Row(8, 'w', 'n', 'n', '-', '-', exports.ACT_BLOCK, 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it', new L1Cure('align the pins (same git hash -> same tracked pin -> one install in the main tree), work in the main tree, or use a separate clone', '@webpieces version SKEW', false), 'trinary-version-skew', [
|
|
202
|
+
new L1UseCase(12, 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked', '`w` / `n` / `n` — row 8', 'BLOCK_AI_CURE', 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then ONE `pnpm install` in the MAIN tree — the pin is tracked, so the same commit gives the same version and a worktree needs no install of its own<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a different version, use a separate CLONE — a clone gets its own node_modules and its own governance; a worktree borrows the main tree\'s and cannot<br>Do NOT: lower the MAIN tree\'s pin to match — that downgrades every tree, including this session\'s own governor', new L1Classification('w', true, false, false, false)),
|
|
203
|
+
new L1UseCase(16, 'a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX`', '`w` / `n` / `n` — row 8; in-repo placement is still `w`', 'BLOCK_AI_CURE', 'A subagent CANNOT fix this alone — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Report to the coordinator: "my worktree is on X, the main tree is on Y — one of us must move"<br>Do NOT: expect exemption because it sits under the repo — K is git\'s `--git-common-dir` answer, not a path test', new L1Classification('w', true, false, false, false)),
|
|
199
204
|
]),
|
|
200
205
|
new L1Row(4, 'pw', '-', '-', 'n', '-', exports.ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [
|
|
201
206
|
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)),
|
|
202
207
|
new L1UseCase(6, '`pnpm test` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', 'ALLOW (handed to L2)', 'none — deliberately untouched, so package-local test runs stay natural', new L1Classification('p', false, false, false, false)),
|
|
203
208
|
new L1UseCase(10, '`echo "cd sub && git push"` passes', '`pw` / `n` / `root` — row 4', 'ALLOW (handed to L2)', 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape', new L1Classification('p', false, false, false, true)),
|
|
204
|
-
new L1UseCase(13, 'the same command from a **subagent** runs normally', '`w` / `
|
|
205
|
-
new L1UseCase(14, '
|
|
209
|
+
new L1UseCase(13, 'the same command from a **subagent** runs normally', '`w` / `y` — row 8 does not match', 'ALLOW (handed to L2)', 'none — a subagent pinned to a worktree is the correct pattern', new L1Classification('w', false, false, false, false)),
|
|
210
|
+
new L1UseCase(14, 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`', '`w` / `n` / `y` — row 8 does not match', 'ALLOW (handed to L2)', 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you', new L1Classification('w', true, true, false, false)),
|
|
206
211
|
]),
|
|
207
212
|
new L1Row(5, 'pw', '-', '-', 'y', 'sub', exports.ACT_BLOCK, '`cd <root> && <original>`', new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true), 'force-to-root', [
|
|
208
213
|
new L1UseCase(7, '`git status` from `packages/http/` is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK_AI_CURE', 'Option 1 (preferred): `cd <root> && git status`', new L1Classification('p', false, false, true, false)),
|
|
@@ -230,7 +235,7 @@ exports.L1_UNROWED_USE_CASES = [
|
|
|
230
235
|
new L1UseCase(2, 'Edit `repositories/vendored/foo.ts` allowed even on stale main', 'filter — the path is in `excludePaths`', 'ALLOW_EXEMPT', 'none needed'),
|
|
231
236
|
new L1UseCase(3, 'Edit `packages/http/foo.ts` blocked on stale main', 'filter keeps the rules → L2 fires', 'BLOCK (at L2)', 'that is L2\'s write-on-main verdict, not L1\'s — follow the L2 message'),
|
|
232
237
|
new L1UseCase(4, 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`', 'filter, on the TARGET path', '→ L2', 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to "fix" it'),
|
|
233
|
-
new L1UseCase(15, '
|
|
238
|
+
new L1UseCase(15, '`cd <worktree> && pnpm install` still runs while row 8 is live — it is the CURE', 'L0 allowlist, ahead of L1', 'ALLOW', 'none — a cure must stay reachable from every tree'),
|
|
234
239
|
];
|
|
235
240
|
/** Every use case, in the doc's numbering — the order the table is rendered and read in. */
|
|
236
241
|
// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module
|
package/src/core/l1-rows.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers four questions: do we govern this call at all, does the directory still EXIST, is the\n// WRONG AGENT standing here, and is the agent stranded away from the root? Drawn as a decision matrix\n// that is SEVEN ordered rows over five dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/** 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"]}
|
|
1
|
+
{"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers four questions: do we govern this call at all, does the directory still EXIST, is the\n// WRONG AGENT standing here, and is the agent stranded away from the root? Drawn as a decision matrix\n// that is SEVEN ordered rows over five dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'm' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'm' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/**\n * V is the same one boolean wearing the doc's own letters: `n` the webpieces versions do NOT agree\n * between this worktree and the main tree, `y` they do.\n *\n * This dimension used to be A (`c` coordinator / `s` subagent). It was replaced rather than removed\n * because agent identity was measured untrustworthy as a proxy for \"which tree am I in\" — a\n * worktree-isolated agent auto-reaped at a turn boundary silently resumes on the primary clone. A\n * version read off the PATH being acted on cannot lie in that way.\n */\nexport type L1VersionSync = 'y' | 'n' | '-';\n\n/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */\nexport type L1ActionKind = 'exempt' | 'down' | 'block';\n\n/**\n * WHICH structural block a blocking row dispatches to. This is the field that makes the array\n * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,\n * so deleting a row from the array removes the block.\n */\nexport type L1BlockId = 'trinary-version-skew' | 'force-to-root' | 'missing-directory';\n\n/**\n * The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a\n * tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use\n * (asking L1_ROWS to classify it would need the very resolution its answer determines).\n *\n * ZERO rather than a seventh row, deliberately. It has to appear in the table — an L1 block the\n * generated doc did not describe is precisely the drift the table exists to prevent, and it was\n * carrying a `KNOWN GAP` comment saying so. But numbering it 7 would assert it sits in the same\n * first-match scan as the others, which is the one thing that is not true about it. Row 0 says\n * \"decided before the scan\" in the number itself. `renderL1Doc()` PRINTS this row above the six, so\n * `row=0` in the L1 log joins to a line the reader can actually find.\n */\nexport const L1_PRESTAGE_ROW = '0';\n\n/**\n * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.\n *\n * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), V (webpieces versions in sync or\n * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).\n */\nexport class L1Classification {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape\n constructor(\n readonly kind: L1Kind,\n readonly versionsSkewed: boolean,\n readonly readOnly: boolean,\n readonly git: boolean,\n readonly atRoot: boolean,\n ) {}\n\n /**\n * The classification the RUNNER enforces on, built from the resolved tree and the caller.\n *\n * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by\n * effective-tree.ts (git has no answer for the directory) and consumed NOWHERE, so a command in no git repo is\n * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what\n * SHOULD happen and is deliberately unreachable from here until the \"Not done\" fix in\n * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,\n * so the two ship together or neither does. Mapping it to `p` here is what preserves today's\n * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.\n */\n // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to\n // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject\n static forEnforcement(\n treeKind: TreeKind,\n versionsSkewed: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f'\n : treeKind === 'missing' ? 'm'\n : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, versionsSkewed, readOnly, git, atRoot);\n }\n}\n\n/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L1Action {\n constructor(readonly label: string, readonly kind: L1ActionKind) {}\n}\n\nexport const ACT_EXEMPT = new L1Action('2 exempt', 'exempt');\nexport const ACT_DOWN = new L1Action('→ L2', 'down');\nexport const ACT_BLOCK = new L1Action('4 block', 'block');\n\n/**\n * The CURE a blocking row prescribes.\n *\n * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,\n * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — \"spawn a\n * subagent bound to the worktree\" — which no allowlist can accept and no reclassification can model,\n * so it declares `runnable: false` and is asserted only on the deny text.\n */\nexport class L1Cure {\n constructor(\n /** How the doc's `why` column spells it. */\n readonly summary: string,\n /** A substring that MUST appear in the deny text the guard emits for this row. */\n readonly denyMention: string,\n readonly runnable: boolean,\n ) {}\n}\n\n/**\n * One row of the \"L1 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed\n * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement\n * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither\n * of which is a row) can carry `null` there.\n */\nexport class L1UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly classification: L1Classification | null = null,\n ) {}\n}\n\n/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */\nexport class L1Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases\n constructor(\n readonly num: number,\n readonly k: L1KindMatch,\n readonly a: L1VersionSync,\n readonly r: L1Flag,\n readonly g: L1Flag,\n readonly p: 'root' | 'sub' | '-',\n readonly action: L1Action,\n /** The `why` cell, verbatim. */\n readonly why: string,\n readonly cure: L1Cure | null,\n readonly blockId: L1BlockId | null,\n readonly useCases: readonly L1UseCase[],\n ) {}\n\n matches(c: L1Classification): boolean {\n if (!this.kindMatches(c.kind)) return false;\n if (this.a !== '-' && (this.a === 'n') !== c.versionsSkewed) return false;\n if (!flagMatches(this.r, c.readOnly)) return false;\n if (!flagMatches(this.g, c.git)) return false;\n return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');\n }\n\n private kindMatches(kind: L1Kind): boolean {\n if (this.k === '-') return true;\n if (this.k === 'pw') return kind === 'p' || kind === 'w';\n return this.k === kind;\n }\n}\n\n// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the\n// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)\n// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module\nfunction flagMatches(cell: L1Flag, value: boolean): boolean {\n if (cell === '-') return true;\n return (cell === 'y') === value;\n}\n\n/**\n * THE seven L1 rows, in first-match-wins order.\n *\n * Rows 3, 5 and 7 are the structural blocks and they run as ONE step (runner.l1LocationBlock) so they\n * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. \"L1 has no\n * objection\" — which is why only those three carry a blockId.\n *\n * Row 7 (`m`, the vanished directory) sits LAST only because row numbers are stable across releases —\n * they are printed in the doc and logged as `row=`, so renumbering rows 1-6 to slot it in front would\n * silently invalidate every existing reference. Position costs nothing here: `m` is matched by no other\n * row, so first-match reaches it wherever it sits.\n */\nexport const L1_ROWS: readonly L1Row[] = [\n new L1Row(1, 'f', '-', '-', '-', '-', ACT_EXEMPT, 'different git repo — hands off', null, null, [\n new L1UseCase(1,\n '`cd repositories/vendored && git commit` goes through untouched',\n '`f` / `y` / - — row 1',\n 'ALLOW_EXEMPT',\n 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off',\n new L1Classification('f', false, false, true, false)),\n ]),\n new L1Row(2, 'o', '-', '-', '-', '-', ACT_DOWN, 'see \"Not done\" below', null, null, []),\n // ROW 3 IS RETIRED — it was coordinator-in-worktree, deleted with CoordinatorWorktreeGuard when the\n // guard hooks went ABSOLUTE (one governor, so the filesystem/governance split it policed became\n // unconstructible). The NUMBER is never reused: row numbers are identity here — they are printed in\n // denies, logged as `row=`, and cited in guards/L1-location.md — so renumbering would silently\n // re-point every historical reference. Its replacement is row 8.\n new L1Row(8, 'w', 'n', 'n', '-', '-', ACT_BLOCK,\n 'this worktree pins a DIFFERENT @webpieces than the main tree that governs it',\n new L1Cure('align the pins (same git hash -> same tracked pin -> one install in the main tree), work in the main tree, or use a separate clone',\n '@webpieces version SKEW', false),\n 'trinary-version-skew', [\n new L1UseCase(12,\n 'a worktree on an older branch pins `0.4.612` while the main tree runs `0.4.616`, and `cd <wt> && pnpm build` is blocked',\n '`w` / `n` / `n` — row 8',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `git pull` BOTH trees onto the same main, then ONE `pnpm install` in the MAIN tree — the pin is tracked, so the same commit gives the same version and a worktree needs no install of its own<br>Option 2: do the work in the main tree, which this guard never blocks<br>Option 3: if the tree genuinely needs a different version, use a separate CLONE — a clone gets its own node_modules and its own governance; a worktree borrows the main tree\\'s and cannot<br>Do NOT: lower the MAIN tree\\'s pin to match — that downgrades every tree, including this session\\'s own governor',\n new L1Classification('w', true, false, false, false)),\n new L1UseCase(16,\n 'a SUBAGENT hits the same block inside `.claude/worktrees/agent-XXXX`',\n '`w` / `n` / `n` — row 8; in-repo placement is still `w`',\n 'BLOCK_AI_CURE',\n 'A subagent CANNOT fix this alone — the main tree is outside its tree, and a worktree-isolated agent may not even still be in the tree it was launched in (measured: auto-reaped at a turn boundary, resumed on the primary). Report to the coordinator: \"my worktree is on X, the main tree is on Y — one of us must move\"<br>Do NOT: expect exemption because it sits under the repo — K is git\\'s `--git-common-dir` answer, not a path test',\n new L1Classification('w', true, false, false, false)),\n ]),\n new L1Row(4, 'pw', '-', '-', 'n', '-', ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [\n new L1UseCase(5,\n '`ls` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — force-to-root has no jurisdiction over non-git commands',\n new L1Classification('p', false, true, false, false)),\n new L1UseCase(6,\n '`pnpm test` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n 'ALLOW (handed to L2)',\n 'none — deliberately untouched, so package-local test runs stay natural',\n new L1Classification('p', false, false, false, false)),\n new L1UseCase(10,\n '`echo \"cd sub && git push\"` passes',\n '`pw` / `n` / `root` — row 4',\n 'ALLOW (handed to L2)',\n 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape',\n new L1Classification('p', false, false, false, true)),\n new L1UseCase(13,\n 'the same command from a **subagent** runs normally',\n '`w` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — a subagent pinned to a worktree is the correct pattern',\n new L1Classification('w', false, false, false, false)),\n new L1UseCase(14,\n 'inspection inside a SKEWED worktree still runs — `cd <worktree> && ls`/`cat`/`grep`',\n '`w` / `n` / `y` — row 8 does not match',\n 'ALLOW (handed to L2)',\n 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you',\n new L1Classification('w', true, true, false, false)),\n ]),\n new L1Row(5, 'pw', '-', '-', 'y', 'sub', ACT_BLOCK, '`cd <root> && <original>`',\n new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true),\n 'force-to-root', [\n new L1UseCase(7,\n '`git status` from `packages/http/` is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(8,\n '`cd packages/http && git status` **typed from the root** is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(11,\n '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one',\n '`pw` / `y` / `sub` — row 5; force-to-root runs first',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\\'s real answer ← costs one extra turn by design; still blocked',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(17,\n 'the printed cure REPLACES your `cd`, it does not stack in front of it',\n '`pw` / `y` / `sub` — row 5, on the cure itself',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed line VERBATIM — `cd <root> && <the work>`, with your own leading `cd` dropped<br>Do NOT: paste `cd <root> && cd <subdir> && <work>`; `effectiveCwd` resolves the leading `cd`s left to right, so that lands in `<subdir>` again and re-fires this exact block',\n new L1Classification('p', false, false, true, false)),\n ]),\n new L1Row(6, 'pw', '-', '-', 'y', 'root', ACT_DOWN, '', null, null, [\n new L1UseCase(9,\n '`cd <root> && git status` passes from anywhere',\n '`pw` / `y` / `root` — row 6',\n 'ALLOW (handed to L2)',\n 'none — this IS the prescribed cure',\n new L1Classification('p', false, false, true, true)),\n ]),\n new L1Row(7, 'm', '-', '-', '-', '-', ACT_BLOCK, 'the directory is GONE — nothing can run there',\n new L1Cure('`cd <root> && <the work>`, never back through the dead path',\n 'no longer exists', true),\n 'missing-directory', [\n new L1UseCase(18,\n 'every command from a worktree another agent REAPED mid-session is blocked',\n '`m` — row 7',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): run the printed `cd <root> && <the work>` line — it does NOT route back through the dead path<br>Do NOT: re-`cd` into the worktree, or `git worktree add` it back expecting your uncommitted work; that work is gone',\n new L1Classification('m', false, false, true, false)),\n new L1UseCase(19,\n 'the same block for a NON-git command there — `m` does not care about G',\n '`m` — row 7; K alone decides it',\n 'BLOCK_AI_CURE',\n 'Option 1 (preferred): the same printed line. A vanished cwd is not a git question — nothing at all can run in a directory that does not exist',\n new L1Classification('m', false, false, false, false)),\n ]),\n];\n\n/**\n * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0\n * allowlist that runs ahead of L1 (15).\n *\n * They are use cases of L1 all the same — \"exempt\" is what emerges when the filter empties the rule\n * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the\n * doc's one numbered table. They carry no classification because no row classifies them.\n */\nexport const L1_UNROWED_USE_CASES: readonly L1UseCase[] = [\n new L1UseCase(2,\n 'Edit `repositories/vendored/foo.ts` allowed even on stale main',\n 'filter — the path is in `excludePaths`',\n 'ALLOW_EXEMPT',\n 'none needed'),\n new L1UseCase(3,\n 'Edit `packages/http/foo.ts` blocked on stale main',\n 'filter keeps the rules → L2 fires',\n 'BLOCK (at L2)',\n 'that is L2\\'s write-on-main verdict, not L1\\'s — follow the L2 message'),\n new L1UseCase(4,\n 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`',\n 'filter, on the TARGET path',\n '→ L2',\n 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to \"fix\" it'),\n new L1UseCase(15,\n '`cd <worktree> && pnpm install` still runs while row 8 is live — it is the CURE',\n 'L0 allowlist, ahead of L1',\n 'ALLOW',\n 'none — a cure must stay reachable from every tree'),\n];\n\n/** Every use case, in the doc's numbering — the order the table is rendered and read in. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module\nexport function allL1UseCases(): readonly L1UseCase[] {\n const all = [...L1_ROWS.flatMap((row: L1Row): readonly L1UseCase[] => row.useCases), ...L1_UNROWED_USE_CASES];\n return all.sort((a: L1UseCase, b: L1UseCase): number => a.num - b.num);\n}\n\n/**\n * FIRST MATCH WINS — the one lookup the guard and the tests share.\n *\n * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a\n * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly\n * that, which is why this returns L1Row rather than L1Row | null.\n */\n// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads\nexport function firstMatchingL1Row(c: L1Classification): L1Row {\n const row = L1_ROWS.find((r: L1Row): boolean => r.matches(c));\n if (row === undefined) throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);\n return row;\n}\n"]}
|
package/src/core/log-stream.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare class StreamIdentity {
|
|
|
24
24
|
* 1. **Parallel hooks.** The hooks reference says "when multiple PreToolUse hooks match a tool call,
|
|
25
25
|
* ALL matching hooks run in parallel". `wp-ai-rules-hook` and `wp-ai-guards-hook` both match
|
|
26
26
|
* Write/Edit/MultiEdit, so on every file edit TWO PROCESSES append to the same file at the same
|
|
27
|
-
* time. L-1's `guarantee-root.sh
|
|
27
|
+
* time. (A retired third hook, L-1's `guarantee-root.sh`, once made it three.)
|
|
28
28
|
* 2. **Subagents.** A subagent without worktree isolation shares the coordinator's tree.
|
|
29
29
|
* 3. **Whole sessions.** Four Claude Code windows on one clone are four coordinators, and `agent_id`
|
|
30
30
|
* is absent for every one of them — so agent identity alone cannot tell them apart.
|
|
@@ -37,10 +37,10 @@ export declare class StreamIdentity {
|
|
|
37
37
|
* ─── The key: the LAYER is the directory, the WRITER is the file ──────────────────────────────────
|
|
38
38
|
* <local>/logs/<stream>/<sessionId>-<agentId | "coordinator">-<hook>.log
|
|
39
39
|
*
|
|
40
|
-
* stream separates the LAYERS ('
|
|
40
|
+
* stream separates the LAYERS ('L0-shim' | 'L1-location' | 'L2-decisions' | …)
|
|
41
41
|
* sessionId separates concurrent Claude Code windows (`session_id`, on every hook payload)
|
|
42
42
|
* agentId separates subagents within one window (`agent_id`, subagent-only — absent = coordinator)
|
|
43
|
-
* hook separates the PARALLEL hooks ('guards' | 'rules'
|
|
43
|
+
* hook separates the PARALLEL hooks ('guards' | 'rules')
|
|
44
44
|
*
|
|
45
45
|
* This class owns the FILE half. One writer per file, by construction, so appends cannot interleave
|
|
46
46
|
* and nothing needs a lock — and all three identity dimensions must stay in the filename for that to
|
|
@@ -75,7 +75,9 @@ export declare class LogStream {
|
|
|
75
75
|
private hook;
|
|
76
76
|
/**
|
|
77
77
|
* Called once per invocation by the adapter that parsed the payload. `agentId` is empty for the
|
|
78
|
-
* coordinator — that absence IS the signal
|
|
78
|
+
* coordinator — that absence IS the signal — and renders as `coordinator`. Naming the writer file is
|
|
79
|
+
* the ONLY thing this identity is used for; which tree a call acts on is measured from the path (see
|
|
80
|
+
* core/version-sync.ts), never from who is asking.
|
|
79
81
|
* An empty `sessionId` renders as `unknown`: visible, never merged into another stream.
|
|
80
82
|
*/
|
|
81
83
|
identify(identity: StreamIdentity): void;
|
package/src/core/log-stream.js
CHANGED
|
@@ -32,7 +32,7 @@ exports.StreamIdentity = StreamIdentity;
|
|
|
32
32
|
* 1. **Parallel hooks.** The hooks reference says "when multiple PreToolUse hooks match a tool call,
|
|
33
33
|
* ALL matching hooks run in parallel". `wp-ai-rules-hook` and `wp-ai-guards-hook` both match
|
|
34
34
|
* Write/Edit/MultiEdit, so on every file edit TWO PROCESSES append to the same file at the same
|
|
35
|
-
* time. L-1's `guarantee-root.sh
|
|
35
|
+
* time. (A retired third hook, L-1's `guarantee-root.sh`, once made it three.)
|
|
36
36
|
* 2. **Subagents.** A subagent without worktree isolation shares the coordinator's tree.
|
|
37
37
|
* 3. **Whole sessions.** Four Claude Code windows on one clone are four coordinators, and `agent_id`
|
|
38
38
|
* is absent for every one of them — so agent identity alone cannot tell them apart.
|
|
@@ -45,10 +45,10 @@ exports.StreamIdentity = StreamIdentity;
|
|
|
45
45
|
* ─── The key: the LAYER is the directory, the WRITER is the file ──────────────────────────────────
|
|
46
46
|
* <local>/logs/<stream>/<sessionId>-<agentId | "coordinator">-<hook>.log
|
|
47
47
|
*
|
|
48
|
-
* stream separates the LAYERS ('
|
|
48
|
+
* stream separates the LAYERS ('L0-shim' | 'L1-location' | 'L2-decisions' | …)
|
|
49
49
|
* sessionId separates concurrent Claude Code windows (`session_id`, on every hook payload)
|
|
50
50
|
* agentId separates subagents within one window (`agent_id`, subagent-only — absent = coordinator)
|
|
51
|
-
* hook separates the PARALLEL hooks ('guards' | 'rules'
|
|
51
|
+
* hook separates the PARALLEL hooks ('guards' | 'rules')
|
|
52
52
|
*
|
|
53
53
|
* This class owns the FILE half. One writer per file, by construction, so appends cannot interleave
|
|
54
54
|
* and nothing needs a lock — and all three identity dimensions must stay in the filename for that to
|
|
@@ -88,7 +88,9 @@ class LogStream {
|
|
|
88
88
|
hook = 'hook';
|
|
89
89
|
/**
|
|
90
90
|
* Called once per invocation by the adapter that parsed the payload. `agentId` is empty for the
|
|
91
|
-
* coordinator — that absence IS the signal
|
|
91
|
+
* coordinator — that absence IS the signal — and renders as `coordinator`. Naming the writer file is
|
|
92
|
+
* the ONLY thing this identity is used for; which tree a call acts on is measured from the path (see
|
|
93
|
+
* core/version-sync.ts), never from who is asking.
|
|
92
94
|
* An empty `sessionId` renders as `unknown`: visible, never merged into another stream.
|
|
93
95
|
*/
|
|
94
96
|
identify(identity) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-stream.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/log-stream.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,MAAa,cAAc;IACd,SAAS,CAAS;IAClB,OAAO,CAAS;IAChB,IAAI,CAAS;IAEtB,YAAY,SAAiB,EAAE,OAAe,EAAE,IAAY;QACxD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAVD,wCAUC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAa,SAAS;IAClB,iGAAiG;IACjG,+FAA+F;IAC/F,iGAAiG;IACjG,gGAAgG;IAChG,oCAAoC;IAC5B,SAAS,GAAG,SAAS,CAAC;IACtB,OAAO,GAAG,EAAE,CAAC;IACb,IAAI,GAAG,MAAM,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,QAAwB;QAC7B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;OAWG;IACH,QAAQ;QACJ,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,MAAc;QACrB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC;IAChF,CAAC;CACJ;AAtDD,8BAsDC;AAED;;;;;GAKG;AACH,gIAAgI;AAChI,SAAS,OAAO,CAAC,GAAW;IACxB,MAAM,OAAO,GAAG,GAAG;SACd,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAG,iDAAiD;SACpF,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAY,2DAA2D;SAC9F,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAe,4BAA4B;SAC/D,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClB,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACU,QAAA,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC","sourcesContent":["/**\n * WHO is writing — the three fields that make a log filename unique, carried as one value.\n *\n * Data-only (per CLAUDE.md: classes for data, explicit construction). It exists as a class rather\n * than three loose parameters because the identity has to CROSS A PROCESS BOUNDARY: the detached\n * main-sync refresher is a separate node process, and the spawner hands it these three fields on\n * argv so parent and child write ONE stream. A single named carrier is what makes \"read it back out\n * of the parent, put it on argv, set it in the child\" a three-line round trip instead of three\n * parallel string parameters that can be reordered at either end.\n */\nexport class StreamIdentity {\n readonly sessionId: string;\n readonly agentId: string;\n readonly hook: string;\n\n constructor(sessionId: string, agentId: string, hook: string) {\n this.sessionId = sessionId;\n this.agentId = agentId;\n this.hook = hook;\n }\n}\n\n/**\n * WHICH LOG FILE does this hook invocation append to?\n *\n * ─── The bug this exists to fix ────────────────────────────────────────────────────────────────────\n * Log paths used to be keyed by the git WORKTREE alone (`<local>/logs/…`). Three separate things share\n * a worktree, so three separate things shared one file:\n *\n * 1. **Parallel hooks.** The hooks reference says \"when multiple PreToolUse hooks match a tool call,\n * ALL matching hooks run in parallel\". `wp-ai-rules-hook` and `wp-ai-guards-hook` both match\n * Write/Edit/MultiEdit, so on every file edit TWO PROCESSES append to the same file at the same\n * time. L-1's `guarantee-root.sh` makes it three.\n * 2. **Subagents.** A subagent without worktree isolation shares the coordinator's tree.\n * 3. **Whole sessions.** Four Claude Code windows on one clone are four coordinators, and `agent_id`\n * is absent for every one of them — so agent identity alone cannot tell them apart.\n *\n * `O_APPEND` is indivisible only under `PIPE_BUF`, which is **512 bytes on macOS**. Measured\n * 2026-08-06 across three repos: the invocation stream 208/3306 lines (6.3%) exceed it, max 608 B;\n * the decision stream 209/4097 (5.1%), max 625 B. So this tears TODAY, and the corrupted line\n * is exactly the long one — the `recover=` line a human needs most.\n *\n * ─── The key: the LAYER is the directory, the WRITER is the file ──────────────────────────────────\n * <local>/logs/<stream>/<sessionId>-<agentId | \"coordinator\">-<hook>.log\n *\n * stream separates the LAYERS ('L-1-cd' | 'L0-shim' | 'L1-location' | …)\n * sessionId separates concurrent Claude Code windows (`session_id`, on every hook payload)\n * agentId separates subagents within one window (`agent_id`, subagent-only — absent = coordinator)\n * hook separates the PARALLEL hooks ('guards' | 'rules' | 'guarantee-root')\n *\n * This class owns the FILE half. One writer per file, by construction, so appends cannot interleave\n * and nothing needs a lock — and all three identity dimensions must stay in the filename for that to\n * hold. `hook` especially: Claude Code runs the guards and rules hooks as separate processes IN\n * PARALLEL on one tool call, so folding `hook` into the directory would put two concurrent appenders\n * on one path, which is the tearing measured above.\n *\n * Nesting by STREAM is NOT the nesting this layout rejects. What it rejects is nesting by IDENTITY\n * (`sessions/<id>/<agent>/…`), which turns every cross-session question into a directory walk. The\n * layer is the one axis you almost always want to slice by first, and it was previously not\n * expressible at all: L1 had no stream, so \"show me every L1 decision\" had no answer. Now\n * `ls logs/L1-location/` is that answer, and a one-level wildcard recovers the flat view —\n * `ls -t logs/[*]/<sid>-*` is still every layer at once, in time order.\n *\n * `transcript_path` is also unique per session, but it is a filesystem PATH — long, and full of\n * separators that would have to be flattened anyway — and `session_id` is its stable identifier, so\n * session_id is the better key.\n *\n * The tree is still visible — every line already carries `root=` / `projectDir=` / `tree=` columns —\n * so nothing is lost by the filename not encoding it.\n *\n * ─── There is no un-split path ─────────────────────────────────────────────────────────────────────\n * Every name is prefixed, always. A caller that never identifies renders as\n * `unknown-coordinator-hook.log` — a distinct, greppable writer, NOT a shared file. Keeping a\n * bare-name fallback would have meant two reachable spellings of one filename, with the tearing one\n * reached by doing nothing; that is the widening-as-absence this whole class exists to remove, so it\n * is not offered.\n */\nexport class LogStream {\n // ALWAYS a real identity. There is no \"unset\" state and no bare-name branch, so there is exactly\n // ONE spelling of a log filename and a writer cannot reach the shared, tearing stream by doing\n // nothing. A caller with no Claude Code payload (the openclaw adapter, library consumers, specs)\n // gets UNIDENTIFIED below — which still prefixes, with `unknown`, so it is a distinct greppable\n // stream rather than a merge point.\n private sessionId = 'unknown';\n private agentId = '';\n private hook = 'hook';\n\n /**\n * Called once per invocation by the adapter that parsed the payload. `agentId` is empty for the\n * coordinator — that absence IS the signal, see AgentIdentity — and renders as `coordinator`.\n * An empty `sessionId` renders as `unknown`: visible, never merged into another stream.\n */\n identify(identity: StreamIdentity): void {\n this.sessionId = identity.sessionId;\n this.agentId = identity.agentId;\n this.hook = identity.hook;\n }\n\n /**\n * This process's identity, readable so it can be HANDED TO A CHILD PROCESS.\n *\n * The detached main-sync refresher (main-sync-refresh.ts → sync-main.ts) is a separate node\n * process with a fresh, unidentified `logStream`. Before this existed the parent logged\n * SPAWN_ATTEMPT to its own prefixed stream while the child logged START/FINISH/ERROR to the\n * shared, unidentified `unknown-coordinator-hook` writer — so ONE refresh cycle was split across\n * two files, every agent's child appended to that one shared file (the PIPE_BUF tearing this\n * class exists to remove, still live on that stream), and the documented\n * \"SPAWN_ATTEMPT with no START means the child never launched\" check read as a false failure on\n * every single cycle. The spawner now reads this and puts it on the child's argv.\n */\n identity(): StreamIdentity {\n return new StreamIdentity(this.sessionId, this.agentId, this.hook);\n }\n\n /**\n * This WRITER's file name within a stream directory —\n * `<sessionId>-<agentId|coordinator>-<hook><suffix>`, ALWAYS.\n *\n * The three identity dimensions stay in the FILE while the stream moves to the DIRECTORY\n * (see log-streams.ts), because they are what makes one writer per file true and the stream is not:\n * `wp-ai-guards-hook` and `wp-ai-rules-hook` are separate processes that Claude Code launches in\n * PARALLEL on the same tool call, so dropping `hook` here would put two concurrent appenders on\n * one path — the exact tearing this class exists to remove, reintroduced by a rename.\n *\n * `suffix` carries the extension so the rotation sibling gets the identical writer key: pass\n * `.log`, and separately `.1.log`.\n */\n writerFile(suffix: string): string {\n const agent = segment(this.agentId === '' ? 'coordinator' : this.agentId);\n return `${segment(this.sessionId)}-${agent}-${segment(this.hook)}${suffix}`;\n }\n}\n\n/**\n * One path segment, sanitised. `session_id` and `agent_id` arrive from a JSON payload, so they are\n * UNTRUSTED INPUT being used to build a filesystem path: `../../../etc` must become a harmless name and\n * never escape the logs directory. Everything outside `[A-Za-z0-9._-]` collapses to `_`, a leading dot\n * is neutralised so nothing becomes a hidden file or `..`, and the result is capped and never empty.\n */\n// webpieces-disable no-function-outside-class -- pure string sanitiser, the module's own leaf helper beside the class it serves\nfunction segment(raw: string): string {\n const cleaned = raw\n .replace(/[^A-Za-z0-9._-]/g, '_') // kills every separator, so nothing can traverse\n .replace(/\\.{2,}/g, '_') // and no run of dots survives, so no segment reads as `..`\n .replace(/^\\.+/, '_') // nor becomes a hidden file\n .slice(0, 64);\n return cleaned === '' ? 'unknown' : cleaned;\n}\n\n/**\n * Process-wide instance. The hook adapters identify it once at the top of the invocation and every\n * writer downstream reads it, which is what keeps `logGuardDecision()` / `logRejection()` signatures\n * unchanged — the alternative was threading three more parameters through every call site.\n */\nexport const logStream = new LogStream();\n"]}
|
|
1
|
+
{"version":3,"file":"log-stream.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/log-stream.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,MAAa,cAAc;IACd,SAAS,CAAS;IAClB,OAAO,CAAS;IAChB,IAAI,CAAS;IAEtB,YAAY,SAAiB,EAAE,OAAe,EAAE,IAAY;QACxD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAVD,wCAUC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAa,SAAS;IAClB,iGAAiG;IACjG,+FAA+F;IAC/F,iGAAiG;IACjG,gGAAgG;IAChG,oCAAoC;IAC5B,SAAS,GAAG,SAAS,CAAC;IACtB,OAAO,GAAG,EAAE,CAAC;IACb,IAAI,GAAG,MAAM,CAAC;IAEtB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAwB;QAC7B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;OAWG;IACH,QAAQ;QACJ,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,MAAc;QACrB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC;IAChF,CAAC;CACJ;AAxDD,8BAwDC;AAED;;;;;GAKG;AACH,gIAAgI;AAChI,SAAS,OAAO,CAAC,GAAW;IACxB,MAAM,OAAO,GAAG,GAAG;SACd,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAG,iDAAiD;SACpF,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAY,2DAA2D;SAC9F,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAe,4BAA4B;SAC/D,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClB,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACU,QAAA,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC","sourcesContent":["/**\n * WHO is writing — the three fields that make a log filename unique, carried as one value.\n *\n * Data-only (per CLAUDE.md: classes for data, explicit construction). It exists as a class rather\n * than three loose parameters because the identity has to CROSS A PROCESS BOUNDARY: the detached\n * main-sync refresher is a separate node process, and the spawner hands it these three fields on\n * argv so parent and child write ONE stream. A single named carrier is what makes \"read it back out\n * of the parent, put it on argv, set it in the child\" a three-line round trip instead of three\n * parallel string parameters that can be reordered at either end.\n */\nexport class StreamIdentity {\n readonly sessionId: string;\n readonly agentId: string;\n readonly hook: string;\n\n constructor(sessionId: string, agentId: string, hook: string) {\n this.sessionId = sessionId;\n this.agentId = agentId;\n this.hook = hook;\n }\n}\n\n/**\n * WHICH LOG FILE does this hook invocation append to?\n *\n * ─── The bug this exists to fix ────────────────────────────────────────────────────────────────────\n * Log paths used to be keyed by the git WORKTREE alone (`<local>/logs/…`). Three separate things share\n * a worktree, so three separate things shared one file:\n *\n * 1. **Parallel hooks.** The hooks reference says \"when multiple PreToolUse hooks match a tool call,\n * ALL matching hooks run in parallel\". `wp-ai-rules-hook` and `wp-ai-guards-hook` both match\n * Write/Edit/MultiEdit, so on every file edit TWO PROCESSES append to the same file at the same\n * time. (A retired third hook, L-1's `guarantee-root.sh`, once made it three.)\n * 2. **Subagents.** A subagent without worktree isolation shares the coordinator's tree.\n * 3. **Whole sessions.** Four Claude Code windows on one clone are four coordinators, and `agent_id`\n * is absent for every one of them — so agent identity alone cannot tell them apart.\n *\n * `O_APPEND` is indivisible only under `PIPE_BUF`, which is **512 bytes on macOS**. Measured\n * 2026-08-06 across three repos: the invocation stream 208/3306 lines (6.3%) exceed it, max 608 B;\n * the decision stream 209/4097 (5.1%), max 625 B. So this tears TODAY, and the corrupted line\n * is exactly the long one — the `recover=` line a human needs most.\n *\n * ─── The key: the LAYER is the directory, the WRITER is the file ──────────────────────────────────\n * <local>/logs/<stream>/<sessionId>-<agentId | \"coordinator\">-<hook>.log\n *\n * stream separates the LAYERS ('L0-shim' | 'L1-location' | 'L2-decisions' | …)\n * sessionId separates concurrent Claude Code windows (`session_id`, on every hook payload)\n * agentId separates subagents within one window (`agent_id`, subagent-only — absent = coordinator)\n * hook separates the PARALLEL hooks ('guards' | 'rules')\n *\n * This class owns the FILE half. One writer per file, by construction, so appends cannot interleave\n * and nothing needs a lock — and all three identity dimensions must stay in the filename for that to\n * hold. `hook` especially: Claude Code runs the guards and rules hooks as separate processes IN\n * PARALLEL on one tool call, so folding `hook` into the directory would put two concurrent appenders\n * on one path, which is the tearing measured above.\n *\n * Nesting by STREAM is NOT the nesting this layout rejects. What it rejects is nesting by IDENTITY\n * (`sessions/<id>/<agent>/…`), which turns every cross-session question into a directory walk. The\n * layer is the one axis you almost always want to slice by first, and it was previously not\n * expressible at all: L1 had no stream, so \"show me every L1 decision\" had no answer. Now\n * `ls logs/L1-location/` is that answer, and a one-level wildcard recovers the flat view —\n * `ls -t logs/[*]/<sid>-*` is still every layer at once, in time order.\n *\n * `transcript_path` is also unique per session, but it is a filesystem PATH — long, and full of\n * separators that would have to be flattened anyway — and `session_id` is its stable identifier, so\n * session_id is the better key.\n *\n * The tree is still visible — every line already carries `root=` / `projectDir=` / `tree=` columns —\n * so nothing is lost by the filename not encoding it.\n *\n * ─── There is no un-split path ─────────────────────────────────────────────────────────────────────\n * Every name is prefixed, always. A caller that never identifies renders as\n * `unknown-coordinator-hook.log` — a distinct, greppable writer, NOT a shared file. Keeping a\n * bare-name fallback would have meant two reachable spellings of one filename, with the tearing one\n * reached by doing nothing; that is the widening-as-absence this whole class exists to remove, so it\n * is not offered.\n */\nexport class LogStream {\n // ALWAYS a real identity. There is no \"unset\" state and no bare-name branch, so there is exactly\n // ONE spelling of a log filename and a writer cannot reach the shared, tearing stream by doing\n // nothing. A caller with no Claude Code payload (the openclaw adapter, library consumers, specs)\n // gets UNIDENTIFIED below — which still prefixes, with `unknown`, so it is a distinct greppable\n // stream rather than a merge point.\n private sessionId = 'unknown';\n private agentId = '';\n private hook = 'hook';\n\n /**\n * Called once per invocation by the adapter that parsed the payload. `agentId` is empty for the\n * coordinator — that absence IS the signal — and renders as `coordinator`. Naming the writer file is\n * the ONLY thing this identity is used for; which tree a call acts on is measured from the path (see\n * core/version-sync.ts), never from who is asking.\n * An empty `sessionId` renders as `unknown`: visible, never merged into another stream.\n */\n identify(identity: StreamIdentity): void {\n this.sessionId = identity.sessionId;\n this.agentId = identity.agentId;\n this.hook = identity.hook;\n }\n\n /**\n * This process's identity, readable so it can be HANDED TO A CHILD PROCESS.\n *\n * The detached main-sync refresher (main-sync-refresh.ts → sync-main.ts) is a separate node\n * process with a fresh, unidentified `logStream`. Before this existed the parent logged\n * SPAWN_ATTEMPT to its own prefixed stream while the child logged START/FINISH/ERROR to the\n * shared, unidentified `unknown-coordinator-hook` writer — so ONE refresh cycle was split across\n * two files, every agent's child appended to that one shared file (the PIPE_BUF tearing this\n * class exists to remove, still live on that stream), and the documented\n * \"SPAWN_ATTEMPT with no START means the child never launched\" check read as a false failure on\n * every single cycle. The spawner now reads this and puts it on the child's argv.\n */\n identity(): StreamIdentity {\n return new StreamIdentity(this.sessionId, this.agentId, this.hook);\n }\n\n /**\n * This WRITER's file name within a stream directory —\n * `<sessionId>-<agentId|coordinator>-<hook><suffix>`, ALWAYS.\n *\n * The three identity dimensions stay in the FILE while the stream moves to the DIRECTORY\n * (see log-streams.ts), because they are what makes one writer per file true and the stream is not:\n * `wp-ai-guards-hook` and `wp-ai-rules-hook` are separate processes that Claude Code launches in\n * PARALLEL on the same tool call, so dropping `hook` here would put two concurrent appenders on\n * one path — the exact tearing this class exists to remove, reintroduced by a rename.\n *\n * `suffix` carries the extension so the rotation sibling gets the identical writer key: pass\n * `.log`, and separately `.1.log`.\n */\n writerFile(suffix: string): string {\n const agent = segment(this.agentId === '' ? 'coordinator' : this.agentId);\n return `${segment(this.sessionId)}-${agent}-${segment(this.hook)}${suffix}`;\n }\n}\n\n/**\n * One path segment, sanitised. `session_id` and `agent_id` arrive from a JSON payload, so they are\n * UNTRUSTED INPUT being used to build a filesystem path: `../../../etc` must become a harmless name and\n * never escape the logs directory. Everything outside `[A-Za-z0-9._-]` collapses to `_`, a leading dot\n * is neutralised so nothing becomes a hidden file or `..`, and the result is capped and never empty.\n */\n// webpieces-disable no-function-outside-class -- pure string sanitiser, the module's own leaf helper beside the class it serves\nfunction segment(raw: string): string {\n const cleaned = raw\n .replace(/[^A-Za-z0-9._-]/g, '_') // kills every separator, so nothing can traverse\n .replace(/\\.{2,}/g, '_') // and no run of dots survives, so no segment reads as `..`\n .replace(/^\\.+/, '_') // nor becomes a hidden file\n .slice(0, 64);\n return cleaned === '' ? 'unknown' : cleaned;\n}\n\n/**\n * Process-wide instance. The hook adapters identify it once at the top of the invocation and every\n * writer downstream reads it, which is what keeps `logGuardDecision()` / `logRejection()` signatures\n * unchanged — the alternative was threading three more parameters through every call site.\n */\nexport const logStream = new LogStream();\n"]}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* either a suffix on a flat name or, for L1, nothing at all.
|
|
8
8
|
*
|
|
9
9
|
* These live in ai-hook-rules beside LogStream, because all FOUR writers are here of this layout and
|
|
10
|
-
*
|
|
11
|
-
* rendered from `bin/
|
|
10
|
+
* not all of them are TypeScript: the L0 `ai-hook.sh` is POSIX sh
|
|
11
|
+
* rendered from `bin/shim-audit-log.ts`, and it must spell these
|
|
12
12
|
* directories identically or the layers scatter. One constant, four consumers — the same reason
|
|
13
13
|
* LogStream itself lives here.
|
|
14
14
|
*
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
* wildcard recovers the flat view here — `ls -t logs/[star]/<sid>-*` is still every stream in time
|
|
24
24
|
* order, across every layer at once.
|
|
25
25
|
*/
|
|
26
|
-
export declare const LMINUS1_CD_STREAM = "L-1-cd";
|
|
27
26
|
export declare const L0_SHIM_STREAM = "L0-shim";
|
|
28
27
|
export declare const L1_LOCATION_STREAM = "L1-location";
|
|
29
28
|
export declare const L2_DECISIONS_STREAM = "L2-decisions";
|
|
@@ -36,3 +35,14 @@ export declare const REJECTIONS_STREAM = "rejections";
|
|
|
36
35
|
* a doc sentence.
|
|
37
36
|
*/
|
|
38
37
|
export declare const ALL_LOG_STREAMS: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* Stream directories an OLDER release wrote and this one never will. Kept beside the live list because a
|
|
40
|
+
* directory that is in neither is invisible to every sweep, so its files would sit on disk forever.
|
|
41
|
+
*
|
|
42
|
+
* `SWEEPABLE_LOG_STREAMS` — live + retired — is what retention and the layout specs must enumerate;
|
|
43
|
+
* `ALL_LOG_STREAMS` stays the answer to "what may be WRITTEN". Keeping the two separate is what stops a
|
|
44
|
+
* retired name from quietly becoming a writable one again.
|
|
45
|
+
*/
|
|
46
|
+
export declare const RETIRED_LOG_STREAMS: readonly string[];
|
|
47
|
+
/** Every directory retention may reap: what this release writes, plus what older ones left behind. */
|
|
48
|
+
export declare const SWEEPABLE_LOG_STREAMS: readonly string[];
|
package/src/core/log-streams.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ALL_LOG_STREAMS = exports.REJECTIONS_STREAM = exports.ASYNC_REFRESH_STREAM = exports.CALLS_STREAM = exports.L2_DECISIONS_STREAM = exports.L1_LOCATION_STREAM = exports.L0_SHIM_STREAM =
|
|
3
|
+
exports.SWEEPABLE_LOG_STREAMS = exports.RETIRED_LOG_STREAMS = exports.ALL_LOG_STREAMS = exports.REJECTIONS_STREAM = exports.ASYNC_REFRESH_STREAM = exports.CALLS_STREAM = exports.L2_DECISIONS_STREAM = exports.L1_LOCATION_STREAM = exports.L0_SHIM_STREAM = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* THE STREAM DIRECTORIES — `<state>/logs/<stream>/<sessionId>-<agentId|coordinator>-<hook>.log`.
|
|
6
6
|
*
|
|
@@ -10,8 +10,8 @@ exports.ALL_LOG_STREAMS = exports.REJECTIONS_STREAM = exports.ASYNC_REFRESH_STRE
|
|
|
10
10
|
* either a suffix on a flat name or, for L1, nothing at all.
|
|
11
11
|
*
|
|
12
12
|
* These live in ai-hook-rules beside LogStream, because all FOUR writers are here of this layout and
|
|
13
|
-
*
|
|
14
|
-
* rendered from `bin/
|
|
13
|
+
* not all of them are TypeScript: the L0 `ai-hook.sh` is POSIX sh
|
|
14
|
+
* rendered from `bin/shim-audit-log.ts`, and it must spell these
|
|
15
15
|
* directories identically or the layers scatter. One constant, four consumers — the same reason
|
|
16
16
|
* LogStream itself lives here.
|
|
17
17
|
*
|
|
@@ -26,7 +26,6 @@ exports.ALL_LOG_STREAMS = exports.REJECTIONS_STREAM = exports.ASYNC_REFRESH_STRE
|
|
|
26
26
|
* wildcard recovers the flat view here — `ls -t logs/[star]/<sid>-*` is still every stream in time
|
|
27
27
|
* order, across every layer at once.
|
|
28
28
|
*/
|
|
29
|
-
exports.LMINUS1_CD_STREAM = 'L-1-cd';
|
|
30
29
|
exports.L0_SHIM_STREAM = 'L0-shim';
|
|
31
30
|
exports.L1_LOCATION_STREAM = 'L1-location';
|
|
32
31
|
exports.L2_DECISIONS_STREAM = 'L2-decisions';
|
|
@@ -39,7 +38,18 @@ exports.REJECTIONS_STREAM = 'rejections';
|
|
|
39
38
|
* a doc sentence.
|
|
40
39
|
*/
|
|
41
40
|
exports.ALL_LOG_STREAMS = [
|
|
42
|
-
exports.
|
|
41
|
+
exports.L0_SHIM_STREAM, exports.L1_LOCATION_STREAM, exports.L2_DECISIONS_STREAM,
|
|
43
42
|
exports.CALLS_STREAM, exports.ASYNC_REFRESH_STREAM, exports.REJECTIONS_STREAM,
|
|
44
43
|
];
|
|
44
|
+
/**
|
|
45
|
+
* Stream directories an OLDER release wrote and this one never will. Kept beside the live list because a
|
|
46
|
+
* directory that is in neither is invisible to every sweep, so its files would sit on disk forever.
|
|
47
|
+
*
|
|
48
|
+
* `SWEEPABLE_LOG_STREAMS` — live + retired — is what retention and the layout specs must enumerate;
|
|
49
|
+
* `ALL_LOG_STREAMS` stays the answer to "what may be WRITTEN". Keeping the two separate is what stops a
|
|
50
|
+
* retired name from quietly becoming a writable one again.
|
|
51
|
+
*/
|
|
52
|
+
exports.RETIRED_LOG_STREAMS = ['L-1-cd'];
|
|
53
|
+
/** Every directory retention may reap: what this release writes, plus what older ones left behind. */
|
|
54
|
+
exports.SWEEPABLE_LOG_STREAMS = [...exports.ALL_LOG_STREAMS, ...exports.RETIRED_LOG_STREAMS];
|
|
45
55
|
//# sourceMappingURL=log-streams.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-streams.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/log-streams.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACU,QAAA,
|
|
1
|
+
{"version":3,"file":"log-streams.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/log-streams.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACU,QAAA,cAAc,GAAG,SAAS,CAAC;AAC3B,QAAA,kBAAkB,GAAG,aAAa,CAAC;AACnC,QAAA,mBAAmB,GAAG,cAAc,CAAC;AACrC,QAAA,YAAY,GAAG,OAAO,CAAC;AACvB,QAAA,oBAAoB,GAAG,eAAe,CAAC;AACvC,QAAA,iBAAiB,GAAG,YAAY,CAAC;AAE9C;;;;GAIG;AACU,QAAA,eAAe,GAAsB;IAC9C,sBAAc,EAAE,0BAAkB,EAAE,2BAAmB;IACvD,oBAAY,EAAE,4BAAoB,EAAE,yBAAiB;CACxD,CAAC;AAEF;;;;;;;GAOG;AACU,QAAA,mBAAmB,GAAsB,CAAC,QAAQ,CAAC,CAAC;AAEjE,sGAAsG;AACzF,QAAA,qBAAqB,GAAsB,CAAC,GAAG,uBAAe,EAAE,GAAG,2BAAmB,CAAC,CAAC","sourcesContent":["/**\n * THE STREAM DIRECTORIES — `<state>/logs/<stream>/<sessionId>-<agentId|coordinator>-<hook>.log`.\n *\n * The stream is the DIRECTORY and the writer is the FILE. That split is the whole point: it puts the\n * LAYER in the path, so \"show me every L1 decision this session\" is one glob\n * (`logs/L1-location/<sid>-*`) instead of a question no glob could answer — every layer used to be\n * either a suffix on a flat name or, for L1, nothing at all.\n *\n * These live in ai-hook-rules beside LogStream, because all FOUR writers are here of this layout and\n * not all of them are TypeScript: the L0 `ai-hook.sh` is POSIX sh\n * rendered from `bin/shim-audit-log.ts`, and it must spell these\n * directories identically or the layers scatter. One constant, four consumers — the same reason\n * LogStream itself lives here.\n *\n * NOT in rules-config, and that is load-bearing rather than tidiness: the `wp-*` bins are SPAWNED as\n * processes with no tsconfig path mapping, so they resolve `@webpieces/rules-config` to the PUBLISHED\n * copy in node_modules — a release behind local source. A stream name defined there would render as\n * `undefined` inside a spawned `wp-upgrade-shim` while the in-process renderer used the real value,\n * and the two shims would differ by exactly the constant. Same-package means same source, always.\n *\n * Nesting by STREAM is not the nesting that was rejected. What `logs/` refused was nesting by\n * IDENTITY (`sessions/<id>/<agent>/…`), which breaks every cross-session question. A one-level\n * wildcard recovers the flat view here — `ls -t logs/[star]/<sid>-*` is still every stream in time\n * order, across every layer at once.\n */\nexport const L0_SHIM_STREAM = 'L0-shim';\nexport const L1_LOCATION_STREAM = 'L1-location';\nexport const L2_DECISIONS_STREAM = 'L2-decisions';\nexport const CALLS_STREAM = 'calls';\nexport const ASYNC_REFRESH_STREAM = 'async-refresh';\nexport const REJECTIONS_STREAM = 'rejections';\n\n/**\n * Every stream directory, for the sweeps and specs that must enumerate them rather than name one.\n * A new stream that is not added here is invisible to retention — which is why this is a list and not\n * a doc sentence.\n */\nexport const ALL_LOG_STREAMS: readonly string[] = [\n L0_SHIM_STREAM, L1_LOCATION_STREAM, L2_DECISIONS_STREAM,\n CALLS_STREAM, ASYNC_REFRESH_STREAM, REJECTIONS_STREAM,\n];\n\n/**\n * Stream directories an OLDER release wrote and this one never will. Kept beside the live list because a\n * directory that is in neither is invisible to every sweep, so its files would sit on disk forever.\n *\n * `SWEEPABLE_LOG_STREAMS` — live + retired — is what retention and the layout specs must enumerate;\n * `ALL_LOG_STREAMS` stays the answer to \"what may be WRITTEN\". Keeping the two separate is what stops a\n * retired name from quietly becoming a writable one again.\n */\nexport const RETIRED_LOG_STREAMS: readonly string[] = ['L-1-cd'];\n\n/** Every directory retention may reap: what this release writes, plus what older ones left behind. */\nexport const SWEEPABLE_LOG_STREAMS: readonly string[] = [...ALL_LOG_STREAMS, ...RETIRED_LOG_STREAMS];\n"]}
|
package/src/core/runner.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { ExcludePaths } from '@webpieces/rules-config';
|
|
2
|
-
import { AgentIdentity } from './coordinator-worktree';
|
|
3
2
|
import { ToolKind, NormalizedToolInput, BlockedResult, HookMode, Rule, Violation, EditContext, FileContext, BashContext } from './types';
|
|
4
3
|
export declare function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[];
|
|
5
4
|
export declare function effectiveBashCwd(command: string, cwd: string): string;
|
|
6
5
|
export declare function isGitOrGhCommand(command: string): boolean;
|
|
7
6
|
export declare function run(toolKind: ToolKind, input: NormalizedToolInput, cwd: string, mode?: HookMode): BlockedResult | null;
|
|
8
|
-
export declare function runBash(command: string, cwd: string, mode?: HookMode
|
|
7
|
+
export declare function runBash(command: string, cwd: string, mode?: HookMode): BlockedResult | null;
|
|
9
8
|
/**
|
|
10
9
|
* The Read path. Deliberately NOT `run()`:
|
|
11
10
|
*
|