@webpieces/ai-hook-rules 0.4.608 → 0.4.610
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 +1 -1
- package/package.json +2 -2
- package/src/adapters/claude-code-response.js +6 -2
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +13 -10
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/guarantee-root.js +23 -10
- package/src/bin/guarantee-root.js.map +1 -1
- package/src/bin/l0-allowlist.js +1 -1
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +0 -3
- package/src/bin/shim-audit-log.js +14 -13
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim.js +2 -2
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +75 -10
- package/src/core/decision-log.js +91 -28
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-fault-codes.d.ts +1 -1
- package/src/core/l0-fault-codes.js +1 -1
- package/src/core/l0-fault-codes.js.map +1 -1
- package/src/core/l1-doc.js +6 -0
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l1-rows.d.ts +13 -0
- package/src/core/l1-rows.js +24 -11
- package/src/core/l1-rows.js.map +1 -1
- package/src/core/log-stream.d.ts +32 -18
- package/src/core/log-stream.js +33 -19
- package/src/core/log-stream.js.map +1 -1
- package/src/core/log-streams.d.ts +38 -0
- package/src/core/log-streams.js +45 -0
- package/src/core/log-streams.js.map +1 -0
- package/src/core/main-sync-log.d.ts +2 -2
- package/src/core/main-sync-log.js +22 -15
- package/src/core/main-sync-log.js.map +1 -1
- package/src/core/main-sync-refresh.d.ts +3 -3
- package/src/core/main-sync-refresh.js +4 -4
- package/src/core/main-sync-refresh.js.map +1 -1
- package/src/core/rejection-log.d.ts +1 -1
- package/src/core/rejection-log.js +13 -12
- package/src/core/rejection-log.js.map +1 -1
- package/src/core/rules/feature-branch-guard.d.ts +11 -0
- package/src/core/rules/feature-branch-guard.js +20 -5
- package/src/core/rules/feature-branch-guard.js.map +1 -1
- package/src/core/rules/merged-branch-bash-guard.d.ts +12 -1
- package/src/core/rules/merged-branch-bash-guard.js +21 -6
- package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
- package/src/core/rules/read-stale-guard.d.ts +11 -0
- package/src/core/rules/read-stale-guard.js +25 -10
- package/src/core/rules/read-stale-guard.js.map +1 -1
- package/src/core/rules/stale-main-bash-guard.d.ts +11 -0
- package/src/core/rules/stale-main-bash-guard.js +22 -7
- package/src/core/rules/stale-main-bash-guard.js.map +1 -1
- package/src/core/runner.js +34 -15
- package/src/core/runner.js.map +1 -1
- package/src/core/sync-main.d.ts +1 -1
- package/src/core/sync-main.js +2 -2
- package/src/core/sync-main.js.map +1 -1
- package/templates/ai-hook.sh +11 -8
- package/templates/guarantee-root.sh +10 -7
package/src/core/decision-log.js
CHANGED
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.invocationLog = exports.InvocationLog = exports.GuardInvocation = exports.GuardDecision = void 0;
|
|
3
|
+
exports.invocationLog = exports.InvocationLog = exports.GuardInvocation = exports.GuardDecision = exports.MATRIX_L2 = exports.MATRIX_L0 = exports.MatrixRef = void 0;
|
|
4
4
|
exports.logGuardDecision = logGuardDecision;
|
|
5
|
+
exports.logL1Decision = logL1Decision;
|
|
5
6
|
exports.branchForLog = branchForLog;
|
|
6
7
|
const tslib_1 = require("tslib");
|
|
7
8
|
const child_process_1 = require("child_process");
|
|
8
9
|
const fs = tslib_1.__importStar(require("fs"));
|
|
9
10
|
const path = tslib_1.__importStar(require("path"));
|
|
10
11
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
12
|
+
const log_streams_1 = require("./log-streams");
|
|
11
13
|
const l0_fault_codes_1 = require("./l0-fault-codes");
|
|
12
14
|
const to_error_1 = require("./to-error");
|
|
13
15
|
const log_stream_1 = require("./log-stream");
|
|
14
16
|
// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is
|
|
15
|
-
// the ASYNC log (
|
|
17
|
+
// the ASYNC log (the `async-refresh/` stream, written by the detached refresher in main-sync-log.ts). This
|
|
16
18
|
// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES
|
|
17
19
|
// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is
|
|
18
|
-
// traceable to a stale or missing async write. Writes to `.webpieces/logs
|
|
19
|
-
//
|
|
20
|
-
const LOG_FILE = 'guard-sync-decisions.log';
|
|
21
|
-
const LOG_FILE_PREV = 'guard-sync-decisions.1.log';
|
|
22
|
-
// The per-INVOCATION stream (companion to the per-DECISION log above): one line for every guards-hook
|
|
23
|
-
// call, so cleanup automation can mine tool + branch + sync-status + OUTCOME over time. See InvocationLog.
|
|
24
|
-
const INVOCATION_LOG_FILE = 'guard-invocations.log';
|
|
25
|
-
const INVOCATION_LOG_FILE_PREV = 'guard-invocations.1.log';
|
|
20
|
+
// traceable to a stale or missing async write. Writes to `.webpieces/logs/L2-decisions/<writer>.log` —
|
|
21
|
+
// the LAYER is the directory, the WRITER is the file (see log-streams.ts and LogStream).
|
|
26
22
|
const MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)
|
|
27
23
|
const MAX_TARGET_LEN = 160;
|
|
24
|
+
/**
|
|
25
|
+
* WHICH ROW of WHICH layer's decision table produced this line. Data-only → a class, per CLAUDE.md.
|
|
26
|
+
*
|
|
27
|
+
* `row` is the row NUMBER from the layer's row array (`L1_ROWS[i].num`, `LMINUS1_ROWS[i].num`) — the
|
|
28
|
+
* same number the generated doc prints, because the doc is rendered from that same array. So a log
|
|
29
|
+
* line joins to its matrix row BY NUMBER, and checking observed behaviour against the documented use
|
|
30
|
+
* cases becomes a lookup rather than an investigation. `'-'` for a layer with no row array yet (L2).
|
|
31
|
+
*/
|
|
32
|
+
class MatrixRef {
|
|
33
|
+
layer;
|
|
34
|
+
row;
|
|
35
|
+
constructor(layer, row) {
|
|
36
|
+
this.layer = layer;
|
|
37
|
+
this.row = row;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.MatrixRef = MatrixRef;
|
|
41
|
+
/**
|
|
42
|
+
* The layer tokens. `row` is `'-'` for a layer with no row array YET (L2 is the un-converted one, and
|
|
43
|
+
* L0's faults are a table of letters rather than numbered rows) — but the LAYER is always named, so
|
|
44
|
+
* `grep layer=L2` works today and the row fills in when L2 converts.
|
|
45
|
+
*
|
|
46
|
+
* These are REQUIRED at the constructor, not defaulted: a defaulted `MatrixRef` would make the
|
|
47
|
+
* uncited case reachable by doing nothing and impossible to grep — the same defect this file's own
|
|
48
|
+
* docblock argues against for `'BLOCK'`, where silence was the one wrong answer available.
|
|
49
|
+
*/
|
|
50
|
+
exports.MATRIX_L0 = new MatrixRef('L0', '-');
|
|
51
|
+
exports.MATRIX_L2 = new MatrixRef('L2', '-');
|
|
28
52
|
// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).
|
|
29
53
|
// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard
|
|
30
54
|
// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted
|
|
@@ -44,8 +68,10 @@ class GuardDecision {
|
|
|
44
68
|
* even while an S storm was blocking every call.
|
|
45
69
|
*/
|
|
46
70
|
fault;
|
|
71
|
+
/** Which layer + row decided this. See MatrixRef — it is what joins a log line to the doc. */
|
|
72
|
+
matrix;
|
|
47
73
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
48
|
-
constructor(rule, tool, target, branch, verdict, reason, cache = '-', fault
|
|
74
|
+
constructor(rule, tool, target, branch, verdict, reason, cache = '-', fault, matrix) {
|
|
49
75
|
this.rule = rule;
|
|
50
76
|
this.tool = tool;
|
|
51
77
|
this.target = target;
|
|
@@ -54,13 +80,14 @@ class GuardDecision {
|
|
|
54
80
|
this.reason = reason;
|
|
55
81
|
this.cache = cache;
|
|
56
82
|
this.fault = fault;
|
|
83
|
+
this.matrix = matrix;
|
|
57
84
|
}
|
|
58
85
|
}
|
|
59
86
|
exports.GuardDecision = GuardDecision;
|
|
60
87
|
/**
|
|
61
|
-
* Append one tab-separated line per decision to `.webpieces/logs
|
|
62
|
-
*
|
|
63
|
-
*
|
|
88
|
+
* Append one tab-separated line per L2 decision to `.webpieces/logs/L2-decisions/<writer>.log`, where
|
|
89
|
+
* <writer> is LogStream's `<sessionId>-<agentId|coordinator>-<hook>` key (a caller that never
|
|
90
|
+
* identified renders as `unknown-coordinator-hook` — there is no un-keyed name).
|
|
64
91
|
* `root` is
|
|
65
92
|
* the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a
|
|
66
93
|
* RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass
|
|
@@ -69,15 +96,39 @@ exports.GuardDecision = GuardDecision;
|
|
|
69
96
|
*/
|
|
70
97
|
// webpieces-disable no-function-outside-class -- the module-scope writer this log has always been, beside branchForLog/oneLine/rotateLogFile; it must stay callable from a tree too broken to build a DI container
|
|
71
98
|
function logGuardDecision(root, decision) {
|
|
99
|
+
appendDecision(root, log_streams_1.L2_DECISIONS_STREAM, decision);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The L1 stream — `.webpieces/logs/L1-location/<writer>.log`.
|
|
103
|
+
*
|
|
104
|
+
* L1 had NO stream. Its three blocking paths wrote into L2's file under an implementation name
|
|
105
|
+
* (`force-to-root`, `coordinator-in-worktree`, `cd-must-be-first`), and its NON-blocking outcomes —
|
|
106
|
+
* the exempt row and the three hand-down rows — wrote nothing at all. So "L1 had no objection" was
|
|
107
|
+
* unobservable, and "show me every L1 decision" had no answer: L1 existed in the trail only as the
|
|
108
|
+
* `root=` / `projectDir=` / `tree=` columns stapled onto somebody else's line.
|
|
109
|
+
*
|
|
110
|
+
* A SIBLING rather than a `base` parameter on logGuardDecision, deliberately: that signature is what
|
|
111
|
+
* the process-wide `logStream` singleton exists to keep unchanged (see LogStream's docblock), and
|
|
112
|
+
* `INVOCATION_LOG_FILE` already establishes the pattern of a second stream owning its own name in
|
|
113
|
+
* this same module.
|
|
114
|
+
*/
|
|
115
|
+
// webpieces-disable no-function-outside-class -- sibling of logGuardDecision, same module-scope writer shape and same reason
|
|
116
|
+
function logL1Decision(root, decision) {
|
|
117
|
+
appendDecision(root, log_streams_1.L1_LOCATION_STREAM, decision);
|
|
118
|
+
}
|
|
119
|
+
// The one appender both streams share. `streamDir` is the LAYER; the writer key inside it is
|
|
120
|
+
// logStream's session/agent/hook, which is what keeps one writer per file.
|
|
121
|
+
// webpieces-disable no-function-outside-class -- the shared body of the two module-scope writers above
|
|
122
|
+
function appendDecision(root, streamDir, decision) {
|
|
72
123
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
73
124
|
try {
|
|
74
125
|
const timestamp = new Date().toISOString();
|
|
75
126
|
// LOCAL scope: a guard decision belongs to the tree it judged. WHO made the call is answered
|
|
76
|
-
// by the filename, which logStream
|
|
77
|
-
const logsDir = rules_config_1.dotWebpieces.
|
|
127
|
+
// by the filename, which logStream names with session/agent/hook.
|
|
128
|
+
const logsDir = rules_config_1.dotWebpieces.logsFile(root, streamDir);
|
|
78
129
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
79
|
-
const logPath = path.join(logsDir, log_stream_1.logStream.
|
|
80
|
-
rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.
|
|
130
|
+
const logPath = path.join(logsDir, log_stream_1.logStream.writerFile('.log'));
|
|
131
|
+
rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.writerFile('.1.log')));
|
|
81
132
|
const line = [
|
|
82
133
|
`[${timestamp}]`,
|
|
83
134
|
decision.verdict,
|
|
@@ -87,6 +138,11 @@ function logGuardDecision(root, decision) {
|
|
|
87
138
|
decision.rule,
|
|
88
139
|
oneLine(decision.reason),
|
|
89
140
|
oneLine(decision.cache),
|
|
141
|
+
// WHICH ROW of WHICH table decided this. The directory already carries the layer, but a
|
|
142
|
+
// line quoted out of its file must still say what judged it — and `row=` is the join key
|
|
143
|
+
// to the generated doc, which is the point of the whole exercise.
|
|
144
|
+
`layer=${decision.matrix.layer}`,
|
|
145
|
+
`row=${decision.matrix.row}`,
|
|
90
146
|
// The tree this decision was actually made against, and what Claude Code told the hook the
|
|
91
147
|
// project was. Appended (never reordered) for the same reason as on the invocation line —
|
|
92
148
|
// see ClaudeEnv: when these two disagree, that disagreement is the bug.
|
|
@@ -132,16 +188,16 @@ class GuardInvocation {
|
|
|
132
188
|
}
|
|
133
189
|
exports.GuardInvocation = GuardInvocation;
|
|
134
190
|
/**
|
|
135
|
-
* The per-INVOCATION stream — `.webpieces/logs/<
|
|
136
|
-
*
|
|
137
|
-
* call (allow or block, bash or file), unlike
|
|
191
|
+
* The per-INVOCATION stream — `.webpieces/logs/calls/<writer>.log` (see LogStream for the writer
|
|
192
|
+
* key), one line for EVERY guards-hook
|
|
193
|
+
* call (allow or block, bash or file), unlike `L2-decisions/` which records only the calls a
|
|
138
194
|
* rule actually judged. It captures the tool, the command/file, the live git branch, the async-written
|
|
139
195
|
* main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class
|
|
140
196
|
* replaced a bare log-and-forget function — HOW THE CALL ENDED.
|
|
141
197
|
*
|
|
142
198
|
* WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry
|
|
143
199
|
* a verdict: the decision had not been made yet. Answering "what happened to this call?" therefore
|
|
144
|
-
* meant joining this file against
|
|
200
|
+
* meant joining this file against the L2 decision stream BY TIMESTAMP, which is exactly the kind of
|
|
145
201
|
* reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the
|
|
146
202
|
* git/cache reads, which must still happen while the hook is running), and {@link finish} — called
|
|
147
203
|
* from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the
|
|
@@ -178,7 +234,7 @@ class InvocationLog {
|
|
|
178
234
|
*
|
|
179
235
|
* `rule` is the rule that blocked, or '-' when there is none; `fault` is the L0 fault code when this
|
|
180
236
|
* call ended on one (S/C/Y — the JS-side faults), else '-'. FIELD ORDER IS APPEND-ONLY: the five
|
|
181
|
-
* original fields keep their positions (cleanup automation mines this file), and `
|
|
237
|
+
* original fields keep their positions (cleanup automation mines this file), and `guards=` /
|
|
182
238
|
* `rule=` / … / `fault=` are added at the end.
|
|
183
239
|
*/
|
|
184
240
|
finish(verdict, rule, fault = l0_fault_codes_1.L0_FAULT_NONE) {
|
|
@@ -188,17 +244,24 @@ class InvocationLog {
|
|
|
188
244
|
return;
|
|
189
245
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
190
246
|
try {
|
|
191
|
-
const logsDir = rules_config_1.dotWebpieces.
|
|
247
|
+
const logsDir = rules_config_1.dotWebpieces.logsFile(invocation.root, log_streams_1.CALLS_STREAM);
|
|
192
248
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
193
|
-
const logPath = path.join(logsDir, log_stream_1.logStream.
|
|
194
|
-
rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.
|
|
249
|
+
const logPath = path.join(logsDir, log_stream_1.logStream.writerFile('.log'));
|
|
250
|
+
rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.writerFile('.1.log')));
|
|
195
251
|
const line = [
|
|
196
252
|
`[${invocation.timestamp}]`,
|
|
197
253
|
invocation.tool,
|
|
198
254
|
invocation.target,
|
|
199
255
|
`branch=${invocation.branch}`,
|
|
200
256
|
invocation.sync,
|
|
201
|
-
`verdict
|
|
257
|
+
// `guards=`, NOT `verdict=`. This hook can only report on ITSELF. Claude Code runs all
|
|
258
|
+
// three PreToolUse hooks IN PARALLEL, so the L-1 `guarantee-root.sh` process may deny a
|
|
259
|
+
// call this one had no objection to, and neither can see the other's answer. Measured:
|
|
260
|
+
// `cd <repo>/packages && ls` was DENIED by L-1 and recorded here three times as
|
|
261
|
+
// `verdict=ALLOW`. The old field name promised an outcome it structurally cannot know;
|
|
262
|
+
// the TRUE final action is the JOIN of this stream with `L-1-cd/`, keyed by the
|
|
263
|
+
// identical writer name — which is what docs/tooling-logs.md now states.
|
|
264
|
+
`guards=${verdict}`,
|
|
202
265
|
`rule=${oneLine(rule) || '-'}`,
|
|
203
266
|
// The tree the guard ACTED in, next to what Claude Code said the project was. Both, on
|
|
204
267
|
// every line, because the diagnostic value is entirely in comparing them — see
|
|
@@ -210,7 +273,7 @@ class InvocationLog {
|
|
|
210
273
|
// pointing at the primary is the straddle, without diffing two absolute paths.
|
|
211
274
|
`tree=${rules_config_1.dotWebpieces.worktreeName(invocation.root) || 'primary'}`,
|
|
212
275
|
// WHICH L0 fault ended this call, in the same letters and the same field name the L0 sh
|
|
213
|
-
// shim uses (
|
|
276
|
+
// shim uses (the `L0-shim/` stream) — so ONE grep spans the whole trail.
|
|
214
277
|
`fault=${fault}`,
|
|
215
278
|
].join('\t') + '\n';
|
|
216
279
|
fs.appendFileSync(logPath, line);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder, claudeEnv } from '@webpieces/rules-config';\n\nimport { L0_FAULT_NONE } from './l0-fault-codes';\nimport { toError } from './to-error';\nimport { logStream } from './log-stream';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (guard-async-work.log, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/logs/<stream>guard-sync-decisions.log`\n// (see LOGS_STATE_DIR: every webpieces log lives under `logs/`, never beside `hooks/`'s non-log state).\nconst LOG_FILE = 'guard-sync-decisions.log';\nconst LOG_FILE_PREV = 'guard-sync-decisions.1.log';\n// The per-INVOCATION stream (companion to the per-DECISION log above): one line for every guards-hook\n// call, so cleanup automation can mine tool + branch + sync-status + OUTCOME over time. See InvocationLog.\nconst INVOCATION_LOG_FILE = 'guard-invocations.log';\nconst INVOCATION_LOG_FILE_PREV = 'guard-invocations.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\nexport type Verdict = 'ALLOW' | 'BLOCK';\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n /**\n * The L0 fault this decision IS, in the codebook's letter (core/l0-fault-codes.ts), or `-` for an\n * ordinary rule decision. The `sh` shim has always stamped `fault=` on its own stream; the three\n * JS-side faults (S/C/Y) reached this one with no label at all, so `grep fault=S` found nothing\n * even while an S storm was blocking every call.\n */\n fault: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-', fault: string = L0_FAULT_NONE) {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n this.fault = fault;\n }\n}\n\n/**\n * Append one tab-separated line per decision to `.webpieces/logs/<stream>guard-sync-decisions.log`,\n * where <stream> is LogStream's `<sessionId>-<agentId|coordinator>-<hook>-` prefix (empty when the\n * caller never identified renders as `unknown-coordinator-hook-` — there is no un-prefixed name).\n * `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\n// webpieces-disable no-function-outside-class -- the module-scope writer this log has always been, beside branchForLog/oneLine/rotateLogFile; it must stay callable from a tree too broken to build a DI container\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged. WHO made the call is answered\n // by the filename, which logStream prefixes with session/agent/hook.\n const logsDir = dotWebpieces.logs(root);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, logStream.fileName(LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(LOG_FILE_PREV)));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n // The tree this decision was actually made against, and what Claude Code told the hook the\n // project was. Appended (never reordered) for the same reason as on the invocation line —\n // see ClaudeEnv: when these two disagree, that disagreement is the bug.\n `root=${root}`,\n `projectDir=${claudeEnv.projectDirForLog()}`,\n // git's name for that tree — `primary`, else the worktree name. Same literal and same\n // derivation as the L0 shim log's `tree=` (shim-audit-log.ts), so one grep spans both\n // streams: L0 carries tree without projectDir, L1 now carries both.\n `tree=${dotWebpieces.worktreeName(root) || 'primary'}`,\n // APPEND-ONLY, same spelling as the invocation line and the L0 shim log: which L0 fault\n // this was, or `-`.\n `fault=${decision.fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * What the guard SAW on one invocation, captured up front and held until the outcome is known.\n * Data-only (per CLAUDE.md: classes for data, explicit construction).\n */\nexport class GuardInvocation {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n public readonly root: string,\n public readonly timestamp: string,\n public readonly tool: string,\n public readonly target: string,\n public readonly branch: string,\n public readonly sync: string,\n public readonly projectDir: string,\n ) {}\n}\n\n/**\n * The per-INVOCATION stream — `.webpieces/logs/<stream>guard-invocations.log` (see LogStream for the\n * `<stream>` prefix), one line for EVERY guards-hook\n * call (allow or block, bash or file), unlike guard-sync-decisions.log which records only the calls a\n * rule actually judged. It captures the tool, the command/file, the live git branch, the async-written\n * main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class\n * replaced a bare log-and-forget function — HOW THE CALL ENDED.\n *\n * WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry\n * a verdict: the decision had not been made yet. Answering \"what happened to this call?\" therefore\n * meant joining this file against guard-sync-decisions.log BY TIMESTAMP, which is exactly the kind of\n * reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the\n * git/cache reads, which must still happen while the hook is running), and {@link finish} — called\n * from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the\n * outcome is known. The two streams stay distinct in purpose: this one is \"every call and how it\n * ended\", the decision log remains \"every judgement and why\".\n *\n * Every error is swallowed: logging must never block or fail a hook.\n */\nexport class InvocationLog {\n private pending: GuardInvocation | null = null;\n\n /**\n * Capture the context of one invocation. `cwd` is the AI's working dir; the repo root that owns\n * `.webpieces` is resolved from it. Writes NOTHING — {@link finish} does that.\n */\n begin(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing\n // on. 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n this.pending = new GuardInvocation(root, new Date().toISOString(), tool, oneLine(target), branch, sync, claudeEnv.projectDirForLog());\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Write the captured line, now stamped with the outcome. A no-op when nothing was captured (the\n * 'rules' hook, or a terminal boundary reached before begin()), and it clears the pending entry so\n * a second emit cannot double-log.\n *\n * `rule` is the rule that blocked, or '-' when there is none; `fault` is the L0 fault code when this\n * call ended on one (S/C/Y — the JS-side faults), else '-'. FIELD ORDER IS APPEND-ONLY: the five\n * original fields keep their positions (cleanup automation mines this file), and `verdict=` /\n * `rule=` / … / `fault=` are added at the end.\n */\n finish(verdict: Verdict, rule: string, fault: string = L0_FAULT_NONE): void {\n const invocation = this.pending;\n this.pending = null;\n if (invocation === null) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const logsDir = dotWebpieces.logs(invocation.root);\n fs.mkdirSync(logsDir, { recursive: true });\n const logPath = path.join(logsDir, logStream.fileName(INVOCATION_LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(INVOCATION_LOG_FILE_PREV)));\n\n const line = [\n `[${invocation.timestamp}]`,\n invocation.tool,\n invocation.target,\n `branch=${invocation.branch}`,\n invocation.sync,\n `verdict=${verdict}`,\n `rule=${oneLine(rule) || '-'}`,\n // The tree the guard ACTED in, next to what Claude Code said the project was. Both, on\n // every line, because the diagnostic value is entirely in comparing them — see\n // ClaudeEnv for the open question this field exists to settle empirically.\n `root=${invocation.root}`,\n `projectDir=${invocation.projectDir}`,\n // See logGuardDecision: the short tree label, so `tree=primary` with a matching\n // projectDir reads as healthy at a glance and `tree=<worktree>` beside a projectDir\n // pointing at the primary is the straddle, without diffing two absolute paths.\n `tree=${dotWebpieces.worktreeName(invocation.root) || 'primary'}`,\n // WHICH L0 fault ended this call, in the same letters and the same field name the L0 sh\n // shim uses (ai-hook-shim.log) — so ONE grep spans the whole trail.\n `fault=${fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Process-wide instance: one hook process handles exactly one tool call, so a single pending entry is\n// the whole state there is. Module-scope (rather than DI) because the terminal boundary that flushes\n// it — emitAllow/emitDeny — is itself module-scope protocol code with no container in reach.\nexport const invocationLog = new InvocationLog();\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder, claudeEnv } from '@webpieces/rules-config';\nimport { L1_LOCATION_STREAM, L2_DECISIONS_STREAM, CALLS_STREAM } from './log-streams';\n\nimport { L0_FAULT_NONE } from './l0-fault-codes';\nimport { toError } from './to-error';\nimport { logStream } from './log-stream';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (the `async-refresh/` stream, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/logs/L2-decisions/<writer>.log` —\n// the LAYER is the directory, the WRITER is the file (see log-streams.ts and LogStream).\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\n/**\n * THE ACTION CODEBOOK, as a type. These are the five actions GUARD_MATRIX.md numbers 1-5, and they\n * are the vocabulary EVERY layer reports in — so one grep spans L-1, L0, L1 and L2.\n *\n * The three distinctions this exists to make, none of which `'ALLOW' | 'BLOCK'` could:\n *\n * ALLOW no objection — the call was HANDED DOWN to the next layer. A layer saying ALLOW\n * is NOT saying the call ran: the layer below it, or the PARALLEL L-1 hook, may\n * still deny. This is L1's `ACT_DOWN`.\n * ALLOW_EXEMPT out of scope by construction — allowed, and evaluation STOPS here. L1's\n * `ACT_EXEMPT`.\n * ALLOW_FAIL_OPEN state could not be established, so nothing was judged. Keeping this distinct\n * from ALLOW is the entire point of the type: a fail-open allow and a real allow\n * that look identical make it impossible to tell whether the guards are protecting\n * anything or quietly abstaining. It used to be a `' (fail-open)'` SUBSTRING on the\n * reason field, which is exactly why the abstentions were never countable.\n * BLOCK_AI_CURE blocked, and the printed cure is a command the AI can run itself.\n * BLOCK_HUMAN blocked, and it needs a human decision — or a delegation (spawn a subagent) that\n * the blocked agent cannot perform for itself.\n *\n * Hard cut, per CLAUDE.md: `'BLOCK'` is GONE rather than aliased, so every construction site fails to\n * compile and has to say which kind of block it is. Before, that question had exactly one wrong\n * answer available — silence.\n */\nexport type Verdict = 'ALLOW' | 'ALLOW_EXEMPT' | 'ALLOW_FAIL_OPEN' | 'BLOCK_AI_CURE' | 'BLOCK_HUMAN';\n\n/**\n * WHICH ROW of WHICH layer's decision table produced this line. Data-only → a class, per CLAUDE.md.\n *\n * `row` is the row NUMBER from the layer's row array (`L1_ROWS[i].num`, `LMINUS1_ROWS[i].num`) — the\n * same number the generated doc prints, because the doc is rendered from that same array. So a log\n * line joins to its matrix row BY NUMBER, and checking observed behaviour against the documented use\n * cases becomes a lookup rather than an investigation. `'-'` for a layer with no row array yet (L2).\n */\nexport class MatrixRef {\n constructor(readonly layer: string, readonly row: string) {}\n}\n\n/**\n * The layer tokens. `row` is `'-'` for a layer with no row array YET (L2 is the un-converted one, and\n * L0's faults are a table of letters rather than numbered rows) — but the LAYER is always named, so\n * `grep layer=L2` works today and the row fills in when L2 converts.\n *\n * These are REQUIRED at the constructor, not defaulted: a defaulted `MatrixRef` would make the\n * uncited case reachable by doing nothing and impossible to grep — the same defect this file's own\n * docblock argues against for `'BLOCK'`, where silence was the one wrong answer available.\n */\nexport const MATRIX_L0 = new MatrixRef('L0', '-');\nexport const MATRIX_L2 = new MatrixRef('L2', '-');\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n /**\n * The L0 fault this decision IS, in the codebook's letter (core/l0-fault-codes.ts), or `-` for an\n * ordinary rule decision. The `sh` shim has always stamped `fault=` on its own stream; the three\n * JS-side faults (S/C/Y) reached this one with no label at all, so `grep fault=S` found nothing\n * even while an S storm was blocking every call.\n */\n fault: string;\n /** Which layer + row decided this. See MatrixRef — it is what joins a log line to the doc. */\n matrix: MatrixRef;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-', fault: string, matrix: MatrixRef) {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n this.fault = fault;\n this.matrix = matrix;\n }\n}\n\n/**\n * Append one tab-separated line per L2 decision to `.webpieces/logs/L2-decisions/<writer>.log`, where\n * <writer> is LogStream's `<sessionId>-<agentId|coordinator>-<hook>` key (a caller that never\n * identified renders as `unknown-coordinator-hook` — there is no un-keyed name).\n * `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\n// webpieces-disable no-function-outside-class -- the module-scope writer this log has always been, beside branchForLog/oneLine/rotateLogFile; it must stay callable from a tree too broken to build a DI container\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n appendDecision(root, L2_DECISIONS_STREAM, decision);\n}\n\n/**\n * The L1 stream — `.webpieces/logs/L1-location/<writer>.log`.\n *\n * L1 had NO stream. Its three blocking paths wrote into L2's file under an implementation name\n * (`force-to-root`, `coordinator-in-worktree`, `cd-must-be-first`), and its NON-blocking outcomes —\n * the exempt row and the three hand-down rows — wrote nothing at all. So \"L1 had no objection\" was\n * unobservable, and \"show me every L1 decision\" had no answer: L1 existed in the trail only as the\n * `root=` / `projectDir=` / `tree=` columns stapled onto somebody else's line.\n *\n * A SIBLING rather than a `base` parameter on logGuardDecision, deliberately: that signature is what\n * the process-wide `logStream` singleton exists to keep unchanged (see LogStream's docblock), and\n * `INVOCATION_LOG_FILE` already establishes the pattern of a second stream owning its own name in\n * this same module.\n */\n// webpieces-disable no-function-outside-class -- sibling of logGuardDecision, same module-scope writer shape and same reason\nexport function logL1Decision(root: string, decision: GuardDecision): void {\n appendDecision(root, L1_LOCATION_STREAM, decision);\n}\n\n// The one appender both streams share. `streamDir` is the LAYER; the writer key inside it is\n// logStream's session/agent/hook, which is what keeps one writer per file.\n// webpieces-disable no-function-outside-class -- the shared body of the two module-scope writers above\nfunction appendDecision(root: string, streamDir: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged. WHO made the call is answered\n // by the filename, which logStream names with session/agent/hook.\n const logsDir = dotWebpieces.logsFile(root, streamDir);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n // WHICH ROW of WHICH table decided this. The directory already carries the layer, but a\n // line quoted out of its file must still say what judged it — and `row=` is the join key\n // to the generated doc, which is the point of the whole exercise.\n `layer=${decision.matrix.layer}`,\n `row=${decision.matrix.row}`,\n // The tree this decision was actually made against, and what Claude Code told the hook the\n // project was. Appended (never reordered) for the same reason as on the invocation line —\n // see ClaudeEnv: when these two disagree, that disagreement is the bug.\n `root=${root}`,\n `projectDir=${claudeEnv.projectDirForLog()}`,\n // git's name for that tree — `primary`, else the worktree name. Same literal and same\n // derivation as the L0 shim log's `tree=` (shim-audit-log.ts), so one grep spans both\n // streams: L0 carries tree without projectDir, L1 now carries both.\n `tree=${dotWebpieces.worktreeName(root) || 'primary'}`,\n // APPEND-ONLY, same spelling as the invocation line and the L0 shim log: which L0 fault\n // this was, or `-`.\n `fault=${decision.fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * What the guard SAW on one invocation, captured up front and held until the outcome is known.\n * Data-only (per CLAUDE.md: classes for data, explicit construction).\n */\nexport class GuardInvocation {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n public readonly root: string,\n public readonly timestamp: string,\n public readonly tool: string,\n public readonly target: string,\n public readonly branch: string,\n public readonly sync: string,\n public readonly projectDir: string,\n ) {}\n}\n\n/**\n * The per-INVOCATION stream — `.webpieces/logs/calls/<writer>.log` (see LogStream for the writer\n * key), one line for EVERY guards-hook\n * call (allow or block, bash or file), unlike `L2-decisions/` which records only the calls a\n * rule actually judged. It captures the tool, the command/file, the live git branch, the async-written\n * main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class\n * replaced a bare log-and-forget function — HOW THE CALL ENDED.\n *\n * WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry\n * a verdict: the decision had not been made yet. Answering \"what happened to this call?\" therefore\n * meant joining this file against the L2 decision stream BY TIMESTAMP, which is exactly the kind of\n * reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the\n * git/cache reads, which must still happen while the hook is running), and {@link finish} — called\n * from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the\n * outcome is known. The two streams stay distinct in purpose: this one is \"every call and how it\n * ended\", the decision log remains \"every judgement and why\".\n *\n * Every error is swallowed: logging must never block or fail a hook.\n */\nexport class InvocationLog {\n private pending: GuardInvocation | null = null;\n\n /**\n * Capture the context of one invocation. `cwd` is the AI's working dir; the repo root that owns\n * `.webpieces` is resolved from it. Writes NOTHING — {@link finish} does that.\n */\n begin(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing\n // on. 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n this.pending = new GuardInvocation(root, new Date().toISOString(), tool, oneLine(target), branch, sync, claudeEnv.projectDirForLog());\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Write the captured line, now stamped with the outcome. A no-op when nothing was captured (the\n * 'rules' hook, or a terminal boundary reached before begin()), and it clears the pending entry so\n * a second emit cannot double-log.\n *\n * `rule` is the rule that blocked, or '-' when there is none; `fault` is the L0 fault code when this\n * call ended on one (S/C/Y — the JS-side faults), else '-'. FIELD ORDER IS APPEND-ONLY: the five\n * original fields keep their positions (cleanup automation mines this file), and `guards=` /\n * `rule=` / … / `fault=` are added at the end.\n */\n finish(verdict: Verdict, rule: string, fault: string = L0_FAULT_NONE): void {\n const invocation = this.pending;\n this.pending = null;\n if (invocation === null) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const logsDir = dotWebpieces.logsFile(invocation.root, CALLS_STREAM);\n fs.mkdirSync(logsDir, { recursive: true });\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${invocation.timestamp}]`,\n invocation.tool,\n invocation.target,\n `branch=${invocation.branch}`,\n invocation.sync,\n // `guards=`, NOT `verdict=`. This hook can only report on ITSELF. Claude Code runs all\n // three PreToolUse hooks IN PARALLEL, so the L-1 `guarantee-root.sh` process may deny a\n // call this one had no objection to, and neither can see the other's answer. Measured:\n // `cd <repo>/packages && ls` was DENIED by L-1 and recorded here three times as\n // `verdict=ALLOW`. The old field name promised an outcome it structurally cannot know;\n // the TRUE final action is the JOIN of this stream with `L-1-cd/`, keyed by the\n // identical writer name — which is what docs/tooling-logs.md now states.\n `guards=${verdict}`,\n `rule=${oneLine(rule) || '-'}`,\n // The tree the guard ACTED in, next to what Claude Code said the project was. Both, on\n // every line, because the diagnostic value is entirely in comparing them — see\n // ClaudeEnv for the open question this field exists to settle empirically.\n `root=${invocation.root}`,\n `projectDir=${invocation.projectDir}`,\n // See logGuardDecision: the short tree label, so `tree=primary` with a matching\n // projectDir reads as healthy at a glance and `tree=<worktree>` beside a projectDir\n // pointing at the primary is the straddle, without diffing two absolute paths.\n `tree=${dotWebpieces.worktreeName(invocation.root) || 'primary'}`,\n // WHICH L0 fault ended this call, in the same letters and the same field name the L0 sh\n // shim uses (the `L0-shim/` stream) — so ONE grep spans the whole trail.\n `fault=${fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Process-wide instance: one hook process handles exactly one tool call, so a single pending entry is\n// the whole state there is. Module-scope (rather than DI) because the terminal boundary that flushes\n// it — emitAllow/emitDeny — is itself module-scope protocol code with no container in reach.\nexport const invocationLog = new InvocationLog();\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
@@ -27,7 +27,7 @@ export declare const L0_SH_FAULT_CODES: readonly ["D", "X", "U", "K"];
|
|
|
27
27
|
/**
|
|
28
28
|
* The faults decided INSIDE the guard bin, in JS. These reached the audit trail with no fault label at
|
|
29
29
|
* all until the JS emitters started stamping them: an `S` storm that blocked an agent for ~20 tool
|
|
30
|
-
* calls left two lines in
|
|
30
|
+
* calls left two lines in the `rejections/` stream, both attributed to a downstream rule, and nothing
|
|
31
31
|
* anywhere identifying L0.
|
|
32
32
|
*/
|
|
33
33
|
export declare const L0_JS_FAULT_CODES: readonly ["S", "C", "Y"];
|
|
@@ -52,7 +52,7 @@ exports.L0_SH_FAULT_CODES = [
|
|
|
52
52
|
/**
|
|
53
53
|
* The faults decided INSIDE the guard bin, in JS. These reached the audit trail with no fault label at
|
|
54
54
|
* all until the JS emitters started stamping them: an `S` storm that blocked an agent for ~20 tool
|
|
55
|
-
* calls left two lines in
|
|
55
|
+
* calls left two lines in the `rejections/` stream, both attributed to a downstream rule, and nothing
|
|
56
56
|
* anywhere identifying L0.
|
|
57
57
|
*/
|
|
58
58
|
exports.L0_JS_FAULT_CODES = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l0-fault-codes.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-fault-codes.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,EAAE;AACF,oGAAoG;AACpG,gGAAgG;AAChG,sGAAsG;AACtG,uGAAuG;AACvG,qGAAqG;AACrG,2DAA2D;AAC3D,EAAE;AACF,sGAAsG;AACtG,6FAA6F;AAC7F,uFAAuF;AACvF,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,uGAAuG;AACvG,oGAAoG;AACpG,eAAe;AACf,8EAA8E;;;AAE9E,gGAAgG;AACnF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC,oGAAoG;AACvF,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAExC,iGAAiG;AACpF,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,oFAAoF;AACvE,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,uGAAuG;AAC1F,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,2EAA2E;AAC9D,QAAA,uBAAuB,GAAG,GAAG,CAAC;AAE3C,yFAAyF;AAC5E,QAAA,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;;;GAKG;AACU,QAAA,aAAa,GAAG,GAAG,CAAC;AAEjC;;;GAGG;AACU,QAAA,iBAAiB,GAAG;IAC7B,sBAAc,EAAE,4BAAoB,EAAE,2BAAmB,EAAE,2BAAmB;CACxE,CAAC;AAEX;;;;;GAKG;AACU,QAAA,iBAAiB,GAAG;IAC7B,2BAAmB,EAAE,+BAAuB,EAAE,mCAA2B;CACnE,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// THE L0 FAULT CODEBOOK — one letter per fault, declared HERE and nowhere else.\n//\n// Both halves of L0 stamp `fault=<code>` onto their audit lines: the POSIX `sh` shim writes D/X/U/K\n// (shim-audit-log.ts), and the guard bin writes S/C/Y in JS (decision-log.ts, via runner.ts and\n// hook-core.ts). The whole value of that field is that ONE grep — `grep 'fault=S'` — spans the entire\n// trail, and that the faults actually observed can be diffed against `L0_FAULTS`. Both properties hold\n// only while every emitter spells the letters the SAME way, and a hand-retyped 'S' in one emitter is\n// exactly the drift this module exists to make impossible.\n//\n// It used to be retyped: the shim assigned `WP_FAULT=X` as a literal, SHIM_LOG_FAULTS listed the four\n// sh-side letters again, and L0_FAULTS listed all seven a third time. Three spellings of one\n// vocabulary, held together by a unit test that could only notice AFTER they diverged.\n//\n// This module is a LEAF — no imports at all — on purpose. `l0-matrix.ts` (core) builds `L0_FAULTS`\n// from these constants and `shim-audit-log.ts` / `shim.ts` (bin) render them into the shim; parking the\n// constants in either of those two would make the other one a core↔bin import cycle. It also keeps the\n// shim renderer dependency-light, which it must be: it has to work on a tree too broken to load the\n// rule engine.\n// ---------------------------------------------------------------------------\n\n/** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */\nexport const L0_FAULT_DRIFT = 'D';\n\n/** `X` — the guard bin is missing (fresh clone, new worktree, package removed). Decided in `sh`. */\nexport const L0_FAULT_BIN_MISSING = 'X';\n\n/** `U` — the bin is missing AND nothing declares the package, so an install is a no-op. `sh`. */\nexport const L0_FAULT_UNDECLARED = 'U';\n\n/** `K` — the bin is present but CRASHED (corrupt node_modules). Decided in `sh`. */\nexport const L0_FAULT_BIN_BROKEN = 'K';\n\n/** `S` — the committed `.claude/webpieces/ai-hook.sh` != `renderShim()`. Decided in the bin, in JS. */\nexport const L0_FAULT_SHIM_STALE = 'S';\n\n/** `C` — `webpieces.config.json` is missing. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_MISSING = 'C';\n\n/** `Y` — a loaded rule has no `webpieces.config.json` key. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_OUT_OF_SYNC = 'Y';\n\n/**\n * No fault AT THIS LAYER — the value every audit line carries when nothing fired.\n *\n * Never a claim that nothing was wrong: a `fault=-` line from the `sh` shim only says the sh half found\n * nothing, and the bin it then exec'd may still have blocked on S/C/Y and stamped its own line.\n */\nexport const L0_FAULT_NONE = '-';\n\n/**\n * The faults decided in POSIX `sh`, BEFORE the bin runs — a stale, missing or broken validator cannot\n * be trusted to validate itself. In first-match-wins order.\n */\nexport const L0_SH_FAULT_CODES = [\n L0_FAULT_DRIFT, L0_FAULT_BIN_MISSING, L0_FAULT_UNDECLARED, L0_FAULT_BIN_BROKEN,\n] as const;\n\n/**\n * The faults decided INSIDE the guard bin, in JS. These reached the audit trail with no fault label at\n * all until the JS emitters started stamping them: an `S` storm that blocked an agent for ~20 tool\n * calls left two lines in
|
|
1
|
+
{"version":3,"file":"l0-fault-codes.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-fault-codes.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,EAAE;AACF,oGAAoG;AACpG,gGAAgG;AAChG,sGAAsG;AACtG,uGAAuG;AACvG,qGAAqG;AACrG,2DAA2D;AAC3D,EAAE;AACF,sGAAsG;AACtG,6FAA6F;AAC7F,uFAAuF;AACvF,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,uGAAuG;AACvG,oGAAoG;AACpG,eAAe;AACf,8EAA8E;;;AAE9E,gGAAgG;AACnF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC,oGAAoG;AACvF,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAExC,iGAAiG;AACpF,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,oFAAoF;AACvE,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,uGAAuG;AAC1F,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,2EAA2E;AAC9D,QAAA,uBAAuB,GAAG,GAAG,CAAC;AAE3C,yFAAyF;AAC5E,QAAA,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;;;GAKG;AACU,QAAA,aAAa,GAAG,GAAG,CAAC;AAEjC;;;GAGG;AACU,QAAA,iBAAiB,GAAG;IAC7B,sBAAc,EAAE,4BAAoB,EAAE,2BAAmB,EAAE,2BAAmB;CACxE,CAAC;AAEX;;;;;GAKG;AACU,QAAA,iBAAiB,GAAG;IAC7B,2BAAmB,EAAE,+BAAuB,EAAE,mCAA2B;CACnE,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// THE L0 FAULT CODEBOOK — one letter per fault, declared HERE and nowhere else.\n//\n// Both halves of L0 stamp `fault=<code>` onto their audit lines: the POSIX `sh` shim writes D/X/U/K\n// (shim-audit-log.ts), and the guard bin writes S/C/Y in JS (decision-log.ts, via runner.ts and\n// hook-core.ts). The whole value of that field is that ONE grep — `grep 'fault=S'` — spans the entire\n// trail, and that the faults actually observed can be diffed against `L0_FAULTS`. Both properties hold\n// only while every emitter spells the letters the SAME way, and a hand-retyped 'S' in one emitter is\n// exactly the drift this module exists to make impossible.\n//\n// It used to be retyped: the shim assigned `WP_FAULT=X` as a literal, SHIM_LOG_FAULTS listed the four\n// sh-side letters again, and L0_FAULTS listed all seven a third time. Three spellings of one\n// vocabulary, held together by a unit test that could only notice AFTER they diverged.\n//\n// This module is a LEAF — no imports at all — on purpose. `l0-matrix.ts` (core) builds `L0_FAULTS`\n// from these constants and `shim-audit-log.ts` / `shim.ts` (bin) render them into the shim; parking the\n// constants in either of those two would make the other one a core↔bin import cycle. It also keeps the\n// shim renderer dependency-light, which it must be: it has to work on a tree too broken to load the\n// rule engine.\n// ---------------------------------------------------------------------------\n\n/** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */\nexport const L0_FAULT_DRIFT = 'D';\n\n/** `X` — the guard bin is missing (fresh clone, new worktree, package removed). Decided in `sh`. */\nexport const L0_FAULT_BIN_MISSING = 'X';\n\n/** `U` — the bin is missing AND nothing declares the package, so an install is a no-op. `sh`. */\nexport const L0_FAULT_UNDECLARED = 'U';\n\n/** `K` — the bin is present but CRASHED (corrupt node_modules). Decided in `sh`. */\nexport const L0_FAULT_BIN_BROKEN = 'K';\n\n/** `S` — the committed `.claude/webpieces/ai-hook.sh` != `renderShim()`. Decided in the bin, in JS. */\nexport const L0_FAULT_SHIM_STALE = 'S';\n\n/** `C` — `webpieces.config.json` is missing. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_MISSING = 'C';\n\n/** `Y` — a loaded rule has no `webpieces.config.json` key. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_OUT_OF_SYNC = 'Y';\n\n/**\n * No fault AT THIS LAYER — the value every audit line carries when nothing fired.\n *\n * Never a claim that nothing was wrong: a `fault=-` line from the `sh` shim only says the sh half found\n * nothing, and the bin it then exec'd may still have blocked on S/C/Y and stamped its own line.\n */\nexport const L0_FAULT_NONE = '-';\n\n/**\n * The faults decided in POSIX `sh`, BEFORE the bin runs — a stale, missing or broken validator cannot\n * be trusted to validate itself. In first-match-wins order.\n */\nexport const L0_SH_FAULT_CODES = [\n L0_FAULT_DRIFT, L0_FAULT_BIN_MISSING, L0_FAULT_UNDECLARED, L0_FAULT_BIN_BROKEN,\n] as const;\n\n/**\n * The faults decided INSIDE the guard bin, in JS. These reached the audit trail with no fault label at\n * all until the JS emitters started stamping them: an `S` storm that blocked an agent for ~20 tool\n * calls left two lines in the `rejections/` stream, both attributed to a downstream rule, and nothing\n * anywhere identifying L0.\n */\nexport const L0_JS_FAULT_CODES = [\n L0_FAULT_SHIM_STALE, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n] as const;\n"]}
|
package/src/core/l1-doc.js
CHANGED
|
@@ -172,6 +172,12 @@ function renderTable() {
|
|
|
172
172
|
'',
|
|
173
173
|
'| # | K | A | R | G | P | act | why |',
|
|
174
174
|
'|---|---|---|---|---|---|---|---|',
|
|
175
|
+
// Row 0 is the PRE-STAGE (`misplacedCdBlock`). It decides from command TEXT before a tree is
|
|
176
|
+
// resolved, so it cannot be classified over the five dimensions rows 1-6 share — but it IS an
|
|
177
|
+
// L1 block, and an L1 block the table did not describe is exactly the drift this table exists
|
|
178
|
+
// to prevent. It is numbered 0, not 7, because it does not sit in the first-match scan; and it
|
|
179
|
+
// is PRINTED because `row=0` in the L1 log has to join to something.
|
|
180
|
+
`| ${l1_rows_1.L1_PRESTAGE_ROW} | – | – | – | – | – | 4 block | a \`cd\` that is not leading + literal, judged before any tree is resolved |`,
|
|
175
181
|
...l1_rows_1.L1_ROWS.map(tableRow),
|
|
176
182
|
'',
|
|
177
183
|
'Rows 3 and 5 are the two structural blocks, and they run as ONE step (`l1LocationBlock` in `runner.ts`)',
|
package/src/core/l1-doc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l1-doc.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { L1Row, L1UseCase, L1_ROWS, 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 ...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** | `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"]}
|
package/src/core/l1-rows.d.ts
CHANGED
|
@@ -20,6 +20,19 @@ export type L1ActionKind = 'exempt' | 'down' | 'block';
|
|
|
20
20
|
* so deleting a row from the array removes the block.
|
|
21
21
|
*/
|
|
22
22
|
export type L1BlockId = 'coordinator-in-worktree' | 'force-to-root';
|
|
23
|
+
/**
|
|
24
|
+
* The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a
|
|
25
|
+
* tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use
|
|
26
|
+
* (asking L1_ROWS to classify it would need the very resolution its answer determines).
|
|
27
|
+
*
|
|
28
|
+
* ZERO rather than a seventh row, deliberately. It has to appear in the table — an L1 block the
|
|
29
|
+
* generated doc did not describe is precisely the drift the table exists to prevent, and it was
|
|
30
|
+
* carrying a `KNOWN GAP` comment saying so. But numbering it 7 would assert it sits in the same
|
|
31
|
+
* first-match scan as the others, which is the one thing that is not true about it. Row 0 says
|
|
32
|
+
* "decided before the scan" in the number itself. `renderL1Doc()` PRINTS this row above the six, so
|
|
33
|
+
* `row=0` in the L1 log joins to a line the reader can actually find.
|
|
34
|
+
*/
|
|
35
|
+
export declare const L1_PRESTAGE_ROW = "0";
|
|
23
36
|
/**
|
|
24
37
|
* One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
|
|
25
38
|
*
|
package/src/core/l1-rows.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.L1_UNROWED_USE_CASES = exports.L1_ROWS = exports.L1Row = exports.L1UseCase = exports.L1Cure = exports.ACT_BLOCK = exports.ACT_DOWN = exports.ACT_EXEMPT = exports.L1Action = exports.L1Classification = void 0;
|
|
3
|
+
exports.L1_UNROWED_USE_CASES = exports.L1_ROWS = exports.L1Row = exports.L1UseCase = exports.L1Cure = exports.ACT_BLOCK = exports.ACT_DOWN = exports.ACT_EXEMPT = exports.L1Action = exports.L1Classification = exports.L1_PRESTAGE_ROW = void 0;
|
|
4
4
|
exports.allL1UseCases = allL1UseCases;
|
|
5
5
|
exports.firstMatchingL1Row = firstMatchingL1Row;
|
|
6
|
+
/**
|
|
7
|
+
* The row number for L1's PRE-STAGE — `misplacedCdBlock`, which decides from command TEXT before a
|
|
8
|
+
* tree has been resolved, and therefore cannot be classified over the five dimensions rows 1-6 use
|
|
9
|
+
* (asking L1_ROWS to classify it would need the very resolution its answer determines).
|
|
10
|
+
*
|
|
11
|
+
* ZERO rather than a seventh row, deliberately. It has to appear in the table — an L1 block the
|
|
12
|
+
* generated doc did not describe is precisely the drift the table exists to prevent, and it was
|
|
13
|
+
* carrying a `KNOWN GAP` comment saying so. But numbering it 7 would assert it sits in the same
|
|
14
|
+
* first-match scan as the others, which is the one thing that is not true about it. Row 0 says
|
|
15
|
+
* "decided before the scan" in the number itself. `renderL1Doc()` PRINTS this row above the six, so
|
|
16
|
+
* `row=0` in the L1 log joins to a line the reader can actually find.
|
|
17
|
+
*/
|
|
18
|
+
exports.L1_PRESTAGE_ROW = '0';
|
|
6
19
|
/**
|
|
7
20
|
* One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
|
|
8
21
|
*
|
|
@@ -174,22 +187,22 @@ exports.L1_ROWS = [
|
|
|
174
187
|
]),
|
|
175
188
|
new L1Row(2, 'o', '-', '-', '-', '-', exports.ACT_DOWN, 'see "Not done" below', null, null, []),
|
|
176
189
|
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', [
|
|
177
|
-
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', '
|
|
190
|
+
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)),
|
|
178
191
|
]),
|
|
179
192
|
new L1Row(4, 'pw', '-', '-', 'n', '-', exports.ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [
|
|
180
|
-
new L1UseCase(5, '`ls` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', '
|
|
181
|
-
new L1UseCase(6, '`pnpm test` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', '
|
|
182
|
-
new L1UseCase(10, '`echo "cd sub && git push"` passes', '`pw` / `n` / `root` — row 4', '
|
|
183
|
-
new L1UseCase(13, 'the same command from a **subagent** runs normally', '`w` / `s` — row 3 does not match', '
|
|
184
|
-
new L1UseCase(14, 'the coordinator\'s `cd <worktree> && ls`/`cat`/`grep` still runs', '`w` / `c` / `y` — row 3 does not match', '
|
|
193
|
+
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)),
|
|
194
|
+
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)),
|
|
195
|
+
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)),
|
|
196
|
+
new L1UseCase(13, 'the same command from a **subagent** runs normally', '`w` / `s` — row 3 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)),
|
|
197
|
+
new L1UseCase(14, 'the coordinator\'s `cd <worktree> && ls`/`cat`/`grep` still runs', '`w` / `c` / `y` — row 3 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)),
|
|
185
198
|
]),
|
|
186
199
|
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', [
|
|
187
|
-
new L1UseCase(7, '`git status` from `packages/http/` is blocked', '`pw` / `y` / `sub` — row 5', '
|
|
188
|
-
new L1UseCase(8, '`cd packages/http && git status` **typed from the root** is blocked', '`pw` / `y` / `sub` — row 5', '
|
|
189
|
-
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', '
|
|
200
|
+
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
|
+
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
|
+
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)),
|
|
190
203
|
]),
|
|
191
204
|
new L1Row(6, 'pw', '-', '-', 'y', 'root', exports.ACT_DOWN, '', null, null, [
|
|
192
|
-
new L1UseCase(9, '`cd <root> && git status` passes from anywhere', '`pw` / `y` / `root` — row 6', '
|
|
205
|
+
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)),
|
|
193
206
|
]),
|
|
194
207
|
];
|
|
195
208
|
/**
|