@webpieces/ai-hook-rules 0.4.633 → 0.4.634

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.4.633",
3
+ "version": "0.4.634",
4
4
  "description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/tooling/ai-hook-rules"
26
26
  },
27
27
  "dependencies": {
28
- "@webpieces/rules-config": "0.4.633"
28
+ "@webpieces/rules-config": "0.4.634"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"
@@ -6,22 +6,26 @@
6
6
  */
7
7
  export declare const SHIM_LOG_MAX_BYTES: number;
8
8
  /**
9
- * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.
9
+ * One verdict label the shim can record, WITH what it means. Data-only → a class, per CLAUDE.md.
10
10
  *
11
- * The three ALLOW-* and three DENY-* labels are the ones this log has always used and are kept
12
- * verbatim, so anything already grepping them keeps working. `PASS-BIN-*` is new: it is the healthy
13
- * case the log used to be silent about.
11
+ * The meaning travels with the label because guards/L0-tooling.md renders this table rather than
12
+ * restating it: a bare `string[]` left the meanings in prose, and the prose is what went stale (the
13
+ * hand-written doc documented `DENY-UNDECLARED` for releases while this array did not list it at all).
14
+ */
15
+ export declare class ShimLogVerdict {
16
+ readonly label: string;
17
+ readonly means: string;
18
+ constructor(label: string, means: string);
19
+ }
20
+ /**
21
+ * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.
14
22
  *
15
- * PASS-BIN-ALLOW no sh-side fault; the bin ran and returned 0 → matrix row 1 (no fault → L1)
16
- * PASS-BIN-BLOCK no sh-side fault; the bin ran and returned 2 → matrix row 1; a LATER layer blocked
17
- * ALLOW-READ allowlist entry 1 (any Read) → PASS, terminal here (use case 10)
18
- * ALLOW-CONFIG allowlist entry 2 (webpieces.config.json) → PASS, terminal here
19
- * ALLOW-CURE allowlist entries 3-8 (a cure command) → ALLOW
20
- * DENY fault X, not on the allowlist → BLOCK_AI_CURE
21
- * DENY-STALE fault D, not on the allowlist → BLOCK_AI_CURE
22
- * DENY-BROKEN fault K, not on the allowlist → BLOCK_AI_CURE
23
+ * The ALLOW-* and DENY-* labels are the ones this log has always used and are kept verbatim, so
24
+ * anything already grepping them keeps working. `PASS-BIN-*` is the healthy case the log used to be
25
+ * silent about, and `DENY-UNDECLARED` is fault U's emitted by the shim since U existed, but missing
26
+ * from this array until the generated doc started reading it.
23
27
  */
24
- export declare const SHIM_LOG_VERDICTS: readonly ["PASS-BIN-ALLOW", "PASS-BIN-BLOCK", "ALLOW-READ", "ALLOW-CONFIG", "ALLOW-CURE", "DENY", "DENY-STALE", "DENY-BROKEN"];
28
+ export declare const SHIM_LOG_VERDICTS: readonly ShimLogVerdict[];
25
29
  /**
26
30
  * The sh-side L0 fault codes, IMPORTED from the one codebook (../core/l0-fault-codes) rather than
27
31
  * retyped here — the letters in this file and the letters in `L0_FAULTS` have to be the same letters or
@@ -30,6 +34,48 @@ export declare const SHIM_LOG_VERDICTS: readonly ["PASS-BIN-ALLOW", "PASS-BIN-BL
30
34
  * field, so a `-` here is a statement about this layer only, never a claim that nothing was wrong.
31
35
  */
32
36
  export declare const SHIM_LOG_FAULTS: readonly ["D", "X", "U", "K", "-"];
37
+ /**
38
+ * One FIELD of the audit line: how it reads on disk, the sh expression that produces it, and what it
39
+ * answers. Data-only → a class, per CLAUDE.md.
40
+ */
41
+ export declare class ShimLogField {
42
+ readonly label: string;
43
+ /** The sh word spliced into the printf below — the ONE place this field's value is spelled. */
44
+ readonly shValue: string;
45
+ readonly means: string;
46
+ /**
47
+ * True for a field that is printed only SOMETIMES (`bin=`, and only when it differs from
48
+ * `shim=`). Such a field carries its OWN trailing tab in its sh value and therefore renders
49
+ * with NO separator of its own — `%s%s` glues it to the next field, so an empty value leaves
50
+ * the line one field shorter rather than leaving a stray tab behind.
51
+ */
52
+ readonly optional: boolean;
53
+ constructor(label: string,
54
+ /** The sh word spliced into the printf below — the ONE place this field's value is spelled. */
55
+ shValue: string, means: string,
56
+ /**
57
+ * True for a field that is printed only SOMETIMES (`bin=`, and only when it differs from
58
+ * `shim=`). Such a field carries its OWN trailing tab in its sh value and therefore renders
59
+ * with NO separator of its own — `%s%s` glues it to the next field, so an empty value leaves
60
+ * the line one field shorter rather than leaving a stray tab behind.
61
+ */
62
+ optional?: boolean);
63
+ }
64
+ /**
65
+ * THE LINE, as data. The printf below is BUILT from this array and guards/L0-tooling.md RENDERS it, so
66
+ * a field cannot be added, dropped or reordered without both the shim and the doc changing with it.
67
+ *
68
+ * That is not decoration: `shim=`/`bin=` were inserted mid-line (deliberately breaking positional
69
+ * readers rather than appending where a stale parser keeps working), then `layer=`/`row=` joined them,
70
+ * and the hand-written doc went on describing a 7-field line with no `U` in its fault set the whole time.
71
+ */
72
+ export declare const SHIM_LOG_FIELDS: readonly ShimLogField[];
73
+ /**
74
+ * The writer's `printf`, assembled from SHIM_LOG_FIELDS — one `%s` per field, in the same order, and a
75
+ * tab after every field EXCEPT an optional one (which carries its own). Retyping either half is what
76
+ * let the format and its documentation disagree, so neither half is retyped anywhere.
77
+ */
78
+ export declare const SHIM_LOG_PRINTF: string;
33
79
  /**
34
80
  * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +
35
81
  * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.
@@ -54,13 +100,13 @@ export declare const RESOLVE_LOG_DIR_SH = "wp_resolve_log_dir() {\n _wp_rp=\"$(
54
100
  /**
55
101
  * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.
56
102
  *
57
- * FORMAT (10 fields, tab-separated, append-only; 11 with the optional `bin=`):
58
- * <iso-ts> <bin-name> <tool> tree=<name|primary> layer=L0 row=<1|2|3> shim=<root>
59
- * [bin=<root>] fault=<D|X|U|K|-> <VERDICT> <command>
103
+ * FORMAT: SHIM_LOG_FIELDS, tab-separated, append-only that array IS the format, and SHIM_LOG_PRINTF
104
+ * is built from it, so neither this docblock nor guards/L0-tooling.md can describe a line the shim does
105
+ * not write.
60
106
  *
61
107
  * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:
62
108
  * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise
63
- * unreadable), the second says which of the six documented faults the sh half detected. The verdict
109
+ * unreadable), the second says which of the sh-side faults the shim detected. The verdict
64
110
  * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\t'` still
65
111
  * finds what it always found.
66
112
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WP_LOG_SH = exports.RESOLVE_LOG_DIR_SH = exports.SHIM_LOG_FAULTS = exports.SHIM_LOG_VERDICTS = exports.SHIM_LOG_MAX_BYTES = void 0;
3
+ exports.WP_LOG_SH = exports.RESOLVE_LOG_DIR_SH = exports.SHIM_LOG_PRINTF = exports.SHIM_LOG_FIELDS = exports.ShimLogField = exports.SHIM_LOG_FAULTS = exports.SHIM_LOG_VERDICTS = exports.ShimLogVerdict = exports.SHIM_LOG_MAX_BYTES = void 0;
4
4
  const rules_config_1 = require("@webpieces/rules-config");
5
5
  const log_streams_1 = require("../core/log-streams");
6
6
  const l0_fault_codes_1 = require("../core/l0-fault-codes");
@@ -39,24 +39,39 @@ const l0_fault_codes_1 = require("../core/l0-fault-codes");
39
39
  */
40
40
  exports.SHIM_LOG_MAX_BYTES = 512 * 1024;
41
41
  /**
42
- * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.
42
+ * One verdict label the shim can record, WITH what it means. Data-only → a class, per CLAUDE.md.
43
43
  *
44
- * The three ALLOW-* and three DENY-* labels are the ones this log has always used and are kept
45
- * verbatim, so anything already grepping them keeps working. `PASS-BIN-*` is new: it is the healthy
46
- * case the log used to be silent about.
44
+ * The meaning travels with the label because guards/L0-tooling.md renders this table rather than
45
+ * restating it: a bare `string[]` left the meanings in prose, and the prose is what went stale (the
46
+ * hand-written doc documented `DENY-UNDECLARED` for releases while this array did not list it at all).
47
+ */
48
+ class ShimLogVerdict {
49
+ label;
50
+ means;
51
+ constructor(label, means) {
52
+ this.label = label;
53
+ this.means = means;
54
+ }
55
+ }
56
+ exports.ShimLogVerdict = ShimLogVerdict;
57
+ /**
58
+ * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.
47
59
  *
48
- * PASS-BIN-ALLOW no sh-side fault; the bin ran and returned 0 → matrix row 1 (no fault → L1)
49
- * PASS-BIN-BLOCK no sh-side fault; the bin ran and returned 2 → matrix row 1; a LATER layer blocked
50
- * ALLOW-READ allowlist entry 1 (any Read) → PASS, terminal here (use case 10)
51
- * ALLOW-CONFIG allowlist entry 2 (webpieces.config.json) → PASS, terminal here
52
- * ALLOW-CURE allowlist entries 3-8 (a cure command) → ALLOW
53
- * DENY fault X, not on the allowlist → BLOCK_AI_CURE
54
- * DENY-STALE fault D, not on the allowlist → BLOCK_AI_CURE
55
- * DENY-BROKEN fault K, not on the allowlist → BLOCK_AI_CURE
60
+ * The ALLOW-* and DENY-* labels are the ones this log has always used and are kept verbatim, so
61
+ * anything already grepping them keeps working. `PASS-BIN-*` is the healthy case the log used to be
62
+ * silent about, and `DENY-UNDECLARED` is fault U's emitted by the shim since U existed, but missing
63
+ * from this array until the generated doc started reading it.
56
64
  */
57
65
  exports.SHIM_LOG_VERDICTS = [
58
- 'PASS-BIN-ALLOW', 'PASS-BIN-BLOCK', 'ALLOW-READ', 'ALLOW-CONFIG', 'ALLOW-CURE',
59
- 'DENY', 'DENY-STALE', 'DENY-BROKEN',
66
+ new ShimLogVerdict('PASS-BIN-ALLOW', 'no sh-side fault; the bin ran and exited 0 — matrix row 1, handed down to L1'),
67
+ new ShimLogVerdict('PASS-BIN-BLOCK', 'no sh-side fault; the bin ran and exited 2 — matrix row 1, a LATER layer blocked'),
68
+ new ShimLogVerdict('ALLOW-READ', 'allowlist entry 1 (any Read) — PASS, but terminal here (the bin never ran)'),
69
+ new ShimLogVerdict('ALLOW-CONFIG', 'allowlist entry 2 (a Write/Edit of webpieces.config.json) — PASS, terminal here'),
70
+ new ShimLogVerdict('ALLOW-CURE', 'a Bash entry of the allowlist matched — ALLOW'),
71
+ new ShimLogVerdict('DENY', 'fault X, not on the allowlist — BLOCK_AI_CURE'),
72
+ new ShimLogVerdict('DENY-UNDECLARED', 'fault U, not on the allowlist — BLOCK_AI_CURE'),
73
+ new ShimLogVerdict('DENY-STALE', 'fault D, not on the allowlist — BLOCK_AI_CURE'),
74
+ new ShimLogVerdict('DENY-BROKEN', 'fault K, not on the allowlist — BLOCK_AI_CURE'),
60
75
  ];
61
76
  /**
62
77
  * The sh-side L0 fault codes, IMPORTED from the one codebook (../core/l0-fault-codes) rather than
@@ -66,6 +81,61 @@ exports.SHIM_LOG_VERDICTS = [
66
81
  * field, so a `-` here is a statement about this layer only, never a claim that nothing was wrong.
67
82
  */
68
83
  exports.SHIM_LOG_FAULTS = [...l0_fault_codes_1.L0_SH_FAULT_CODES, l0_fault_codes_1.L0_FAULT_NONE];
84
+ /**
85
+ * One FIELD of the audit line: how it reads on disk, the sh expression that produces it, and what it
86
+ * answers. Data-only → a class, per CLAUDE.md.
87
+ */
88
+ class ShimLogField {
89
+ label;
90
+ shValue;
91
+ means;
92
+ optional;
93
+ // eslint-disable-next-line @typescript-eslint/max-params
94
+ constructor(label,
95
+ /** The sh word spliced into the printf below — the ONE place this field's value is spelled. */
96
+ shValue, means,
97
+ /**
98
+ * True for a field that is printed only SOMETIMES (`bin=`, and only when it differs from
99
+ * `shim=`). Such a field carries its OWN trailing tab in its sh value and therefore renders
100
+ * with NO separator of its own — `%s%s` glues it to the next field, so an empty value leaves
101
+ * the line one field shorter rather than leaving a stray tab behind.
102
+ */
103
+ optional = false) {
104
+ this.label = label;
105
+ this.shValue = shValue;
106
+ this.means = means;
107
+ this.optional = optional;
108
+ }
109
+ }
110
+ exports.ShimLogField = ShimLogField;
111
+ /**
112
+ * THE LINE, as data. The printf below is BUILT from this array and guards/L0-tooling.md RENDERS it, so
113
+ * a field cannot be added, dropped or reordered without both the shim and the doc changing with it.
114
+ *
115
+ * That is not decoration: `shim=`/`bin=` were inserted mid-line (deliberately breaking positional
116
+ * readers rather than appending where a stale parser keeps working), then `layer=`/`row=` joined them,
117
+ * and the hand-written doc went on describing a 7-field line with no `U` in its fault set the whole time.
118
+ */
119
+ exports.SHIM_LOG_FIELDS = [
120
+ new ShimLogField('<iso-ts>', `"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)"`, 'when the shim judged the call, local time with offset'),
121
+ new ShimLogField('<bin-name>', '"$BIN_NAME"', 'WHICH hook ran - wp-ai-guards-hook or wp-ai-rules-hook; Claude Code runs them in parallel'),
122
+ new ShimLogField('<tool>', '"$TOOL"', 'the PreToolUse tool name (Bash, Read, Write, Edit, …)'),
123
+ new ShimLogField('tree=<name|primary>', '"tree=$WP_TREE"', 'git\'s own name for the worktree the CALL was made in, derived from the payload\'s cwd'),
124
+ new ShimLogField(`layer=${l0_fault_codes_1.L0_LAYER}`, `"layer=${l0_fault_codes_1.L0_LAYER}"`, 'the layer that judged it — constant here, and the first half of the join key a deny cites'),
125
+ new ShimLogField(`row=<${l0_fault_codes_1.L0_ROW_HANDED_DOWN}|${l0_fault_codes_1.L0_ROW_ALLOWLISTED}|${l0_fault_codes_1.L0_ROW_BLOCKED}>`, '"row=$_wp_row"', 'WHICH row of the three-row matrix this call took, read off the verdict (hand-down / allowlisted / blocked)'),
126
+ new ShimLogField('shim=<root>', '"shim=$ROOT"', 'WHICH COPY of ai-hook.sh ran, resolved from $0 — against tree= it is the straddle detector'),
127
+ new ShimLogField('bin=<root>', '"$_wp_bin"', 'WHICH TREE supplied the binary — printed ONLY when it differs from shim=, so its presence IS the borrow', true),
128
+ new ShimLogField(`fault=<${exports.SHIM_LOG_FAULTS.join('|')}>`, '"fault=$1"', 'the sh-side L0 fault, or `-`; S/C/Y are the binary\'s and are stamped on ITS streams'),
129
+ new ShimLogField('<VERDICT>', '"$2"', 'one of the verdict labels below — kept adjacent to the command'),
130
+ new ShimLogField('<command>', '"$CMD_LOG"', 'the command PREFIX (the audit spelling; the DECISION reads $CMD, which fails closed on a quote)'),
131
+ ];
132
+ /**
133
+ * The writer's `printf`, assembled from SHIM_LOG_FIELDS — one `%s` per field, in the same order, and a
134
+ * tab after every field EXCEPT an optional one (which carries its own). Retyping either half is what
135
+ * let the format and its documentation disagree, so neither half is retyped anywhere.
136
+ */
137
+ exports.SHIM_LOG_PRINTF = `printf '${exports.SHIM_LOG_FIELDS.map((f, i) => '%s' + (i === exports.SHIM_LOG_FIELDS.length - 1 ? '' : (f.optional ? '' : '\\t'))).join('')}\\n' `
138
+ + `${exports.SHIM_LOG_FIELDS.map((f) => f.shValue).join(' ')} >> "$_wp_f"`;
69
139
  /**
70
140
  * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +
71
141
  * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.
@@ -122,13 +192,13 @@ exports.RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {
122
192
  /**
123
193
  * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.
124
194
  *
125
- * FORMAT (10 fields, tab-separated, append-only; 11 with the optional `bin=`):
126
- * <iso-ts> <bin-name> <tool> tree=<name|primary> layer=L0 row=<1|2|3> shim=<root>
127
- * [bin=<root>] fault=<D|X|U|K|-> <VERDICT> <command>
195
+ * FORMAT: SHIM_LOG_FIELDS, tab-separated, append-only that array IS the format, and SHIM_LOG_PRINTF
196
+ * is built from it, so neither this docblock nor guards/L0-tooling.md can describe a line the shim does
197
+ * not write.
128
198
  *
129
199
  * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:
130
200
  * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise
131
- * unreadable), the second says which of the six documented faults the sh half detected. The verdict
201
+ * unreadable), the second says which of the sh-side faults the shim detected. The verdict
132
202
  * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\t'` still
133
203
  * finds what it always found.
134
204
  *
@@ -198,7 +268,7 @@ wp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label
198
268
  # what distinguishes it from the ~50 constant bytes 'bin=' used to spend above.
199
269
  _wp_row=${l0_fault_codes_1.L0_ROW_HANDED_DOWN}
200
270
  case "$2" in ALLOW*) _wp_row=${l0_fault_codes_1.L0_ROW_ALLOWLISTED} ;; DENY*) _wp_row=${l0_fault_codes_1.L0_ROW_BLOCKED} ;; esac
201
- printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s%s\\t%s\\t%s\\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "tree=$WP_TREE" "layer=L0" "row=$_wp_row" "shim=$ROOT" "$_wp_bin" "fault=$1" "$2" "$CMD_LOG" >> "$_wp_f"
271
+ ${exports.SHIM_LOG_PRINTF}
202
272
  } 2>/dev/null || true
203
273
  }`;
204
274
  //# sourceMappingURL=shim-audit-log.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shim-audit-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-audit-log.ts"],"names":[],"mappings":";;;AAAA,0DAAgG;AAChG,qDAAqD;AAErD,2DAEgC;AAEhC,8EAA8E;AAC9E,mDAAmD;AACnD,8FAA8F;AAC9F,uGAAuG;AACvG,qGAAqG;AACrG,mGAAmG;AACnG,mDAAmD;AACnD,EAAE;AACF,qGAAqG;AACrG,mGAAmG;AACnG,uGAAuG;AACvG,EAAE;AACF,sGAAsG;AACtG,sGAAsG;AACtG,uGAAuG;AACvG,oGAAoG;AACpG,sGAAsG;AACtG,sGAAsG;AACtG,qGAAqG;AACrG,yFAAyF;AACzF,EAAE;AACF,iCAAiC;AACjC,oGAAoG;AACpG,sGAAsG;AACtG,wDAAwD;AACxD,oEAAoE;AACpE,8EAA8E;AAE9E;;;;;GAKG;AACU,QAAA,kBAAkB,GAAG,GAAG,GAAG,IAAI,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACU,QAAA,iBAAiB,GAAG;IAC7B,gBAAgB,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY;IAC9E,MAAM,EAAE,YAAY,EAAE,aAAa;CAC7B,CAAC;AAEX;;;;;;GAMG;AACU,QAAA,eAAe,GAAG,CAAC,GAAG,kCAAiB,EAAE,8BAAa,CAAU,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;GAmBG;AACU,QAAA,kBAAkB,GAAG;;;;;2CAKS,gCAAiB,IAAI,6BAAc;;;;;;;;;;;;;;;;;qCAiBzC,gCAAiB,IAAI,6BAAc;;;;;;;;+BAQzC,gCAAiB,IAAI,iCAAkB,aAAa,6BAAc;;EAE/F,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACU,QAAA,SAAS,GAAG;;;;EAIvB,0BAAkB;;;;;;;;;0BASM,4BAAc;;;;;;;;;;;;;;;;sBAgBlB,MAAM,CAAC,0BAAkB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6BlC,mCAAkB;mCACG,mCAAkB,sBAAsB,+BAAc;;;EAGvF,CAAC","sourcesContent":["import { LOGS_STATE_DIR, WORKTREE_STATE_DIR, WEBPIECES_TMP_DIR } from '@webpieces/rules-config';\nimport { L0_SHIM_STREAM } from '../core/log-streams';\n\nimport {\n L0_FAULT_NONE, L0_SH_FAULT_CODES, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED, L0_ROW_HANDED_DOWN,\n} from '../core/l0-fault-codes';\n\n// ---------------------------------------------------------------------------\n// THE L0 AUDIT LOG, in POSIX sh — the shim half of\n// `.webpieces/**/logs/L0-shim/<session>-<agent|coordinator>-<binName>.log`. The writer key is\n// the sh twin of ai-hook-rules' LogStream.writerFile(): wp-ai-guards-hook and wp-ai-rules-hook are run\n// IN PARALLEL by Claude Code on every file edit, so an unsplit name means two writers, one file, and\n// torn appends above PIPE_BUF. A payload with no session_id renders 'unknown', never a bare name —\n// there is no un-prefixed spelling on either side.\n//\n// Split out of ./shim.ts (which renders the shim body) purely so both stay readable; shim.ts splices\n// these fragments in verbatim and re-exports the constants. Like l0-allowlist.ts, this module must\n// stay dependency-light: the shim it renders has to work on a tree too broken to load the rule engine.\n//\n// ─── What changed, and why it is not just a bigger log ─────────────────────────────────────────────\n// This log used to be a FAULT log wearing an audit log's name. `wp_log` fired only on the fail-closed\n// path (ALLOW-READ / ALLOW-CONFIG / ALLOW-CURE / DENY*), so a HEALTHY call — the overwhelming majority\n// — exec'd the bin and recorded nothing at all. You could therefore never answer \"what did L0 do to\n// this tool call?\", only \"what did L0 do on the calls where L0 was already broken\". Absence of a line\n// meant either \"healthy\" or \"the shim never ran\", and those are the two answers you most need to tell\n// apart. Every path now logs exactly one line, including the pass-through, so the file can be diffed\n// against the documented matrix in guards/L0-tooling.md rather than merely spot-checked.\n//\n// Two more defects went with it:\n// • it wrote to a hardcoded `$ROOT/.webpieces/logs`, so every worktree's lines landed in one flat\n// file (or, worse, in whichever tree happened to hold the shim) instead of the per-tree namespace\n// the L1 binary has used since the state-dir split;\n// • it had NO rotation, on a file now written on EVERY tool call.\n// ---------------------------------------------------------------------------\n\n/**\n * Rotation threshold, in bytes — 512 KB, the SAME number and the same `.1.log` naming as\n * decision-log.ts / rejection-log.ts / main-sync-log.ts. Deliberately identical rather than merely\n * similar: two log families in one directory with two different retention rules is a trap for whoever\n * later tries to reason about how much history they still have.\n */\nexport const SHIM_LOG_MAX_BYTES = 512 * 1024;\n\n/**\n * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.\n *\n * The three ALLOW-* and three DENY-* labels are the ones this log has always used and are kept\n * verbatim, so anything already grepping them keeps working. `PASS-BIN-*` is new: it is the healthy\n * case the log used to be silent about.\n *\n * PASS-BIN-ALLOW no sh-side fault; the bin ran and returned 0 → matrix row 1 (no fault → L1)\n * PASS-BIN-BLOCK no sh-side fault; the bin ran and returned 2 → matrix row 1; a LATER layer blocked\n * ALLOW-READ allowlist entry 1 (any Read) → PASS, terminal here (use case 10)\n * ALLOW-CONFIG allowlist entry 2 (webpieces.config.json) → PASS, terminal here\n * ALLOW-CURE allowlist entries 3-8 (a cure command) → ALLOW\n * DENY fault X, not on the allowlist → BLOCK_AI_CURE\n * DENY-STALE fault D, not on the allowlist → BLOCK_AI_CURE\n * DENY-BROKEN fault K, not on the allowlist → BLOCK_AI_CURE\n */\nexport const SHIM_LOG_VERDICTS = [\n 'PASS-BIN-ALLOW', 'PASS-BIN-BLOCK', 'ALLOW-READ', 'ALLOW-CONFIG', 'ALLOW-CURE',\n 'DENY', 'DENY-STALE', 'DENY-BROKEN',\n] as const;\n\n/**\n * The sh-side L0 fault codes, IMPORTED from the one codebook (../core/l0-fault-codes) rather than\n * retyped here — the letters in this file and the letters in `L0_FAULTS` have to be the same letters or\n * the log cannot be reconciled against the matrix. `-` means \"no sh-side fault\": the shim cannot\n * classify S / C / Y, which the BINARY detects and stamps onto its OWN streams with the same `fault=`\n * field, so a `-` here is a statement about this layer only, never a claim that nothing was wrong.\n */\nexport const SHIM_LOG_FAULTS = [...L0_SH_FAULT_CODES, L0_FAULT_NONE] as const;\n\n/**\n * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +\n * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.\n *\n * sh cannot import TypeScript, so this derivation is duplicated by necessity; the mitigation is\n * `shim-audit-log.spec.ts`, which runs THIS function through a real /bin/sh in real git worktrees and\n * asserts it returns exactly what `dotWebpieces.worktreeName()` returns. If the two ever disagree the\n * lock goes red rather than the logs quietly splitting in half.\n *\n * It asks git the SAME question the TS side asks — `--git-dir` vs `--git-common-dir`, which differ if\n * and only if this is a linked worktree — but in ONE `rev-parse` (it accepts both flags and prints a\n * line each) rather than two, because this runs on the blocking path of every tool call.\n *\n * The tree is derived from the PAYLOAD's `cwd` (Claude Code documents it as the working directory the\n * hook was invoked from), not from `$ROOT`. `$ROOT` is where the shim FILE lives and stays the anchor\n * for what the drift guard MEASURES — this fragment changes only where the log is WRITTEN.\n *\n * Fails soft, exactly like the TS side: when git cannot answer, the log collapses to\n * `<cwd>/.webpieces/logs`, which is the pre-change behaviour.\n */\nexport const RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {\n _wp_rp=\"$(git -C \"$WP_CWD\" rev-parse --git-dir --git-common-dir 2>/dev/null)\"\n _wp_gd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 1p)\"\n _wp_cd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 2p)\"\n if [ -z \"$_wp_gd\" ] || [ -z \"$_wp_cd\" ]; then\n WP_TREE=primary; WP_LOG_DIR=\"$WP_CWD/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n WP_PRIMARY_LOG_DIR=\"$WP_LOG_DIR\"; return 0\n fi\n # git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS\n # twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.\n case \"$_wp_gd\" in /*) : ;; *) _wp_gd=\"$WP_CWD/$_wp_gd\" ;; esac\n case \"$_wp_cd\" in /*) : ;; *) _wp_cd=\"$WP_CWD/$_wp_cd\" ;; esac\n # The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared\n # dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().\n _wp_primary=\"$WP_CWD\"\n case \"$_wp_cd\" in\n */.git) [ -d \"\\${_wp_cd%/*}\" ] && _wp_primary=\"\\${_wp_cd%/*}\" ;;\n esac\n # The PRIMARY clone's log dir, resolved on both branches. A deny that has to tell a human WHERE the\n # audit trail is (the inverse-drift escalation in shim.ts) must be able to name both the tree it is\n # standing in and the primary — a subagent has no reach into the second one, so the deny has to quote\n # that path rather than send anyone to go and look.\n WP_PRIMARY_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n if [ \"$_wp_gd\" = \"$_wp_cd\" ]; then\n WP_TREE=primary\n WP_LOG_DIR=\"$WP_PRIMARY_LOG_DIR\"\n else\n # git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the\n # directory's basename — two worktrees under different parents may share a directory name.\n WP_TREE=\"\\${_wp_gd##*/}\"\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${WORKTREE_STATE_DIR}/$WP_TREE/${LOGS_STATE_DIR}\"\n fi\n}`;\n\n/**\n * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.\n *\n * FORMAT (10 fields, tab-separated, append-only; 11 with the optional `bin=`):\n * <iso-ts> <bin-name> <tool> tree=<name|primary> layer=L0 row=<1|2|3> shim=<root>\n * [bin=<root>] fault=<D|X|U|K|-> <VERDICT> <command>\n *\n * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:\n * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise\n * unreadable), the second says which of the six documented faults the sh half detected. The verdict\n * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\\\t'` still\n * finds what it always found.\n *\n * NEVER breaks or blocks the hook: the whole body is wrapped so a failure of any kind — unwritable\n * directory, read-only filesystem, missing `git` — is swallowed, and nothing is ever written to\n * stdout (stdout is the PreToolUse decision channel; a stray byte there corrupts allow/deny).\n *\n * The log dir is resolved LAZILY on first use so a call that never logs never pays for the git probe.\n */\nexport const WP_LOG_SH = `WP_TREE=\"\"\nWP_LOG_DIR=\"\"\nWP_PRIMARY_LOG_DIR=\"\"\nWP_TAB=\"$(printf '\\\\t')\" # one real tab, so the OPTIONAL bin= field can carry its own separator\n${RESOLVE_LOG_DIR_SH}\nwp_clean() { # one path segment from an UNTRUSTED payload id — twin of LogStream's segment()\n printf '%s' \"$1\" | tr -c 'A-Za-z0-9._-' '_' | sed -e 's/\\\\.\\\\{2,\\\\}/_/g' -e 's/^\\\\.\\\\{1,\\\\}/_/' | cut -c1-64\n}\nwp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label\n {\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n # The LAYER is the directory and the WRITER is the file — same layout the TS writers use, spelled\n # from the same constant so the two halves cannot drift apart.\n _wp_sd=\"$WP_LOG_DIR/${L0_SHIM_STREAM}\"\n mkdir -p \"$_wp_sd\" 2>/dev/null || return 0\n # Same writer key as LogStream.writerFile(): <session>-<agent|coordinator>-<hook>.log. $BIN_NAME\n # IS the hook discriminator here (wp-ai-guards-hook vs wp-ai-rules-hook), and Claude Code runs those\n # two IN PARALLEL on every file edit — without this prefix they append to ONE file and tear above\n # PIPE_BUF. An empty session id renders 'unknown' — this has no bare-name branch, matching\n # LogStream.writerFile(), which has none either.\n # ALWAYS prefixed - a missing session_id renders as 'unknown', never as the shared bare name.\n # Gating this on a non-empty id would drop both parallel hooks back onto one file, which is the\n # torn-append case this exists to remove. Twin of LogStream.writerFile(), which has no bare branch.\n _wp_pfx=\"$(wp_clean \"\\${WP_SID:-unknown}\")-$(wp_clean \"\\${WP_AID:-coordinator}\")-$BIN_NAME\"\n _wp_f=\"$_wp_sd/\\${_wp_pfx}.log\"\n # Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs\n # on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).\n _wp_sz=\"$(wc -c < \"$_wp_f\" 2>/dev/null | tr -d ' ')\"\n case \"$_wp_sz\" in ''|*[!0-9]*) _wp_sz=0 ;; esac\n [ \"$_wp_sz\" -gt ${String(SHIM_LOG_MAX_BYTES)} ] && mv -f \"$_wp_f\" \"$_wp_sd/\\${_wp_pfx}.1.log\" 2>/dev/null\n # shim= and bin= are the two facts this log could not previously answer, and they are the ones that\n # decide whether a tree was governed by its OWN release or a borrowed one:\n # shim= WHICH COPY OF ai-hook.sh RAN — $ROOT, resolved from $0. The file is TRACKED, so every\n # worktree carries the version at ITS commit; settings.json registers it ABSOLUTE, so the copy\n # that runs is the SESSION ROOT's. Logged rather than assumed, on EVERY line: compared against\n # tree= it is the STRADDLE detector (tree=agent-X shim=<repo> = standing in one tree, judged by\n # another), and that pair varies constantly.\n # bin= WHICH TREE SUPPLIED THE BINARY — $BIN_ROOT, the upward walk's answer.\n #\n # bin= IS PRINTED ONLY WHEN IT DIFFERS FROM shim=, so its mere PRESENCE is the diagnostic (\"the binary\n # came from a different tree than the shim\") instead of ~50 bytes repeated on every line. Measured\n # across 549 logged lines: it differed on 39, every one a worktree agent's first few calls before it\n # ran pnpm install — after that they matched for the rest of that agent's life. And since the hooks\n # went ABSOLUTE, shim= is always the MAIN tree, so the two can now only differ when the main tree\n # itself has no node_modules (a fresh clone before install). ~7% of lines then, near 0% going forward.\n # A unit test asserts the field appears if and only if the roots differ, so it cannot quietly become\n # unconditional noise again.\n _wp_bin=\"\"\n [ \"$BIN_ROOT\" != \"$ROOT\" ] && _wp_bin=\"bin=$BIN_ROOT$WP_TAB\"\n # layer= and row= are the JOIN KEYS, and they are here so the join is REAL rather than promised.\n # Every L0 deny now opens '[<guard>] (layer=L0 fault=<code> row=<n>)' and cites \"the same coordinates\n # the audit line carries\" — which was true of the JS half (MATRIX_L0_BLOCK, via decision-log) and\n # FALSE of this one, which carried 'fault=' alone. Fixing the message instead of the line would have\n # left 'grep 'layer=L0 row=3'' finding one half of L0 and silently missing the other four faults.\n #\n # 'row=' is NOT a constant: it is the row of the three-row matrix this call actually took, read off\n # the verdict — hand-down, allowlisted, or blocked — exactly as L1 logs 'row=' from L1_ROWS. That is\n # what distinguishes it from the ~50 constant bytes 'bin=' used to spend above.\n _wp_row=${L0_ROW_HANDED_DOWN}\n case \"$2\" in ALLOW*) _wp_row=${L0_ROW_ALLOWLISTED} ;; DENY*) _wp_row=${L0_ROW_BLOCKED} ;; esac\n printf '%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s\\\\t%s%s\\\\t%s\\\\t%s\\\\n' \"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)\" \"$BIN_NAME\" \"$TOOL\" \"tree=$WP_TREE\" \"layer=L0\" \"row=$_wp_row\" \"shim=$ROOT\" \"$_wp_bin\" \"fault=$1\" \"$2\" \"$CMD_LOG\" >> \"$_wp_f\"\n } 2>/dev/null || true\n}`;\n"]}
1
+ {"version":3,"file":"shim-audit-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-audit-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { LOGS_STATE_DIR, WORKTREE_STATE_DIR, WEBPIECES_TMP_DIR } from '@webpieces/rules-config';\nimport { L0_SHIM_STREAM } from '../core/log-streams';\n\nimport {\n L0_FAULT_NONE, L0_LAYER, L0_SH_FAULT_CODES, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED, L0_ROW_HANDED_DOWN,\n} from '../core/l0-fault-codes';\n\n// ---------------------------------------------------------------------------\n// THE L0 AUDIT LOG, in POSIX sh — the shim half of\n// `.webpieces/**/logs/L0-shim/<session>-<agent|coordinator>-<binName>.log`. The writer key is\n// the sh twin of ai-hook-rules' LogStream.writerFile(): wp-ai-guards-hook and wp-ai-rules-hook are run\n// IN PARALLEL by Claude Code on every file edit, so an unsplit name means two writers, one file, and\n// torn appends above PIPE_BUF. A payload with no session_id renders 'unknown', never a bare name —\n// there is no un-prefixed spelling on either side.\n//\n// Split out of ./shim.ts (which renders the shim body) purely so both stay readable; shim.ts splices\n// these fragments in verbatim and re-exports the constants. Like l0-allowlist.ts, this module must\n// stay dependency-light: the shim it renders has to work on a tree too broken to load the rule engine.\n//\n// ─── What changed, and why it is not just a bigger log ─────────────────────────────────────────────\n// This log used to be a FAULT log wearing an audit log's name. `wp_log` fired only on the fail-closed\n// path (ALLOW-READ / ALLOW-CONFIG / ALLOW-CURE / DENY*), so a HEALTHY call — the overwhelming majority\n// — exec'd the bin and recorded nothing at all. You could therefore never answer \"what did L0 do to\n// this tool call?\", only \"what did L0 do on the calls where L0 was already broken\". Absence of a line\n// meant either \"healthy\" or \"the shim never ran\", and those are the two answers you most need to tell\n// apart. Every path now logs exactly one line, including the pass-through, so the file can be diffed\n// against the documented matrix in guards/L0-tooling.md rather than merely spot-checked.\n//\n// Two more defects went with it:\n// • it wrote to a hardcoded `$ROOT/.webpieces/logs`, so every worktree's lines landed in one flat\n// file (or, worse, in whichever tree happened to hold the shim) instead of the per-tree namespace\n// the L1 binary has used since the state-dir split;\n// • it had NO rotation, on a file now written on EVERY tool call.\n// ---------------------------------------------------------------------------\n\n/**\n * Rotation threshold, in bytes — 512 KB, the SAME number and the same `.1.log` naming as\n * decision-log.ts / rejection-log.ts / main-sync-log.ts. Deliberately identical rather than merely\n * similar: two log families in one directory with two different retention rules is a trap for whoever\n * later tries to reason about how much history they still have.\n */\nexport const SHIM_LOG_MAX_BYTES = 512 * 1024;\n\n/**\n * One verdict label the shim can record, WITH what it means. Data-only → a class, per CLAUDE.md.\n *\n * The meaning travels with the label because guards/L0-tooling.md renders this table rather than\n * restating it: a bare `string[]` left the meanings in prose, and the prose is what went stale (the\n * hand-written doc documented `DENY-UNDECLARED` for releases while this array did not list it at all).\n */\nexport class ShimLogVerdict {\n constructor(\n readonly label: string,\n readonly means: string,\n ) {}\n}\n\n/**\n * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.\n *\n * The ALLOW-* and DENY-* labels are the ones this log has always used and are kept verbatim, so\n * anything already grepping them keeps working. `PASS-BIN-*` is the healthy case the log used to be\n * silent about, and `DENY-UNDECLARED` is fault U's — emitted by the shim since U existed, but missing\n * from this array until the generated doc started reading it.\n */\nexport const SHIM_LOG_VERDICTS: readonly ShimLogVerdict[] = [\n new ShimLogVerdict('PASS-BIN-ALLOW', 'no sh-side fault; the bin ran and exited 0 — matrix row 1, handed down to L1'),\n new ShimLogVerdict('PASS-BIN-BLOCK', 'no sh-side fault; the bin ran and exited 2 — matrix row 1, a LATER layer blocked'),\n new ShimLogVerdict('ALLOW-READ', 'allowlist entry 1 (any Read) — PASS, but terminal here (the bin never ran)'),\n new ShimLogVerdict('ALLOW-CONFIG', 'allowlist entry 2 (a Write/Edit of webpieces.config.json) — PASS, terminal here'),\n new ShimLogVerdict('ALLOW-CURE', 'a Bash entry of the allowlist matched — ALLOW'),\n new ShimLogVerdict('DENY', 'fault X, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-UNDECLARED', 'fault U, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-STALE', 'fault D, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-BROKEN', 'fault K, not on the allowlist — BLOCK_AI_CURE'),\n];\n\n/**\n * The sh-side L0 fault codes, IMPORTED from the one codebook (../core/l0-fault-codes) rather than\n * retyped here — the letters in this file and the letters in `L0_FAULTS` have to be the same letters or\n * the log cannot be reconciled against the matrix. `-` means \"no sh-side fault\": the shim cannot\n * classify S / C / Y, which the BINARY detects and stamps onto its OWN streams with the same `fault=`\n * field, so a `-` here is a statement about this layer only, never a claim that nothing was wrong.\n */\nexport const SHIM_LOG_FAULTS = [...L0_SH_FAULT_CODES, L0_FAULT_NONE] as const;\n\n/**\n * One FIELD of the audit line: how it reads on disk, the sh expression that produces it, and what it\n * answers. Data-only → a class, per CLAUDE.md.\n */\nexport class ShimLogField {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly label: string,\n /** The sh word spliced into the printf below — the ONE place this field's value is spelled. */\n readonly shValue: string,\n readonly means: string,\n /**\n * True for a field that is printed only SOMETIMES (`bin=`, and only when it differs from\n * `shim=`). Such a field carries its OWN trailing tab in its sh value and therefore renders\n * with NO separator of its own — `%s%s` glues it to the next field, so an empty value leaves\n * the line one field shorter rather than leaving a stray tab behind.\n */\n readonly optional: boolean = false,\n ) {}\n}\n\n/**\n * THE LINE, as data. The printf below is BUILT from this array and guards/L0-tooling.md RENDERS it, so\n * a field cannot be added, dropped or reordered without both the shim and the doc changing with it.\n *\n * That is not decoration: `shim=`/`bin=` were inserted mid-line (deliberately breaking positional\n * readers rather than appending where a stale parser keeps working), then `layer=`/`row=` joined them,\n * and the hand-written doc went on describing a 7-field line with no `U` in its fault set the whole time.\n */\nexport const SHIM_LOG_FIELDS: readonly ShimLogField[] = [\n new ShimLogField('<iso-ts>', `\"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)\"`,\n 'when the shim judged the call, local time with offset'),\n new ShimLogField('<bin-name>', '\"$BIN_NAME\"',\n 'WHICH hook ran - wp-ai-guards-hook or wp-ai-rules-hook; Claude Code runs them in parallel'),\n new ShimLogField('<tool>', '\"$TOOL\"', 'the PreToolUse tool name (Bash, Read, Write, Edit, …)'),\n new ShimLogField('tree=<name|primary>', '\"tree=$WP_TREE\"',\n 'git\\'s own name for the worktree the CALL was made in, derived from the payload\\'s cwd'),\n new ShimLogField(`layer=${L0_LAYER}`, `\"layer=${L0_LAYER}\"`,\n 'the layer that judged it — constant here, and the first half of the join key a deny cites'),\n new ShimLogField(`row=<${L0_ROW_HANDED_DOWN}|${L0_ROW_ALLOWLISTED}|${L0_ROW_BLOCKED}>`, '\"row=$_wp_row\"',\n 'WHICH row of the three-row matrix this call took, read off the verdict (hand-down / allowlisted / blocked)'),\n new ShimLogField('shim=<root>', '\"shim=$ROOT\"',\n 'WHICH COPY of ai-hook.sh ran, resolved from $0 — against tree= it is the straddle detector'),\n new ShimLogField('bin=<root>', '\"$_wp_bin\"',\n 'WHICH TREE supplied the binary — printed ONLY when it differs from shim=, so its presence IS the borrow',\n true),\n new ShimLogField(`fault=<${SHIM_LOG_FAULTS.join('|')}>`, '\"fault=$1\"',\n 'the sh-side L0 fault, or `-`; S/C/Y are the binary\\'s and are stamped on ITS streams'),\n new ShimLogField('<VERDICT>', '\"$2\"', 'one of the verdict labels below — kept adjacent to the command'),\n new ShimLogField('<command>', '\"$CMD_LOG\"',\n 'the command PREFIX (the audit spelling; the DECISION reads $CMD, which fails closed on a quote)'),\n];\n\n/**\n * The writer's `printf`, assembled from SHIM_LOG_FIELDS — one `%s` per field, in the same order, and a\n * tab after every field EXCEPT an optional one (which carries its own). Retyping either half is what\n * let the format and its documentation disagree, so neither half is retyped anywhere.\n */\nexport const SHIM_LOG_PRINTF =\n `printf '${SHIM_LOG_FIELDS.map(\n (f: ShimLogField, i: number): string => '%s' + (i === SHIM_LOG_FIELDS.length - 1 ? '' : (f.optional ? '' : '\\\\t')),\n ).join('')}\\\\n' `\n + `${SHIM_LOG_FIELDS.map((f: ShimLogField): string => f.shValue).join(' ')} >> \"$_wp_f\"`;\n\n/**\n * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +\n * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.\n *\n * sh cannot import TypeScript, so this derivation is duplicated by necessity; the mitigation is\n * `shim-audit-log.spec.ts`, which runs THIS function through a real /bin/sh in real git worktrees and\n * asserts it returns exactly what `dotWebpieces.worktreeName()` returns. If the two ever disagree the\n * lock goes red rather than the logs quietly splitting in half.\n *\n * It asks git the SAME question the TS side asks — `--git-dir` vs `--git-common-dir`, which differ if\n * and only if this is a linked worktree — but in ONE `rev-parse` (it accepts both flags and prints a\n * line each) rather than two, because this runs on the blocking path of every tool call.\n *\n * The tree is derived from the PAYLOAD's `cwd` (Claude Code documents it as the working directory the\n * hook was invoked from), not from `$ROOT`. `$ROOT` is where the shim FILE lives and stays the anchor\n * for what the drift guard MEASURES — this fragment changes only where the log is WRITTEN.\n *\n * Fails soft, exactly like the TS side: when git cannot answer, the log collapses to\n * `<cwd>/.webpieces/logs`, which is the pre-change behaviour.\n */\nexport const RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {\n _wp_rp=\"$(git -C \"$WP_CWD\" rev-parse --git-dir --git-common-dir 2>/dev/null)\"\n _wp_gd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 1p)\"\n _wp_cd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 2p)\"\n if [ -z \"$_wp_gd\" ] || [ -z \"$_wp_cd\" ]; then\n WP_TREE=primary; WP_LOG_DIR=\"$WP_CWD/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n WP_PRIMARY_LOG_DIR=\"$WP_LOG_DIR\"; return 0\n fi\n # git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS\n # twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.\n case \"$_wp_gd\" in /*) : ;; *) _wp_gd=\"$WP_CWD/$_wp_gd\" ;; esac\n case \"$_wp_cd\" in /*) : ;; *) _wp_cd=\"$WP_CWD/$_wp_cd\" ;; esac\n # The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared\n # dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().\n _wp_primary=\"$WP_CWD\"\n case \"$_wp_cd\" in\n */.git) [ -d \"\\${_wp_cd%/*}\" ] && _wp_primary=\"\\${_wp_cd%/*}\" ;;\n esac\n # The PRIMARY clone's log dir, resolved on both branches. A deny that has to tell a human WHERE the\n # audit trail is (the inverse-drift escalation in shim.ts) must be able to name both the tree it is\n # standing in and the primary — a subagent has no reach into the second one, so the deny has to quote\n # that path rather than send anyone to go and look.\n WP_PRIMARY_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n if [ \"$_wp_gd\" = \"$_wp_cd\" ]; then\n WP_TREE=primary\n WP_LOG_DIR=\"$WP_PRIMARY_LOG_DIR\"\n else\n # git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the\n # directory's basename — two worktrees under different parents may share a directory name.\n WP_TREE=\"\\${_wp_gd##*/}\"\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${WORKTREE_STATE_DIR}/$WP_TREE/${LOGS_STATE_DIR}\"\n fi\n}`;\n\n/**\n * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.\n *\n * FORMAT: SHIM_LOG_FIELDS, tab-separated, append-only — that array IS the format, and SHIM_LOG_PRINTF\n * is built from it, so neither this docblock nor guards/L0-tooling.md can describe a line the shim does\n * not write.\n *\n * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:\n * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise\n * unreadable), the second says which of the sh-side faults the shim detected. The verdict\n * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\\\t'` still\n * finds what it always found.\n *\n * NEVER breaks or blocks the hook: the whole body is wrapped so a failure of any kind — unwritable\n * directory, read-only filesystem, missing `git` — is swallowed, and nothing is ever written to\n * stdout (stdout is the PreToolUse decision channel; a stray byte there corrupts allow/deny).\n *\n * The log dir is resolved LAZILY on first use so a call that never logs never pays for the git probe.\n */\nexport const WP_LOG_SH = `WP_TREE=\"\"\nWP_LOG_DIR=\"\"\nWP_PRIMARY_LOG_DIR=\"\"\nWP_TAB=\"$(printf '\\\\t')\" # one real tab, so the OPTIONAL bin= field can carry its own separator\n${RESOLVE_LOG_DIR_SH}\nwp_clean() { # one path segment from an UNTRUSTED payload id — twin of LogStream's segment()\n printf '%s' \"$1\" | tr -c 'A-Za-z0-9._-' '_' | sed -e 's/\\\\.\\\\{2,\\\\}/_/g' -e 's/^\\\\.\\\\{1,\\\\}/_/' | cut -c1-64\n}\nwp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label\n {\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n # The LAYER is the directory and the WRITER is the file — same layout the TS writers use, spelled\n # from the same constant so the two halves cannot drift apart.\n _wp_sd=\"$WP_LOG_DIR/${L0_SHIM_STREAM}\"\n mkdir -p \"$_wp_sd\" 2>/dev/null || return 0\n # Same writer key as LogStream.writerFile(): <session>-<agent|coordinator>-<hook>.log. $BIN_NAME\n # IS the hook discriminator here (wp-ai-guards-hook vs wp-ai-rules-hook), and Claude Code runs those\n # two IN PARALLEL on every file edit — without this prefix they append to ONE file and tear above\n # PIPE_BUF. An empty session id renders 'unknown' — this has no bare-name branch, matching\n # LogStream.writerFile(), which has none either.\n # ALWAYS prefixed - a missing session_id renders as 'unknown', never as the shared bare name.\n # Gating this on a non-empty id would drop both parallel hooks back onto one file, which is the\n # torn-append case this exists to remove. Twin of LogStream.writerFile(), which has no bare branch.\n _wp_pfx=\"$(wp_clean \"\\${WP_SID:-unknown}\")-$(wp_clean \"\\${WP_AID:-coordinator}\")-$BIN_NAME\"\n _wp_f=\"$_wp_sd/\\${_wp_pfx}.log\"\n # Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs\n # on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).\n _wp_sz=\"$(wc -c < \"$_wp_f\" 2>/dev/null | tr -d ' ')\"\n case \"$_wp_sz\" in ''|*[!0-9]*) _wp_sz=0 ;; esac\n [ \"$_wp_sz\" -gt ${String(SHIM_LOG_MAX_BYTES)} ] && mv -f \"$_wp_f\" \"$_wp_sd/\\${_wp_pfx}.1.log\" 2>/dev/null\n # shim= and bin= are the two facts this log could not previously answer, and they are the ones that\n # decide whether a tree was governed by its OWN release or a borrowed one:\n # shim= WHICH COPY OF ai-hook.sh RAN — $ROOT, resolved from $0. The file is TRACKED, so every\n # worktree carries the version at ITS commit; settings.json registers it ABSOLUTE, so the copy\n # that runs is the SESSION ROOT's. Logged rather than assumed, on EVERY line: compared against\n # tree= it is the STRADDLE detector (tree=agent-X shim=<repo> = standing in one tree, judged by\n # another), and that pair varies constantly.\n # bin= WHICH TREE SUPPLIED THE BINARY — $BIN_ROOT, the upward walk's answer.\n #\n # bin= IS PRINTED ONLY WHEN IT DIFFERS FROM shim=, so its mere PRESENCE is the diagnostic (\"the binary\n # came from a different tree than the shim\") instead of ~50 bytes repeated on every line. Measured\n # across 549 logged lines: it differed on 39, every one a worktree agent's first few calls before it\n # ran pnpm install — after that they matched for the rest of that agent's life. And since the hooks\n # went ABSOLUTE, shim= is always the MAIN tree, so the two can now only differ when the main tree\n # itself has no node_modules (a fresh clone before install). ~7% of lines then, near 0% going forward.\n # A unit test asserts the field appears if and only if the roots differ, so it cannot quietly become\n # unconditional noise again.\n _wp_bin=\"\"\n [ \"$BIN_ROOT\" != \"$ROOT\" ] && _wp_bin=\"bin=$BIN_ROOT$WP_TAB\"\n # layer= and row= are the JOIN KEYS, and they are here so the join is REAL rather than promised.\n # Every L0 deny now opens '[<guard>] (layer=L0 fault=<code> row=<n>)' and cites \"the same coordinates\n # the audit line carries\" — which was true of the JS half (MATRIX_L0_BLOCK, via decision-log) and\n # FALSE of this one, which carried 'fault=' alone. Fixing the message instead of the line would have\n # left 'grep 'layer=L0 row=3'' finding one half of L0 and silently missing the other four faults.\n #\n # 'row=' is NOT a constant: it is the row of the three-row matrix this call actually took, read off\n # the verdict — hand-down, allowlisted, or blocked — exactly as L1 logs 'row=' from L1_ROWS. That is\n # what distinguishes it from the ~50 constant bytes 'bin=' used to spend above.\n _wp_row=${L0_ROW_HANDED_DOWN}\n case \"$2\" in ALLOW*) _wp_row=${L0_ROW_ALLOWLISTED} ;; DENY*) _wp_row=${L0_ROW_BLOCKED} ;; esac\n ${SHIM_LOG_PRINTF}\n } 2>/dev/null || true\n}`;\n"]}
@@ -49,6 +49,12 @@ export type L0FaultCode = typeof L0_SH_FAULT_CODES[number] | typeof L0_JS_FAULT_
49
49
  * L1 row would (see L1_ROWS' header).
50
50
  */
51
51
  export declare const L0_FAULT_NAMES: Readonly<Record<L0FaultCode, string>>;
52
+ /**
53
+ * The LAYER token — the other half of every join key, spelled once. The deny header, the matrix
54
+ * citation, the sh audit line and guards/L0-tooling.md all read it from here, so `grep layer=L0` cannot
55
+ * miss an artifact that typed the token itself.
56
+ */
57
+ export declare const L0_LAYER = "L0";
52
58
  /**
53
59
  * The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the
54
60
  * numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine
@@ -20,7 +20,7 @@
20
20
  // rule engine.
21
21
  // ---------------------------------------------------------------------------
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.L0_ROW_BLOCKED = exports.L0_ROW_ALLOWLISTED = exports.L0_ROW_HANDED_DOWN = exports.L0_FAULT_NAMES = exports.L0_JS_FAULT_CODES = exports.L0_SH_FAULT_CODES = exports.L0_FAULT_NONE = exports.L0_FAULT_CONFIG_OUT_OF_SYNC = exports.L0_FAULT_CONFIG_MISSING = exports.L0_FAULT_SHIM_STALE = exports.L0_FAULT_BIN_BROKEN = exports.L0_FAULT_UNDECLARED = exports.L0_FAULT_BIN_MISSING = exports.L0_FAULT_DRIFT = void 0;
23
+ exports.L0_ROW_BLOCKED = exports.L0_ROW_ALLOWLISTED = exports.L0_ROW_HANDED_DOWN = exports.L0_LAYER = exports.L0_FAULT_NAMES = exports.L0_JS_FAULT_CODES = exports.L0_SH_FAULT_CODES = exports.L0_FAULT_NONE = exports.L0_FAULT_CONFIG_OUT_OF_SYNC = exports.L0_FAULT_CONFIG_MISSING = exports.L0_FAULT_SHIM_STALE = exports.L0_FAULT_BIN_BROKEN = exports.L0_FAULT_UNDECLARED = exports.L0_FAULT_BIN_MISSING = exports.L0_FAULT_DRIFT = void 0;
24
24
  exports.l0GuardHeader = l0GuardHeader;
25
25
  exports.l0MatrixCitation = l0MatrixCitation;
26
26
  /** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */
@@ -78,6 +78,12 @@ exports.L0_FAULT_NAMES = {
78
78
  [exports.L0_FAULT_CONFIG_MISSING]: 'config-missing',
79
79
  [exports.L0_FAULT_CONFIG_OUT_OF_SYNC]: 'config-out-of-sync',
80
80
  };
81
+ /**
82
+ * The LAYER token — the other half of every join key, spelled once. The deny header, the matrix
83
+ * citation, the sh audit line and guards/L0-tooling.md all read it from here, so `grep layer=L0` cannot
84
+ * miss an artifact that typed the token itself.
85
+ */
86
+ exports.L0_LAYER = 'L0';
81
87
  /**
82
88
  * The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the
83
89
  * numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine
@@ -102,7 +108,7 @@ exports.L0_ROW_BLOCKED = '3';
102
108
  */
103
109
  // webpieces-disable no-function-outside-class -- pure string builder over this leaf module's own constants; it must stay importable by the dependency-free shim renderer.
104
110
  function l0GuardHeader(fault, detail) {
105
- return `[${exports.L0_FAULT_NAMES[fault]}] (layer=L0 fault=${fault} row=${exports.L0_ROW_BLOCKED}, ${detail})`;
111
+ return `[${exports.L0_FAULT_NAMES[fault]}] (layer=${exports.L0_LAYER} fault=${fault} row=${exports.L0_ROW_BLOCKED}, ${detail})`;
106
112
  }
107
113
  /**
108
114
  * The one-line citation of WHICH matrix row was taken and on what dimension values — L1's pattern
@@ -110,6 +116,6 @@ function l0GuardHeader(fault, detail) {
110
116
  */
111
117
  // webpieces-disable no-function-outside-class -- sibling of l0GuardHeader in this leaf codebook module.
112
118
  function l0MatrixCitation(fault) {
113
- return `matrix row ${exports.L0_ROW_BLOCKED}: fault=${fault} present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=${exports.L0_ROW_BLOCKED} fault=${fault}) and the same row webpieces.guard-matrix.md prints.`;
119
+ return `matrix row ${exports.L0_ROW_BLOCKED}: fault=${fault} present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=${exports.L0_LAYER} row=${exports.L0_ROW_BLOCKED} fault=${fault}) and the same row webpieces.guard-matrix.md prints.`;
114
120
  }
115
121
  //# sourceMappingURL=l0-fault-codes.js.map
@@ -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;;;AAyH9E,sCAEC;AAOD,4CAEC;AAlID,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;AA6BX;;;;;;;;GAQG;AACU,QAAA,cAAc,GAA0C;IACjE,CAAC,sBAAc,CAAC,EAAE,eAAe;IACjC,CAAC,4BAAoB,CAAC,EAAE,mBAAmB;IAC3C,CAAC,2BAAmB,CAAC,EAAE,sBAAsB;IAC7C,CAAC,2BAAmB,CAAC,EAAE,mBAAmB;IAC1C,CAAC,2BAAmB,CAAC,EAAE,sBAAsB;IAC7C,CAAC,+BAAuB,CAAC,EAAE,gBAAgB;IAC3C,CAAC,mCAA2B,CAAC,EAAE,oBAAoB;CACtD,CAAC;AAEF;;;;;;;;GAQG;AACU,QAAA,kBAAkB,GAAG,GAAG,CAAC;AAEtC,0GAA0G;AAC7F,QAAA,kBAAkB,GAAG,GAAG,CAAC;AAEtC,oGAAoG;AACvF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC;;;;;;;GAOG;AACH,0KAA0K;AAC1K,SAAgB,aAAa,CAAC,KAAkB,EAAE,MAAc;IAC5D,OAAO,IAAI,sBAAc,CAAC,KAAK,CAAC,qBAAqB,KAAK,QAAQ,sBAAc,KAAK,MAAM,GAAG,CAAC;AACnG,CAAC;AAED;;;GAGG;AACH,wGAAwG;AACxG,SAAgB,gBAAgB,CAAC,KAAkB;IAC/C,OAAO,cAAc,sBAAc,WAAW,KAAK,iHAAiH,sBAAc,UAAU,KAAK,sDAAsD,CAAC;AAC5P,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\n// ---------------------------------------------------------------------------\n// THE JOIN KEYS — the three artifacts that describe one L0 event, and the coordinates that line them up.\n//\n// There are three, and until now they could not be grepped together:\n// 1. THE DENY the agent reads in the moment (shim-deny-reason.ts for S; DENY_REASON_SH for D/X/U/K)\n// 2. THE AUDIT LINE (`.webpieces/logs/**`), which carries `layer=` `row=` `fault=`\n// 3. THE MATRIX DOC (webpieces.guard-matrix.md, rendered from L0_FAULTS + L0_ALLOWLIST)\n//\n// The deny had NONE of them: no fault letter, no row, and — the highest-value omission — no guard NAME,\n// while every L1/L2 deny opens `[<rule-name>] (N violations)`. So a transcript could not be debugged\n// against the log after the fact, and a reader could not find the matrix row by eye.\n//\n// These constants are that vocabulary, declared HERE for the same reason the letters are: this module is\n// a LEAF with no imports, so `core/l0-matrix.ts` (the doc), `bin/shim*.ts` (the denies) and\n// `core/decision-log.ts` (the log) can all reach it without an import cycle. Retyping a name in any one\n// of them is the drift this file exists to make impossible.\n// ---------------------------------------------------------------------------\n\n/**\n * EVERY L0 fault code, as a type. The two arrays above are the halves; this is their union, and it is\n * what makes `L0_FAULT_NAMES` TOTAL — a `Record<string, …>` would have forced a `?? 'unknown'` fallback\n * at every read, which is shim shape #4 (a runtime default standing in for a type that could have\n * expressed the invariant). With the union, a new fault added without a name is a COMPILE error, and\n * neither reader needs a defensive branch.\n */\nexport type L0FaultCode = typeof L0_SH_FAULT_CODES[number] | typeof L0_JS_FAULT_CODES[number];\n\n/**\n * The stable, human-readable GUARD NAME per fault code — what goes in the deny's `[…]` header, in the\n * matrix doc's own `guard` column, and nowhere else in a second spelling.\n *\n * L1 prints `[stale-main-bash-guard] (1 violation)` and L0 printed nothing comparable; the names below\n * are deliberately in that same kebab shape so the two layers read as one system. They are IDENTITY, not\n * prose: renaming one silently breaks a grep that spans all three artifacts, exactly as renumbering an\n * L1 row would (see L1_ROWS' header).\n */\nexport const L0_FAULT_NAMES: Readonly<Record<L0FaultCode, string>> = {\n [L0_FAULT_DRIFT]: 'version-drift',\n [L0_FAULT_BIN_MISSING]: 'guard-bin-missing',\n [L0_FAULT_UNDECLARED]: 'guard-pkg-undeclared',\n [L0_FAULT_BIN_BROKEN]: 'guard-bin-crashed',\n [L0_FAULT_SHIM_STALE]: 'managed-hook-surface',\n [L0_FAULT_CONFIG_MISSING]: 'config-missing',\n [L0_FAULT_CONFIG_OUT_OF_SYNC]: 'config-out-of-sync',\n};\n\n/**\n * The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the\n * numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine\n * second dimension: every branch reduces to `fault present?` x `on the allowlist?`.\n *\n * All three are named because all three are LOGGED: the sh half writes a line for the healthy hand-down\n * too (`PASS-BIN-ALLOW`), which is the line that tells \"the guard ran and found nothing\" apart from\n * \"the guard never ran\".\n */\nexport const L0_ROW_HANDED_DOWN = '1';\n\n/** Row 2: a fault is present but the call is on the L0 allowlist — a cure, a Read, or the config edit. */\nexport const L0_ROW_ALLOWLISTED = '2';\n\n/** Row 3: a fault is present and the call is NOT on the allowlist. The one row that ever BLOCKS. */\nexport const L0_ROW_BLOCKED = '3';\n\n/**\n * The `[guard-name] (layer=L0 fault=<code> row=<n>)` header every L0 deny opens with, after the ❌ line.\n *\n * ONE builder, called by the JS denies directly and interpolated into the POSIX-sh denies at render\n * time by renderShim() — so the sh half cannot spell the coordinates differently from the JS half even\n * though it cannot import anything at runtime. `detail` is the per-fault count that mirrors\n * formatReport's `(N violations)`.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over this leaf module's own constants; it must stay importable by the dependency-free shim renderer.\nexport function l0GuardHeader(fault: L0FaultCode, detail: string): string {\n return `[${L0_FAULT_NAMES[fault]}] (layer=L0 fault=${fault} row=${L0_ROW_BLOCKED}, ${detail})`;\n}\n\n/**\n * The one-line citation of WHICH matrix row was taken and on what dimension values — L1's pattern\n * (its deny cites \"`w` / `n` / `n` - row 8\"), in L0's own two columns.\n */\n// webpieces-disable no-function-outside-class -- sibling of l0GuardHeader in this leaf codebook module.\nexport function l0MatrixCitation(fault: L0FaultCode): string {\n return `matrix row ${L0_ROW_BLOCKED}: fault=${fault} present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=${L0_ROW_BLOCKED} fault=${fault}) and the same row webpieces.guard-matrix.md prints.`;\n}\n"]}
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;;;AAgI9E,sCAEC;AAOD,4CAEC;AAzID,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;AA6BX;;;;;;;;GAQG;AACU,QAAA,cAAc,GAA0C;IACjE,CAAC,sBAAc,CAAC,EAAE,eAAe;IACjC,CAAC,4BAAoB,CAAC,EAAE,mBAAmB;IAC3C,CAAC,2BAAmB,CAAC,EAAE,sBAAsB;IAC7C,CAAC,2BAAmB,CAAC,EAAE,mBAAmB;IAC1C,CAAC,2BAAmB,CAAC,EAAE,sBAAsB;IAC7C,CAAC,+BAAuB,CAAC,EAAE,gBAAgB;IAC3C,CAAC,mCAA2B,CAAC,EAAE,oBAAoB;CACtD,CAAC;AAEF;;;;GAIG;AACU,QAAA,QAAQ,GAAG,IAAI,CAAC;AAE7B;;;;;;;;GAQG;AACU,QAAA,kBAAkB,GAAG,GAAG,CAAC;AAEtC,0GAA0G;AAC7F,QAAA,kBAAkB,GAAG,GAAG,CAAC;AAEtC,oGAAoG;AACvF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC;;;;;;;GAOG;AACH,0KAA0K;AAC1K,SAAgB,aAAa,CAAC,KAAkB,EAAE,MAAc;IAC5D,OAAO,IAAI,sBAAc,CAAC,KAAK,CAAC,YAAY,gBAAQ,UAAU,KAAK,QAAQ,sBAAc,KAAK,MAAM,GAAG,CAAC;AAC5G,CAAC;AAED;;;GAGG;AACH,wGAAwG;AACxG,SAAgB,gBAAgB,CAAC,KAAkB;IAC/C,OAAO,cAAc,sBAAc,WAAW,KAAK,0GAA0G,gBAAQ,QAAQ,sBAAc,UAAU,KAAK,sDAAsD,CAAC;AACrQ,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\n// ---------------------------------------------------------------------------\n// THE JOIN KEYS — the three artifacts that describe one L0 event, and the coordinates that line them up.\n//\n// There are three, and until now they could not be grepped together:\n// 1. THE DENY the agent reads in the moment (shim-deny-reason.ts for S; DENY_REASON_SH for D/X/U/K)\n// 2. THE AUDIT LINE (`.webpieces/logs/**`), which carries `layer=` `row=` `fault=`\n// 3. THE MATRIX DOC (webpieces.guard-matrix.md, rendered from L0_FAULTS + L0_ALLOWLIST)\n//\n// The deny had NONE of them: no fault letter, no row, and — the highest-value omission — no guard NAME,\n// while every L1/L2 deny opens `[<rule-name>] (N violations)`. So a transcript could not be debugged\n// against the log after the fact, and a reader could not find the matrix row by eye.\n//\n// These constants are that vocabulary, declared HERE for the same reason the letters are: this module is\n// a LEAF with no imports, so `core/l0-matrix.ts` (the doc), `bin/shim*.ts` (the denies) and\n// `core/decision-log.ts` (the log) can all reach it without an import cycle. Retyping a name in any one\n// of them is the drift this file exists to make impossible.\n// ---------------------------------------------------------------------------\n\n/**\n * EVERY L0 fault code, as a type. The two arrays above are the halves; this is their union, and it is\n * what makes `L0_FAULT_NAMES` TOTAL — a `Record<string, …>` would have forced a `?? 'unknown'` fallback\n * at every read, which is shim shape #4 (a runtime default standing in for a type that could have\n * expressed the invariant). With the union, a new fault added without a name is a COMPILE error, and\n * neither reader needs a defensive branch.\n */\nexport type L0FaultCode = typeof L0_SH_FAULT_CODES[number] | typeof L0_JS_FAULT_CODES[number];\n\n/**\n * The stable, human-readable GUARD NAME per fault code — what goes in the deny's `[…]` header, in the\n * matrix doc's own `guard` column, and nowhere else in a second spelling.\n *\n * L1 prints `[stale-main-bash-guard] (1 violation)` and L0 printed nothing comparable; the names below\n * are deliberately in that same kebab shape so the two layers read as one system. They are IDENTITY, not\n * prose: renaming one silently breaks a grep that spans all three artifacts, exactly as renumbering an\n * L1 row would (see L1_ROWS' header).\n */\nexport const L0_FAULT_NAMES: Readonly<Record<L0FaultCode, string>> = {\n [L0_FAULT_DRIFT]: 'version-drift',\n [L0_FAULT_BIN_MISSING]: 'guard-bin-missing',\n [L0_FAULT_UNDECLARED]: 'guard-pkg-undeclared',\n [L0_FAULT_BIN_BROKEN]: 'guard-bin-crashed',\n [L0_FAULT_SHIM_STALE]: 'managed-hook-surface',\n [L0_FAULT_CONFIG_MISSING]: 'config-missing',\n [L0_FAULT_CONFIG_OUT_OF_SYNC]: 'config-out-of-sync',\n};\n\n/**\n * The LAYER token — the other half of every join key, spelled once. The deny header, the matrix\n * citation, the sh audit line and guards/L0-tooling.md all read it from here, so `grep layer=L0` cannot\n * miss an artifact that typed the token itself.\n */\nexport const L0_LAYER = 'L0';\n\n/**\n * The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the\n * numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine\n * second dimension: every branch reduces to `fault present?` x `on the allowlist?`.\n *\n * All three are named because all three are LOGGED: the sh half writes a line for the healthy hand-down\n * too (`PASS-BIN-ALLOW`), which is the line that tells \"the guard ran and found nothing\" apart from\n * \"the guard never ran\".\n */\nexport const L0_ROW_HANDED_DOWN = '1';\n\n/** Row 2: a fault is present but the call is on the L0 allowlist — a cure, a Read, or the config edit. */\nexport const L0_ROW_ALLOWLISTED = '2';\n\n/** Row 3: a fault is present and the call is NOT on the allowlist. The one row that ever BLOCKS. */\nexport const L0_ROW_BLOCKED = '3';\n\n/**\n * The `[guard-name] (layer=L0 fault=<code> row=<n>)` header every L0 deny opens with, after the ❌ line.\n *\n * ONE builder, called by the JS denies directly and interpolated into the POSIX-sh denies at render\n * time by renderShim() — so the sh half cannot spell the coordinates differently from the JS half even\n * though it cannot import anything at runtime. `detail` is the per-fault count that mirrors\n * formatReport's `(N violations)`.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over this leaf module's own constants; it must stay importable by the dependency-free shim renderer.\nexport function l0GuardHeader(fault: L0FaultCode, detail: string): string {\n return `[${L0_FAULT_NAMES[fault]}] (layer=${L0_LAYER} fault=${fault} row=${L0_ROW_BLOCKED}, ${detail})`;\n}\n\n/**\n * The one-line citation of WHICH matrix row was taken and on what dimension values — L1's pattern\n * (its deny cites \"`w` / `n` / `n` - row 8\"), in L0's own two columns.\n */\n// webpieces-disable no-function-outside-class -- sibling of l0GuardHeader in this leaf codebook module.\nexport function l0MatrixCitation(fault: L0FaultCode): string {\n return `matrix row ${L0_ROW_BLOCKED}: fault=${fault} present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=${L0_LAYER} row=${L0_ROW_BLOCKED} fault=${fault}) and the same row webpieces.guard-matrix.md prints.`;\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /** Opening marker of the generated block, as it appears in guards/L0-tooling.md. */
2
+ export declare const L0_DOC_BEGIN = "<!-- BEGIN GENERATED \u2014 L0ToolingDoc.render() in ai-hook-rules/src/core/l0-tooling-doc.ts; run `pnpm guards:generate` -->";
3
+ /** Closing marker. Everything between the two is machine-owned; everything outside is prose. */
4
+ export declare const L0_DOC_END = "<!-- END GENERATED \u2014 hand-written prose resumes here -->";
5
+ /**
6
+ * The generated section of guards/L0-tooling.md, and the splice that puts it there.
7
+ *
8
+ * A class rather than a family of module functions (see l1-doc.ts, which predates the rule): the
9
+ * renderer, the extractor and the splicer are one unit, and the spec drives all three.
10
+ */
11
+ export declare class L0ToolingDoc {
12
+ /** The whole generated block, WITHOUT the markers — those belong to the file, not to the renderer. */
13
+ render(): string;
14
+ /**
15
+ * The generated text of `doc`, exactly as committed. Throws when a marker is missing or doubled —
16
+ * a silently-unspliced doc is the drift this whole arrangement exists to end.
17
+ */
18
+ extract(doc: string): string;
19
+ /** `doc` with the generated block replaced by today's render. Preserves every byte outside it. */
20
+ splice(doc: string): string;
21
+ /** A markdown cell: a literal `|` inside a value would end the column. */
22
+ private cell;
23
+ private preamble;
24
+ private faultTable;
25
+ /**
26
+ * The cures, one row per option. LITERAL commands only, rendered from `L0_FAULTS[].cures` — the same
27
+ * array `webpieces.guard-matrix.md` renders its Fix sections from, so the two can never prescribe
28
+ * different commands for one fault.
29
+ */
30
+ private fixTable;
31
+ private matrixTable;
32
+ private allowlistTable;
33
+ /**
34
+ * Fault `S`'s subject: the THREE managed things, and the two registrations rendered from
35
+ * `shimCommand()` — which is what makes "they are ABSOLUTE" a fact this doc cannot get wrong.
36
+ */
37
+ private managedSurface;
38
+ /**
39
+ * The audit line, rendered from `SHIM_LOG_FIELDS` + `SHIM_LOG_VERDICTS`. The optional field prints
40
+ * in brackets because that is exactly what it is: `bin=` appears only when it differs from `shim=`.
41
+ */
42
+ private auditLine;
43
+ private logPaths;
44
+ }
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.L0ToolingDoc = exports.L0_DOC_END = exports.L0_DOC_BEGIN = void 0;
4
+ const rules_config_1 = require("@webpieces/rules-config");
5
+ const shim_1 = require("../bin/shim");
6
+ const hook_registration_1 = require("../bin/hook-registration");
7
+ const l0_fault_codes_1 = require("./l0-fault-codes");
8
+ const log_streams_1 = require("./log-streams");
9
+ const l0_matrix_1 = require("./l0-matrix");
10
+ // ---------------------------------------------------------------------------
11
+ // THE GENERATED HALF OF guards/L0-tooling.md.
12
+ //
13
+ // That file is the largest guard doc in the repo and, unlike its two siblings, it was hand-written from
14
+ // end to end — so it went stale twice in one session: it described FOUR managed surfaces after there
15
+ // were three, and a 7-field audit line after `shim=`/`bin=`/`layer=`/`row=` had joined it.
16
+ //
17
+ // The split, and WHY it is a split rather than a whole-file renderer:
18
+ //
19
+ // GENERATED (here) anything that is a COORDINATE of the code — the fault codes and their guard
20
+ // names, the cures, the three matrix rows, the allowlist, the managed surfaces,
21
+ // the audit-line fields and the verdict vocabulary. Every one of those is
22
+ // already an array somewhere; a doc that re-types them is a second spelling.
23
+ // HAND-WRITTEN (there) the incident histories, the config-validation invariant, the known gaps, the
24
+ // worked example. That is argument, not data, and a renderer would mangle it.
25
+ //
26
+ // The section is spliced BETWEEN two literal markers, the pattern renderL1Doc() already uses for its
27
+ // prose/table interleave, so the prose surrounds the tables instead of being swallowed by them.
28
+ // `pnpm guards:generate` rewrites the block and `l0-tooling-doc.spec.ts` locks it byte-for-byte.
29
+ //
30
+ // EVERY COMMAND PRINTED HERE COMES FROM A CONSTANT, never a retyped literal: the L0 allowlist matches
31
+ // WHOLE command strings, so a paraphrased cure in a doc is an unrunnable cure. The spec re-asserts that
32
+ // by running isAllowed() over every command this renderer prints.
33
+ // ---------------------------------------------------------------------------
34
+ /** Opening marker of the generated block, as it appears in guards/L0-tooling.md. */
35
+ exports.L0_DOC_BEGIN = '<!-- BEGIN GENERATED — L0ToolingDoc.render() in ai-hook-rules/src/core/l0-tooling-doc.ts; run `pnpm guards:generate` -->';
36
+ /** Closing marker. Everything between the two is machine-owned; everything outside is prose. */
37
+ exports.L0_DOC_END = '<!-- END GENERATED — hand-written prose resumes here -->';
38
+ /**
39
+ * The generated section of guards/L0-tooling.md, and the splice that puts it there.
40
+ *
41
+ * A class rather than a family of module functions (see l1-doc.ts, which predates the rule): the
42
+ * renderer, the extractor and the splicer are one unit, and the spec drives all three.
43
+ */
44
+ class L0ToolingDoc {
45
+ /** The whole generated block, WITHOUT the markers — those belong to the file, not to the renderer. */
46
+ render() {
47
+ return [
48
+ ...this.preamble(),
49
+ ...this.faultTable(),
50
+ ...this.fixTable(),
51
+ ...this.matrixTable(),
52
+ ...this.allowlistTable(),
53
+ ...this.managedSurface(),
54
+ ...this.auditLine(),
55
+ ].join('\n');
56
+ }
57
+ /**
58
+ * The generated text of `doc`, exactly as committed. Throws when a marker is missing or doubled —
59
+ * a silently-unspliced doc is the drift this whole arrangement exists to end.
60
+ */
61
+ extract(doc) {
62
+ const opens = doc.split(exports.L0_DOC_BEGIN).length - 1;
63
+ const closes = doc.split(exports.L0_DOC_END).length - 1;
64
+ if (opens !== 1 || closes !== 1) {
65
+ throw new Error(`guards/L0-tooling.md must carry exactly one BEGIN/END marker pair, found ${String(opens)}/${String(closes)}`);
66
+ }
67
+ const afterBegin = doc.slice(doc.indexOf(exports.L0_DOC_BEGIN) + exports.L0_DOC_BEGIN.length);
68
+ return afterBegin.slice(0, afterBegin.indexOf(exports.L0_DOC_END)).replace(/^\n/, '').replace(/\n$/, '');
69
+ }
70
+ /** `doc` with the generated block replaced by today's render. Preserves every byte outside it. */
71
+ splice(doc) {
72
+ const head = doc.slice(0, doc.indexOf(exports.L0_DOC_BEGIN) + exports.L0_DOC_BEGIN.length);
73
+ const tail = doc.slice(doc.indexOf(exports.L0_DOC_END));
74
+ // extract() is called for its VALIDATION — one marker pair — before anything is rewritten.
75
+ this.extract(doc);
76
+ return `${head}\n${this.render()}\n${tail}`;
77
+ }
78
+ /** A markdown cell: a literal `|` inside a value would end the column. */
79
+ cell(value) {
80
+ return value.split('|').join('\\|');
81
+ }
82
+ preamble() {
83
+ return [
84
+ '> **GENERATED — do not hand-edit between the markers.** Rendered by `L0ToolingDoc.render()`',
85
+ '> (`ai-hook-rules/src/core/l0-tooling-doc.ts`) from `L0_FAULTS`, `L0_ALLOWLIST`, the managed-surface',
86
+ '> constants and `SHIM_LOG_FIELDS` — the same arrays the guard consults. `pnpm guards:generate`',
87
+ '> rewrites it; `l0-tooling-doc.spec.ts` locks it byte-for-byte. The prose outside the markers is',
88
+ '> hand-written and stays that way.',
89
+ '',
90
+ ];
91
+ }
92
+ faultTable() {
93
+ return [
94
+ '### The faults',
95
+ '',
96
+ '| code | guard name | fault | detected by | enforced in |',
97
+ '|---|---|---|---|---|',
98
+ ...l0_matrix_1.L0_FAULTS.map((f) => `| \`${f.code}\` | \`${l0_fault_codes_1.L0_FAULT_NAMES[f.code]}\` | ${this.cell(f.name)} | ${f.detectedBy} | ${f.enforcedIn} |`),
99
+ '',
100
+ `First match wins. \`${l0_fault_codes_1.L0_SH_FAULT_CODES.join('`/`')}\` are decided in POSIX \`sh\` inside the committed shim, BEFORE`,
101
+ `the guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.`,
102
+ `\`${l0_fault_codes_1.L0_JS_FAULT_CODES.join('`/`')}\` are decided inside the bin, in JS.`,
103
+ '',
104
+ ];
105
+ }
106
+ /**
107
+ * The cures, one row per option. LITERAL commands only, rendered from `L0_FAULTS[].cures` — the same
108
+ * array `webpieces.guard-matrix.md` renders its Fix sections from, so the two can never prescribe
109
+ * different commands for one fault.
110
+ */
111
+ fixTable() {
112
+ const rows = [];
113
+ for (const fault of l0_matrix_1.L0_FAULTS) {
114
+ fault.cures.forEach((cure, i) => {
115
+ const literal = cure.isCommand() ? `\`${cure.call.command}\`` : `edit \`${cure.mention}\` yourself`;
116
+ const option = `${String(i + 1)}${cure.preferred ? ' (preferred)' : ''}`;
117
+ rows.push(`| \`${fault.code}\` | ${option} | ${this.cell(literal)} | ${this.cell(cure.discriminator)} |`);
118
+ });
119
+ }
120
+ return [
121
+ '### The fix, per fault — type the option EXACTLY as written, and run nothing else on that line',
122
+ '',
123
+ '| fault | option | run EXACTLY | pick this when |',
124
+ '|---|---|---|---|',
125
+ ...rows,
126
+ '',
127
+ ];
128
+ }
129
+ matrixTable() {
130
+ return [
131
+ '### The matrix — three rows, and the fault only picks the MESSAGE',
132
+ '',
133
+ `| row | fault | on the allowlist? | outcome | logged as |`,
134
+ '|---|---|---|---|---|',
135
+ `| ${l0_fault_codes_1.L0_ROW_HANDED_DOWN} | none | — | hand down to the next guard layer | \`layer=${l0_fault_codes_1.L0_LAYER} row=${l0_fault_codes_1.L0_ROW_HANDED_DOWN}\` |`,
136
+ `| ${l0_fault_codes_1.L0_ROW_ALLOWLISTED} | any | yes | PASS or ALLOW (see the entry) | \`layer=${l0_fault_codes_1.L0_LAYER} row=${l0_fault_codes_1.L0_ROW_ALLOWLISTED}\` |`,
137
+ `| ${l0_fault_codes_1.L0_ROW_BLOCKED} | any | no | BLOCK — **only the message varies by fault** | \`layer=${l0_fault_codes_1.L0_LAYER} row=${l0_fault_codes_1.L0_ROW_BLOCKED}\` |`,
138
+ '',
139
+ 'The tool is not a dimension either: "any Read" is an allowlist ENTRY, not a tool check. Those are',
140
+ 'the same coordinates every L0 deny opens with, so a deny, a log line and this table join by eye.',
141
+ '',
142
+ ];
143
+ }
144
+ allowlistTable() {
145
+ return [
146
+ '### The allowlist — ONE list, consulted identically by every fault',
147
+ '',
148
+ '| # | allowed | outcome | bypasses L1 on a HEALTHY tree? |',
149
+ '|---|---|---|---|',
150
+ ...shim_1.L0_ALLOWLIST.map((e, i) => `| ${String(i + 1)} | ${this.cell(e.label)} | ${e.kind.toUpperCase()} | ${e.cure ? 'yes — it REPAIRS the tooling' : 'no — it repairs nothing, so L1 still judges it'} |`),
151
+ '',
152
+ '- **PASS** — L0 has no objection; the call falls THROUGH so downstream guards still judge it.',
153
+ '- **ALLOW** — terminal; bypasses everything, because a cure must stay reachable even when a',
154
+ ' downstream guard would block it.',
155
+ '',
156
+ 'Every Bash entry is anchored to the WHOLE command. A leading `cd <dir> &&`, a trailing `2>&1` and a',
157
+ 'pipe into `tail`/`head` are tolerated; nothing else. Appending `&& git status` makes it a DIFFERENT',
158
+ 'command and it is rejected again — that is not the guard refusing its own cure.',
159
+ '',
160
+ '`git merge` and a **bare** `git pull` are both deliberately absent — see "The git-sync split"',
161
+ 'below for why the one safe pull spelling is on the list and the bare one is not. Main is merged',
162
+ 'ONLY through the 3-point fork merge (`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a',
163
+ 'PR is already open).',
164
+ '',
165
+ ];
166
+ }
167
+ /**
168
+ * Fault `S`'s subject: the THREE managed things, and the two registrations rendered from
169
+ * `shimCommand()` — which is what makes "they are ABSOLUTE" a fact this doc cannot get wrong.
170
+ */
171
+ managedSurface() {
172
+ return [
173
+ '### The managed hook surface — what fault `S` compares (THREE things, one set)',
174
+ '',
175
+ '| # | surface |',
176
+ '|---|---|',
177
+ `| 1 | \`${hook_registration_1.SHIM_SURFACE}\` |`,
178
+ `| 2 | ${this.cell(hook_registration_1.REGISTRATION_SURFACE)} |`,
179
+ `| 3 | ${this.cell(hook_registration_1.ENV_SURFACE)} |`,
180
+ '',
181
+ 'The registration is TWO PreToolUse entries, and both are ABSOLUTE — they resolve from any cwd:',
182
+ '',
183
+ '```',
184
+ (0, hook_registration_1.shimCommand)(hook_registration_1.GUARDS_BIN),
185
+ (0, hook_registration_1.shimCommand)(hook_registration_1.RULES_BIN),
186
+ '```',
187
+ '',
188
+ `\`${shim_1.UPGRADE_SHIM_CMD}\` repairs all three. \`${shim_1.RESTORE_SHIM_CMD}\``,
189
+ `repairs \`${hook_registration_1.SHIM_SURFACE}\` and nothing else, so it is the fallback for an installed release too old`,
190
+ 'to carry the first.',
191
+ '',
192
+ ];
193
+ }
194
+ /**
195
+ * The audit line, rendered from `SHIM_LOG_FIELDS` + `SHIM_LOG_VERDICTS`. The optional field prints
196
+ * in brackets because that is exactly what it is: `bin=` appears only when it differs from `shim=`.
197
+ */
198
+ auditLine() {
199
+ const shape = shim_1.SHIM_LOG_FIELDS.map((f) => (f.optional ? `[${f.label}]` : f.label)).join(' ');
200
+ return [
201
+ '### The L0 audit line — one tab-separated line per tool call',
202
+ '',
203
+ '```',
204
+ shape,
205
+ '```',
206
+ '',
207
+ '| # | field | means |',
208
+ '|---|---|---|',
209
+ ...shim_1.SHIM_LOG_FIELDS.map((f, i) => `| ${String(i + 1)} | \`${this.cell(f.label)}\` | ${this.cell(f.means)} |`),
210
+ '',
211
+ '| verdict | means |',
212
+ '|---|---|',
213
+ ...shim_1.SHIM_LOG_VERDICTS.map((v) => `| \`${v.label}\` | ${this.cell(v.means)} |`),
214
+ '',
215
+ ...this.logPaths(),
216
+ ];
217
+ }
218
+ logPaths() {
219
+ const stream = `${rules_config_1.LOGS_STATE_DIR}/${log_streams_1.L0_SHIM_STREAM}/<session>-<agent|coordinator>-<binName>.log`;
220
+ return [
221
+ 'It lands in the log directory of the tree the CALL was made in, centralized under the primary clone',
222
+ 'so that removing a worktree does not take its audit trail with it:',
223
+ '',
224
+ '```',
225
+ `<primary>/${rules_config_1.WEBPIECES_TMP_DIR}/${rules_config_1.WORKTREE_STATE_DIR}/<tree>/${stream}`,
226
+ `<primary>/${rules_config_1.WEBPIECES_TMP_DIR}/${stream} # from the primary clone itself`,
227
+ '```',
228
+ '',
229
+ `The binary stamps the same \`layer=\`/\`row=\`/\`fault=\` fields onto its OWN streams —`,
230
+ `\`${log_streams_1.L1_LOCATION_STREAM}/\`, \`${log_streams_1.L2_DECISIONS_STREAM}/\`, \`${log_streams_1.CALLS_STREAM}/\` and \`${log_streams_1.REJECTIONS_STREAM}/\` under the same`,
231
+ `\`${rules_config_1.LOGS_STATE_DIR}/\` — so one grep spans the whole trail. A \`${rules_config_1.CONFIG_FILENAME}\` fault (\`C\`/\`Y\`) is`,
232
+ 'therefore visible there and never on an `L0-shim` line, which only ever carries the `sh`-side codes.',
233
+ '',
234
+ ];
235
+ }
236
+ }
237
+ exports.L0ToolingDoc = L0ToolingDoc;
238
+ //# sourceMappingURL=l0-tooling-doc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l0-tooling-doc.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-tooling-doc.ts"],"names":[],"mappings":"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","sourcesContent":["import { CONFIG_FILENAME, LOGS_STATE_DIR, WEBPIECES_TMP_DIR, WORKTREE_STATE_DIR } from '@webpieces/rules-config';\n\nimport {\n L0AllowEntry, L0_ALLOWLIST, RESTORE_SHIM_CMD, SHIM_LOG_FIELDS, SHIM_LOG_VERDICTS, ShimLogField,\n ShimLogVerdict, UPGRADE_SHIM_CMD,\n} from '../bin/shim';\nimport {\n ENV_SURFACE, GUARDS_BIN, REGISTRATION_SURFACE, RULES_BIN, SHIM_SURFACE, shimCommand,\n} from '../bin/hook-registration';\nimport {\n L0FaultCode, L0_FAULT_NAMES, L0_JS_FAULT_CODES, L0_LAYER, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED,\n L0_ROW_HANDED_DOWN, L0_SH_FAULT_CODES,\n} from './l0-fault-codes';\nimport {\n CALLS_STREAM, L0_SHIM_STREAM, L1_LOCATION_STREAM, L2_DECISIONS_STREAM, REJECTIONS_STREAM,\n} from './log-streams';\nimport { L0Cure, L0Fault, L0_FAULTS } from './l0-matrix';\n\n// ---------------------------------------------------------------------------\n// THE GENERATED HALF OF guards/L0-tooling.md.\n//\n// That file is the largest guard doc in the repo and, unlike its two siblings, it was hand-written from\n// end to end — so it went stale twice in one session: it described FOUR managed surfaces after there\n// were three, and a 7-field audit line after `shim=`/`bin=`/`layer=`/`row=` had joined it.\n//\n// The split, and WHY it is a split rather than a whole-file renderer:\n//\n// GENERATED (here) anything that is a COORDINATE of the code — the fault codes and their guard\n// names, the cures, the three matrix rows, the allowlist, the managed surfaces,\n// the audit-line fields and the verdict vocabulary. Every one of those is\n// already an array somewhere; a doc that re-types them is a second spelling.\n// HAND-WRITTEN (there) the incident histories, the config-validation invariant, the known gaps, the\n// worked example. That is argument, not data, and a renderer would mangle it.\n//\n// The section is spliced BETWEEN two literal markers, the pattern renderL1Doc() already uses for its\n// prose/table interleave, so the prose surrounds the tables instead of being swallowed by them.\n// `pnpm guards:generate` rewrites the block and `l0-tooling-doc.spec.ts` locks it byte-for-byte.\n//\n// EVERY COMMAND PRINTED HERE COMES FROM A CONSTANT, never a retyped literal: the L0 allowlist matches\n// WHOLE command strings, so a paraphrased cure in a doc is an unrunnable cure. The spec re-asserts that\n// by running isAllowed() over every command this renderer prints.\n// ---------------------------------------------------------------------------\n\n/** Opening marker of the generated block, as it appears in guards/L0-tooling.md. */\nexport const L0_DOC_BEGIN = '<!-- BEGIN GENERATED — L0ToolingDoc.render() in ai-hook-rules/src/core/l0-tooling-doc.ts; run `pnpm guards:generate` -->';\n\n/** Closing marker. Everything between the two is machine-owned; everything outside is prose. */\nexport const L0_DOC_END = '<!-- END GENERATED — hand-written prose resumes here -->';\n\n/**\n * The generated section of guards/L0-tooling.md, and the splice that puts it there.\n *\n * A class rather than a family of module functions (see l1-doc.ts, which predates the rule): the\n * renderer, the extractor and the splicer are one unit, and the spec drives all three.\n */\nexport class L0ToolingDoc {\n /** The whole generated block, WITHOUT the markers — those belong to the file, not to the renderer. */\n render(): string {\n return [\n ...this.preamble(),\n ...this.faultTable(),\n ...this.fixTable(),\n ...this.matrixTable(),\n ...this.allowlistTable(),\n ...this.managedSurface(),\n ...this.auditLine(),\n ].join('\\n');\n }\n\n /**\n * The generated text of `doc`, exactly as committed. Throws when a marker is missing or doubled —\n * a silently-unspliced doc is the drift this whole arrangement exists to end.\n */\n extract(doc: string): string {\n const opens = doc.split(L0_DOC_BEGIN).length - 1;\n const closes = doc.split(L0_DOC_END).length - 1;\n if (opens !== 1 || closes !== 1) {\n throw new Error(`guards/L0-tooling.md must carry exactly one BEGIN/END marker pair, found ${String(opens)}/${String(closes)}`);\n }\n const afterBegin = doc.slice(doc.indexOf(L0_DOC_BEGIN) + L0_DOC_BEGIN.length);\n return afterBegin.slice(0, afterBegin.indexOf(L0_DOC_END)).replace(/^\\n/, '').replace(/\\n$/, '');\n }\n\n /** `doc` with the generated block replaced by today's render. Preserves every byte outside it. */\n splice(doc: string): string {\n const head = doc.slice(0, doc.indexOf(L0_DOC_BEGIN) + L0_DOC_BEGIN.length);\n const tail = doc.slice(doc.indexOf(L0_DOC_END));\n // extract() is called for its VALIDATION — one marker pair — before anything is rewritten.\n this.extract(doc);\n return `${head}\\n${this.render()}\\n${tail}`;\n }\n\n /** A markdown cell: a literal `|` inside a value would end the column. */\n private cell(value: string): string {\n return value.split('|').join('\\\\|');\n }\n\n private preamble(): string[] {\n return [\n '> **GENERATED — do not hand-edit between the markers.** Rendered by `L0ToolingDoc.render()`',\n '> (`ai-hook-rules/src/core/l0-tooling-doc.ts`) from `L0_FAULTS`, `L0_ALLOWLIST`, the managed-surface',\n '> constants and `SHIM_LOG_FIELDS` — the same arrays the guard consults. `pnpm guards:generate`',\n '> rewrites it; `l0-tooling-doc.spec.ts` locks it byte-for-byte. The prose outside the markers is',\n '> hand-written and stays that way.',\n '',\n ];\n }\n\n private faultTable(): string[] {\n return [\n '### The faults',\n '',\n '| code | guard name | fault | detected by | enforced in |',\n '|---|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string =>\n `| \\`${f.code}\\` | \\`${L0_FAULT_NAMES[f.code as L0FaultCode]}\\` | ${this.cell(f.name)} | ${f.detectedBy} | ${f.enforcedIn} |`),\n '',\n `First match wins. \\`${L0_SH_FAULT_CODES.join('`/`')}\\` are decided in POSIX \\`sh\\` inside the committed shim, BEFORE`,\n `the guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.`,\n `\\`${L0_JS_FAULT_CODES.join('`/`')}\\` are decided inside the bin, in JS.`,\n '',\n ];\n }\n\n /**\n * The cures, one row per option. LITERAL commands only, rendered from `L0_FAULTS[].cures` — the same\n * array `webpieces.guard-matrix.md` renders its Fix sections from, so the two can never prescribe\n * different commands for one fault.\n */\n private fixTable(): string[] {\n const rows: string[] = [];\n for (const fault of L0_FAULTS) {\n fault.cures.forEach((cure: L0Cure, i: number): void => {\n const literal = cure.isCommand() ? `\\`${cure.call.command}\\`` : `edit \\`${cure.mention}\\` yourself`;\n const option = `${String(i + 1)}${cure.preferred ? ' (preferred)' : ''}`;\n rows.push(`| \\`${fault.code}\\` | ${option} | ${this.cell(literal)} | ${this.cell(cure.discriminator)} |`);\n });\n }\n return [\n '### The fix, per fault — type the option EXACTLY as written, and run nothing else on that line',\n '',\n '| fault | option | run EXACTLY | pick this when |',\n '|---|---|---|---|',\n ...rows,\n '',\n ];\n }\n\n private matrixTable(): string[] {\n return [\n '### The matrix — three rows, and the fault only picks the MESSAGE',\n '',\n `| row | fault | on the allowlist? | outcome | logged as |`,\n '|---|---|---|---|---|',\n `| ${L0_ROW_HANDED_DOWN} | none | — | hand down to the next guard layer | \\`layer=${L0_LAYER} row=${L0_ROW_HANDED_DOWN}\\` |`,\n `| ${L0_ROW_ALLOWLISTED} | any | yes | PASS or ALLOW (see the entry) | \\`layer=${L0_LAYER} row=${L0_ROW_ALLOWLISTED}\\` |`,\n `| ${L0_ROW_BLOCKED} | any | no | BLOCK — **only the message varies by fault** | \\`layer=${L0_LAYER} row=${L0_ROW_BLOCKED}\\` |`,\n '',\n 'The tool is not a dimension either: \"any Read\" is an allowlist ENTRY, not a tool check. Those are',\n 'the same coordinates every L0 deny opens with, so a deny, a log line and this table join by eye.',\n '',\n ];\n }\n\n private allowlistTable(): string[] {\n return [\n '### The allowlist — ONE list, consulted identically by every fault',\n '',\n '| # | allowed | outcome | bypasses L1 on a HEALTHY tree? |',\n '|---|---|---|---|',\n ...L0_ALLOWLIST.map((e: L0AllowEntry, i: number): string =>\n `| ${String(i + 1)} | ${this.cell(e.label)} | ${e.kind.toUpperCase()} | ${e.cure ? 'yes — it REPAIRS the tooling' : 'no — it repairs nothing, so L1 still judges it'} |`),\n '',\n '- **PASS** — L0 has no objection; the call falls THROUGH so downstream guards still judge it.',\n '- **ALLOW** — terminal; bypasses everything, because a cure must stay reachable even when a',\n ' downstream guard would block it.',\n '',\n 'Every Bash entry is anchored to the WHOLE command. A leading `cd <dir> &&`, a trailing `2>&1` and a',\n 'pipe into `tail`/`head` are tolerated; nothing else. Appending `&& git status` makes it a DIFFERENT',\n 'command and it is rejected again — that is not the guard refusing its own cure.',\n '',\n '`git merge` and a **bare** `git pull` are both deliberately absent — see \"The git-sync split\"',\n 'below for why the one safe pull spelling is on the list and the bare one is not. Main is merged',\n 'ONLY through the 3-point fork merge (`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a',\n 'PR is already open).',\n '',\n ];\n }\n\n /**\n * Fault `S`'s subject: the THREE managed things, and the two registrations rendered from\n * `shimCommand()` — which is what makes \"they are ABSOLUTE\" a fact this doc cannot get wrong.\n */\n private managedSurface(): string[] {\n return [\n '### The managed hook surface — what fault `S` compares (THREE things, one set)',\n '',\n '| # | surface |',\n '|---|---|',\n `| 1 | \\`${SHIM_SURFACE}\\` |`,\n `| 2 | ${this.cell(REGISTRATION_SURFACE)} |`,\n `| 3 | ${this.cell(ENV_SURFACE)} |`,\n '',\n 'The registration is TWO PreToolUse entries, and both are ABSOLUTE — they resolve from any cwd:',\n '',\n '```',\n shimCommand(GUARDS_BIN),\n shimCommand(RULES_BIN),\n '```',\n '',\n `\\`${UPGRADE_SHIM_CMD}\\` repairs all three. \\`${RESTORE_SHIM_CMD}\\``,\n `repairs \\`${SHIM_SURFACE}\\` and nothing else, so it is the fallback for an installed release too old`,\n 'to carry the first.',\n '',\n ];\n }\n\n /**\n * The audit line, rendered from `SHIM_LOG_FIELDS` + `SHIM_LOG_VERDICTS`. The optional field prints\n * in brackets because that is exactly what it is: `bin=` appears only when it differs from `shim=`.\n */\n private auditLine(): string[] {\n const shape = SHIM_LOG_FIELDS.map((f: ShimLogField): string => (f.optional ? `[${f.label}]` : f.label)).join(' ');\n return [\n '### The L0 audit line — one tab-separated line per tool call',\n '',\n '```',\n shape,\n '```',\n '',\n '| # | field | means |',\n '|---|---|---|',\n ...SHIM_LOG_FIELDS.map((f: ShimLogField, i: number): string =>\n `| ${String(i + 1)} | \\`${this.cell(f.label)}\\` | ${this.cell(f.means)} |`),\n '',\n '| verdict | means |',\n '|---|---|',\n ...SHIM_LOG_VERDICTS.map((v: ShimLogVerdict): string => `| \\`${v.label}\\` | ${this.cell(v.means)} |`),\n '',\n ...this.logPaths(),\n ];\n }\n\n private logPaths(): string[] {\n const stream = `${LOGS_STATE_DIR}/${L0_SHIM_STREAM}/<session>-<agent|coordinator>-<binName>.log`;\n return [\n 'It lands in the log directory of the tree the CALL was made in, centralized under the primary clone',\n 'so that removing a worktree does not take its audit trail with it:',\n '',\n '```',\n `<primary>/${WEBPIECES_TMP_DIR}/${WORKTREE_STATE_DIR}/<tree>/${stream}`,\n `<primary>/${WEBPIECES_TMP_DIR}/${stream} # from the primary clone itself`,\n '```',\n '',\n `The binary stamps the same \\`layer=\\`/\\`row=\\`/\\`fault=\\` fields onto its OWN streams —`,\n `\\`${L1_LOCATION_STREAM}/\\`, \\`${L2_DECISIONS_STREAM}/\\`, \\`${CALLS_STREAM}/\\` and \\`${REJECTIONS_STREAM}/\\` under the same`,\n `\\`${LOGS_STATE_DIR}/\\` — so one grep spans the whole trail. A \\`${CONFIG_FILENAME}\\` fault (\\`C\\`/\\`Y\\`) is`,\n 'therefore visible there and never on an `L0-shim` line, which only ever carries the `sh`-side codes.',\n '',\n ];\n }\n}\n"]}