@webpieces/ai-hook-rules 0.4.629 → 0.4.631
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/adapters/claude-code-response.js +16 -1
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +1 -1
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +3 -2
- package/src/bin/shim-audit-log.js +31 -10
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +0 -5
- package/src/bin/shim-deny-reason.js +199 -108
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim.js +55 -14
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +12 -4
- package/src/core/decision-log.js +13 -5
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-fault-codes.d.ts +46 -0
- package/src/core/l0-fault-codes.js +55 -1
- package/src/core/l0-fault-codes.js.map +1 -1
- package/src/core/l0-matrix.d.ts +23 -4
- package/src/core/l0-matrix.js +66 -21
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/rules/branch-switch-scan.d.ts +38 -0
- package/src/core/rules/branch-switch-scan.js +127 -0
- package/src/core/rules/branch-switch-scan.js.map +1 -0
- package/src/core/rules/redirect-how-to-merge-main.d.ts +1 -0
- package/src/core/rules/redirect-how-to-merge-main.js +15 -5
- package/src/core/rules/redirect-how-to-merge-main.js.map +1 -1
- package/src/core/rules/stale-main-bash-guard.d.ts +1 -6
- package/src/core/rules/stale-main-bash-guard.js +7 -21
- package/src/core/rules/stale-main-bash-guard.js.map +1 -1
- package/src/core/runner.js +1 -1
- package/src/core/runner.js.map +1 -1
- package/templates/ai-hook.sh +53 -19
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
// rule engine.
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
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_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
|
+
exports.l0GuardHeader = l0GuardHeader;
|
|
25
|
+
exports.l0MatrixCitation = l0MatrixCitation;
|
|
24
26
|
/** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */
|
|
25
27
|
exports.L0_FAULT_DRIFT = 'D';
|
|
26
28
|
/** `X` — the guard bin is missing (fresh clone, new worktree, package removed). Decided in `sh`. */
|
|
@@ -58,4 +60,56 @@ exports.L0_SH_FAULT_CODES = [
|
|
|
58
60
|
exports.L0_JS_FAULT_CODES = [
|
|
59
61
|
exports.L0_FAULT_SHIM_STALE, exports.L0_FAULT_CONFIG_MISSING, exports.L0_FAULT_CONFIG_OUT_OF_SYNC,
|
|
60
62
|
];
|
|
63
|
+
/**
|
|
64
|
+
* The stable, human-readable GUARD NAME per fault code — what goes in the deny's `[…]` header, in the
|
|
65
|
+
* matrix doc's own `guard` column, and nowhere else in a second spelling.
|
|
66
|
+
*
|
|
67
|
+
* L1 prints `[stale-main-bash-guard] (1 violation)` and L0 printed nothing comparable; the names below
|
|
68
|
+
* are deliberately in that same kebab shape so the two layers read as one system. They are IDENTITY, not
|
|
69
|
+
* prose: renaming one silently breaks a grep that spans all three artifacts, exactly as renumbering an
|
|
70
|
+
* L1 row would (see L1_ROWS' header).
|
|
71
|
+
*/
|
|
72
|
+
exports.L0_FAULT_NAMES = {
|
|
73
|
+
[exports.L0_FAULT_DRIFT]: 'version-drift',
|
|
74
|
+
[exports.L0_FAULT_BIN_MISSING]: 'guard-bin-missing',
|
|
75
|
+
[exports.L0_FAULT_UNDECLARED]: 'guard-pkg-undeclared',
|
|
76
|
+
[exports.L0_FAULT_BIN_BROKEN]: 'guard-bin-crashed',
|
|
77
|
+
[exports.L0_FAULT_SHIM_STALE]: 'managed-hook-surface',
|
|
78
|
+
[exports.L0_FAULT_CONFIG_MISSING]: 'config-missing',
|
|
79
|
+
[exports.L0_FAULT_CONFIG_OUT_OF_SYNC]: 'config-out-of-sync',
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the
|
|
83
|
+
* numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine
|
|
84
|
+
* second dimension: every branch reduces to `fault present?` x `on the allowlist?`.
|
|
85
|
+
*
|
|
86
|
+
* All three are named because all three are LOGGED: the sh half writes a line for the healthy hand-down
|
|
87
|
+
* too (`PASS-BIN-ALLOW`), which is the line that tells "the guard ran and found nothing" apart from
|
|
88
|
+
* "the guard never ran".
|
|
89
|
+
*/
|
|
90
|
+
exports.L0_ROW_HANDED_DOWN = '1';
|
|
91
|
+
/** Row 2: a fault is present but the call is on the L0 allowlist — a cure, a Read, or the config edit. */
|
|
92
|
+
exports.L0_ROW_ALLOWLISTED = '2';
|
|
93
|
+
/** Row 3: a fault is present and the call is NOT on the allowlist. The one row that ever BLOCKS. */
|
|
94
|
+
exports.L0_ROW_BLOCKED = '3';
|
|
95
|
+
/**
|
|
96
|
+
* The `[guard-name] (layer=L0 fault=<code> row=<n>)` header every L0 deny opens with, after the ❌ line.
|
|
97
|
+
*
|
|
98
|
+
* ONE builder, called by the JS denies directly and interpolated into the POSIX-sh denies at render
|
|
99
|
+
* time by renderShim() — so the sh half cannot spell the coordinates differently from the JS half even
|
|
100
|
+
* though it cannot import anything at runtime. `detail` is the per-fault count that mirrors
|
|
101
|
+
* formatReport's `(N violations)`.
|
|
102
|
+
*/
|
|
103
|
+
// 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
|
+
function l0GuardHeader(fault, detail) {
|
|
105
|
+
return `[${exports.L0_FAULT_NAMES[fault]}] (layer=L0 fault=${fault} row=${exports.L0_ROW_BLOCKED}, ${detail})`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The one-line citation of WHICH matrix row was taken and on what dimension values — L1's pattern
|
|
109
|
+
* (its deny cites "`w` / `n` / `n` - row 8"), in L0's own two columns.
|
|
110
|
+
*/
|
|
111
|
+
// webpieces-disable no-function-outside-class -- sibling of l0GuardHeader in this leaf codebook module.
|
|
112
|
+
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.`;
|
|
114
|
+
}
|
|
61
115
|
//# 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;;;AAE9E,gGAAgG;AACnF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC,oGAAoG;AACvF,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAExC,iGAAiG;AACpF,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,oFAAoF;AACvE,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,uGAAuG;AAC1F,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,2EAA2E;AAC9D,QAAA,uBAAuB,GAAG,GAAG,CAAC;AAE3C,yFAAyF;AAC5E,QAAA,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;;;GAKG;AACU,QAAA,aAAa,GAAG,GAAG,CAAC;AAEjC;;;GAGG;AACU,QAAA,iBAAiB,GAAG;IAC7B,sBAAc,EAAE,4BAAoB,EAAE,2BAAmB,EAAE,2BAAmB;CACxE,CAAC;AAEX;;;;;GAKG;AACU,QAAA,iBAAiB,GAAG;IAC7B,2BAAmB,EAAE,+BAAuB,EAAE,mCAA2B;CACnE,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// THE L0 FAULT CODEBOOK — one letter per fault, declared HERE and nowhere else.\n//\n// Both halves of L0 stamp `fault=<code>` onto their audit lines: the POSIX `sh` shim writes D/X/U/K\n// (shim-audit-log.ts), and the guard bin writes S/C/Y in JS (decision-log.ts, via runner.ts and\n// hook-core.ts). The whole value of that field is that ONE grep — `grep 'fault=S'` — spans the entire\n// trail, and that the faults actually observed can be diffed against `L0_FAULTS`. Both properties hold\n// only while every emitter spells the letters the SAME way, and a hand-retyped 'S' in one emitter is\n// exactly the drift this module exists to make impossible.\n//\n// It used to be retyped: the shim assigned `WP_FAULT=X` as a literal, SHIM_LOG_FAULTS listed the four\n// sh-side letters again, and L0_FAULTS listed all seven a third time. Three spellings of one\n// vocabulary, held together by a unit test that could only notice AFTER they diverged.\n//\n// This module is a LEAF — no imports at all — on purpose. `l0-matrix.ts` (core) builds `L0_FAULTS`\n// from these constants and `shim-audit-log.ts` / `shim.ts` (bin) render them into the shim; parking the\n// constants in either of those two would make the other one a core↔bin import cycle. It also keeps the\n// shim renderer dependency-light, which it must be: it has to work on a tree too broken to load the\n// rule engine.\n// ---------------------------------------------------------------------------\n\n/** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */\nexport const L0_FAULT_DRIFT = 'D';\n\n/** `X` — the guard bin is missing (fresh clone, new worktree, package removed). Decided in `sh`. */\nexport const L0_FAULT_BIN_MISSING = 'X';\n\n/** `U` — the bin is missing AND nothing declares the package, so an install is a no-op. `sh`. */\nexport const L0_FAULT_UNDECLARED = 'U';\n\n/** `K` — the bin is present but CRASHED (corrupt node_modules). Decided in `sh`. */\nexport const L0_FAULT_BIN_BROKEN = 'K';\n\n/** `S` — the committed `.claude/webpieces/ai-hook.sh` != `renderShim()`. Decided in the bin, in JS. */\nexport const L0_FAULT_SHIM_STALE = 'S';\n\n/** `C` — `webpieces.config.json` is missing. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_MISSING = 'C';\n\n/** `Y` — a loaded rule has no `webpieces.config.json` key. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_OUT_OF_SYNC = 'Y';\n\n/**\n * No fault AT THIS LAYER — the value every audit line carries when nothing fired.\n *\n * Never a claim that nothing was wrong: a `fault=-` line from the `sh` shim only says the sh half found\n * nothing, and the bin it then exec'd may still have blocked on S/C/Y and stamped its own line.\n */\nexport const L0_FAULT_NONE = '-';\n\n/**\n * The faults decided in POSIX `sh`, BEFORE the bin runs — a stale, missing or broken validator cannot\n * be trusted to validate itself. In first-match-wins order.\n */\nexport const L0_SH_FAULT_CODES = [\n L0_FAULT_DRIFT, L0_FAULT_BIN_MISSING, L0_FAULT_UNDECLARED, L0_FAULT_BIN_BROKEN,\n] as const;\n\n/**\n * The faults decided INSIDE the guard bin, in JS. These reached the audit trail with no fault label at\n * all until the JS emitters started stamping them: an `S` storm that blocked an agent for ~20 tool\n * calls left two lines in the `rejections/` stream, both attributed to a downstream rule, and nothing\n * anywhere identifying L0.\n */\nexport const L0_JS_FAULT_CODES = [\n L0_FAULT_SHIM_STALE, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n] as const;\n"]}
|
|
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"]}
|
package/src/core/l0-matrix.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { L0Call } from '../bin/shim';
|
|
2
|
+
import { L0FaultCode } from './l0-fault-codes';
|
|
2
3
|
/**
|
|
3
4
|
* The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,
|
|
4
5
|
* and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.
|
|
@@ -47,7 +48,12 @@ export declare class L0Cure {
|
|
|
47
48
|
}
|
|
48
49
|
/** One L0 fault. Data-only → a class, per CLAUDE.md. */
|
|
49
50
|
export declare class L0Fault {
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The codebook's letter — typed as the UNION of every declared code, not `string`, so
|
|
53
|
+
* `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a
|
|
54
|
+
* `?? 'unknown'` fallback. A fault added without a name fails to compile.
|
|
55
|
+
*/
|
|
56
|
+
readonly code: L0FaultCode;
|
|
51
57
|
readonly name: string;
|
|
52
58
|
readonly detectedBy: string;
|
|
53
59
|
readonly enforcedIn: string;
|
|
@@ -58,7 +64,13 @@ export declare class L0Fault {
|
|
|
58
64
|
* the same bytes the consumer runs, which is what the mention assertion needs to search.
|
|
59
65
|
*/
|
|
60
66
|
readonly denyText: string;
|
|
61
|
-
constructor(
|
|
67
|
+
constructor(
|
|
68
|
+
/**
|
|
69
|
+
* The codebook's letter — typed as the UNION of every declared code, not `string`, so
|
|
70
|
+
* `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a
|
|
71
|
+
* `?? 'unknown'` fallback. A fault added without a name fails to compile.
|
|
72
|
+
*/
|
|
73
|
+
code: L0FaultCode, name: string, detectedBy: string, enforcedIn: string, cures: readonly L0Cure[],
|
|
62
74
|
/**
|
|
63
75
|
* The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for
|
|
64
76
|
* D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —
|
|
@@ -67,7 +79,7 @@ export declare class L0Fault {
|
|
|
67
79
|
denyText: string);
|
|
68
80
|
}
|
|
69
81
|
export declare const CONFIG_MISSING_REPORT: string;
|
|
70
|
-
export declare const CONFIG_OUT_OF_SYNC_HEADER
|
|
82
|
+
export declare const CONFIG_OUT_OF_SYNC_HEADER: string;
|
|
71
83
|
/**
|
|
72
84
|
* THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a
|
|
73
85
|
* stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside
|
|
@@ -91,5 +103,12 @@ export declare function renderGuardMatrixDoc(): string;
|
|
|
91
103
|
* with a crash.
|
|
92
104
|
*/
|
|
93
105
|
export declare function writeGuardMatrixDoc(workspaceRoot: string): string;
|
|
94
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written.
|
|
108
|
+
*
|
|
109
|
+
* It opens with a NEWLINE, not a space: the JS-side L0 denies render in the house format now (a header,
|
|
110
|
+
* a `[guard-name]` block, `Fix Option N:` lines), so a pointer glued onto the end of the last line would
|
|
111
|
+
* be the one place the shape broke. A real newline is safe on both call paths — denyJson() JSON.stringifies
|
|
112
|
+
* it, exactly as it does for every multi-line L1 report.
|
|
113
|
+
*/
|
|
95
114
|
export declare function guardMatrixPointer(docPath: string): string;
|
package/src/core/l0-matrix.js
CHANGED
|
@@ -78,7 +78,13 @@ class L0Fault {
|
|
|
78
78
|
enforcedIn;
|
|
79
79
|
cures;
|
|
80
80
|
denyText;
|
|
81
|
-
constructor(
|
|
81
|
+
constructor(
|
|
82
|
+
/**
|
|
83
|
+
* The codebook's letter — typed as the UNION of every declared code, not `string`, so
|
|
84
|
+
* `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a
|
|
85
|
+
* `?? 'unknown'` fallback. A fault added without a name fails to compile.
|
|
86
|
+
*/
|
|
87
|
+
code, name, detectedBy, enforcedIn, cures,
|
|
82
88
|
/**
|
|
83
89
|
* The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for
|
|
84
90
|
* D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —
|
|
@@ -108,19 +114,41 @@ exports.L0Fault = L0Fault;
|
|
|
108
114
|
// agent. Writing the file is the one cure that always works, and it is the same cure every other config
|
|
109
115
|
// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every
|
|
110
116
|
// error at once, so the write/validate loop converges in a couple of passes.
|
|
111
|
-
exports.CONFIG_MISSING_REPORT =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
exports.CONFIG_MISSING_REPORT = [
|
|
118
|
+
`❌ webpieces ai-hooks blocked this call: ${rules_config_1.CONFIG_FILENAME} not found.`,
|
|
119
|
+
'',
|
|
120
|
+
(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_CONFIG_MISSING, '1 violation'),
|
|
121
|
+
` ${rules_config_1.CONFIG_FILENAME}`,
|
|
122
|
+
' → the webpieces guards cannot run without it, so every OTHER tool call is blocked.',
|
|
123
|
+
` → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_CONFIG_MISSING)}`,
|
|
124
|
+
'',
|
|
125
|
+
'Still allowed while this block is up:',
|
|
126
|
+
' - any Read',
|
|
127
|
+
` - any Write/Edit whose target is ${rules_config_1.CONFIG_FILENAME}`,
|
|
128
|
+
' - every command on the L0 allowlist, including the Fix Options below',
|
|
129
|
+
' THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human.',
|
|
130
|
+
'',
|
|
131
|
+
` Fix Option 1: (preferred) it needs no other tool and it never prompts - create ${rules_config_1.CONFIG_FILENAME}`,
|
|
132
|
+
' yourself. The validator reports EVERY missing/invalid entry at once (each with the snippet to',
|
|
133
|
+
' paste), so a minimal first draft converges in about two passes.',
|
|
134
|
+
' Fix Option 2: pick this ONLY at an interactive terminal where you can answer its two prompts - it',
|
|
135
|
+
' goes on to wire the Claude Code hooks and asks for a target twice, which hangs a non-interactive',
|
|
136
|
+
' session.',
|
|
137
|
+
' run EXACTLY: `pnpm exec wp-install-ai-hooks`',
|
|
138
|
+
'',
|
|
139
|
+
'Do not append anything to the option you pick — the allowlist is anchored to the whole command.',
|
|
140
|
+
].join('\n');
|
|
141
|
+
// The HEADER of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule
|
|
142
|
+
// detail as the `[…]` block's offenders). Kept here so the fault table quotes the same text the runner
|
|
143
|
+
// emits, and so Y opens with the same `[guard-name] (layer=L0 fault=Y row=3)` coordinates as every
|
|
144
|
+
// other L0 fault — that triple is what joins the deny to the audit line and to the matrix row.
|
|
145
|
+
exports.CONFIG_OUT_OF_SYNC_HEADER = [
|
|
146
|
+
`❌ webpieces ai-hooks blocked this call: ${rules_config_1.CONFIG_FILENAME} is out of sync.`,
|
|
147
|
+
'',
|
|
148
|
+
(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_CONFIG_OUT_OF_SYNC, '1 violation'),
|
|
149
|
+
` new built-in rules are present that have no entry in ${rules_config_1.CONFIG_FILENAME}`,
|
|
150
|
+
` → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_CONFIG_OUT_OF_SYNC)}`,
|
|
151
|
+
].join('\n');
|
|
124
152
|
// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation
|
|
125
153
|
// invariant in guards/L0-tooling.md: every config problem cures to "make the file right", the validator
|
|
126
154
|
// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits
|
|
@@ -224,9 +252,15 @@ function renderGuardMatrixDoc() {
|
|
|
224
252
|
'',
|
|
225
253
|
'## The faults',
|
|
226
254
|
'',
|
|
227
|
-
'
|
|
228
|
-
'
|
|
229
|
-
|
|
255
|
+
'THE JOIN KEYS ARE `guard`, `fault=` and `row=`. Every L0 deny opens',
|
|
256
|
+
'`[<guard>] (layer=L0 fault=<code> row=3, …)`, and every audit line — from BOTH halves of L0, the',
|
|
257
|
+
'`sh` shim and the guard bin — carries `layer=L0 row=<n> fault=<code>`. So one grep lands you in',
|
|
258
|
+
'the deny, the log line and the row below. The guard names come from `L0_FAULT_NAMES` and the row',
|
|
259
|
+
'numbers from `L0_ROW_*`, both spelled in exactly one place (`core/l0-fault-codes.ts`).',
|
|
260
|
+
'',
|
|
261
|
+
'| code | guard | fault | detected by | enforced in |',
|
|
262
|
+
'|---|---|---|---|---|',
|
|
263
|
+
...exports.L0_FAULTS.map((f) => `| \`${f.code}\` | \`${l0_fault_codes_1.L0_FAULT_NAMES[f.code]}\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),
|
|
230
264
|
'',
|
|
231
265
|
'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',
|
|
232
266
|
'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',
|
|
@@ -256,8 +290,12 @@ function renderMatrixAndAllowlist() {
|
|
|
256
290
|
'| # | fault | on the allowlist? | outcome |',
|
|
257
291
|
'|---|---|---|---|',
|
|
258
292
|
'| 1 | none | — | hand down to the next guard layer |',
|
|
259
|
-
|
|
260
|
-
|
|
293
|
+
`| ${l0_fault_codes_1.L0_ROW_ALLOWLISTED} | any | yes | PASS or ALLOW (see the entry) |`,
|
|
294
|
+
`| ${l0_fault_codes_1.L0_ROW_BLOCKED} | any | no | BLOCK — **only the message varies by fault** |`,
|
|
295
|
+
'',
|
|
296
|
+
`Row ${l0_fault_codes_1.L0_ROW_BLOCKED} is the only row that blocks, so every L0 deny cites it — \`row=${l0_fault_codes_1.L0_ROW_BLOCKED}\` in the`,
|
|
297
|
+
'deny header, `row=' + l0_fault_codes_1.L0_ROW_BLOCKED + '` on the audit line, and this row here. Same numbers as L1 uses for',
|
|
298
|
+
'its own rows (see `L1_ROWS`), and for the same reason: a row number is IDENTITY, so it is never reused.',
|
|
261
299
|
'',
|
|
262
300
|
'The tool is not a dimension either: "any Read" is an allowlist ENTRY, not a tool check.',
|
|
263
301
|
'',
|
|
@@ -316,11 +354,18 @@ function writeGuardMatrixDoc(workspaceRoot) {
|
|
|
316
354
|
return '';
|
|
317
355
|
}
|
|
318
356
|
}
|
|
319
|
-
/**
|
|
357
|
+
/**
|
|
358
|
+
* The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written.
|
|
359
|
+
*
|
|
360
|
+
* It opens with a NEWLINE, not a space: the JS-side L0 denies render in the house format now (a header,
|
|
361
|
+
* a `[guard-name]` block, `Fix Option N:` lines), so a pointer glued onto the end of the last line would
|
|
362
|
+
* be the one place the shape broke. A real newline is safe on both call paths — denyJson() JSON.stringifies
|
|
363
|
+
* it, exactly as it does for every multi-line L1 report.
|
|
364
|
+
*/
|
|
320
365
|
// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module
|
|
321
366
|
function guardMatrixPointer(docPath) {
|
|
322
367
|
if (docPath === '')
|
|
323
368
|
return '';
|
|
324
|
-
return
|
|
369
|
+
return `\nThe full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;
|
|
325
370
|
}
|
|
326
371
|
//# sourceMappingURL=l0-matrix.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l0-matrix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-matrix.ts"],"names":[],"mappings":"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","sourcesContent":["import { CONFIG_FILENAME, writeTemplate } from '@webpieces/rules-config';\n\nimport {\n ADD_HOOK_PKG_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call, L0_ALLOWLIST, RECOVERY_CMD,\n RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, renderShim,\n} from '../bin/shim';\nimport { ENV_SURFACE, REGISTRATION_SURFACE } from '../bin/hook-registration';\nimport { shimStaleDenyReason } from '../bin/shim-deny-reason';\nimport {\n L0_FAULT_BIN_BROKEN, L0_FAULT_BIN_MISSING, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n L0_FAULT_DRIFT, L0_FAULT_SHIM_STALE, L0_FAULT_UNDECLARED,\n} from './l0-fault-codes';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// L0 — the TOOLING-INTEGRITY layer, as data.\n//\n// L0 is the outermost guard: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its faults are\n// enumerated in L0_FAULTS below and — drawn as a decision matrix — have NO genuine second dimension:\n//\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n//\n// so the only thing that varies per fault is the MESSAGE. This module holds the fault table and\n// renders it, together with L0_ALLOWLIST (../bin/shim), into webpieces.guard-matrix.md — the doc the\n// deny messages point the AI at. Doc and code come from the SAME arrays, so they cannot drift.\n// ---------------------------------------------------------------------------\n\n/**\n * The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,\n * and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.\n *\n * That generated doc is the AUTHORITY for the fault table and the allowlist (same arrays, cannot\n * drift). guards/L0-tooling.md is the hand-written companion: it adds L0's evaluation\n * ORDER, the use cases and the known gaps, and it documents L1, none of which are rendered from code.\n * Change L0_FAULTS or L0_ALLOWLIST and the generated doc follows automatically — guards/L0-tooling.md does\n * not, so update it in the same PR.\n */\nexport const GUARD_MATRIX_DOC = 'webpieces.guard-matrix.md';\n\n/**\n * One CURE for a fault: the exact call, plus the `mention` that must appear in that fault's deny text.\n *\n * Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny\n * message must actually name it. That pairing is the anti-deadlock invariant — a message that\n * prescribes a command the allowlist rejects is exactly the shape of the three deadlocks CLAUDE.md\n * records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.\n */\nexport class L0Cure {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly mention: string,\n readonly call: L0Call,\n /**\n * The one to reach for first when a fault has several. Exactly one cure per fault carries it,\n * so the rendered Fix section never asks the reader to choose between equals.\n */\n readonly preferred: boolean,\n /**\n * WHEN to pick this cure over its siblings — the sentence that makes a list of commands\n * actionable instead of a menu (\"when the PIN is the stale side\", not \"an alternative\").\n */\n readonly discriminator: string,\n ) {}\n\n /** A Bash cure renders as a literal command; a tool-shaped one renders as the edit it stands for. */\n isCommand(): boolean {\n return this.call.toolName === 'Bash';\n }\n}\n\n/** One L0 fault. Data-only → a class, per CLAUDE.md. */\nexport class L0Fault {\n constructor(\n readonly code: string,\n readonly name: string,\n readonly detectedBy: string,\n readonly enforcedIn: string,\n readonly cures: readonly L0Cure[],\n /**\n * The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for\n * D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —\n * the same bytes the consumer runs, which is what the mention assertion needs to search.\n */\n readonly denyText: string,\n ) {}\n}\n\n// The deny for fault C, and the ONLY message L0 has for a repo with no webpieces.config.json at all.\n// Moved here from runner.ts so the fault table and the runner cannot state different cures.\n//\n// It used to name `./node_modules/.bin/wp-setup-ai-hooks` — a bin that HAS NOT EXISTED since it was\n// renamed to wp-install-ai-hooks. So the one command this deny prescribed was (a) not installable and\n// (b) not on the L0 allowlist in that spelling, i.e. the AI was handed a cure it could neither run nor\n// get past the guard. Now it names the installer that actually seeds the config AND is entry 8 of the\n// allowlist, and it says out loud that writing the config yourself is allowed through.\n//\n// ORDERING (2026-08-02): writing the file yourself now LEADS. The bare installer used to be OPTION 1,\n// but it seeds the config and then PROMPTS twice for a hook target, which hangs a non-interactive\n// agent. Writing the file is the one cure that always works, and it is the same cure every other config\n// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every\n// error at once, so the write/validate loop converges in a couple of passes.\nexport const CONFIG_MISSING_REPORT =\n `${CONFIG_FILENAME} not found — the webpieces guards cannot run without it, so every other tool call is blocked.\\n` +\n 'THIS IS NOT A DEADLOCK: both options below are explicitly allowed through while this guard is up.\\n' +\n `OPTION 1 (preferred — it needs no other tool and it never prompts) - create ${CONFIG_FILENAME}\\n` +\n 'yourself: any Read, and any Write/Edit whose target is that file, is always allowed through, so\\n' +\n 'you can inspect the repo and write it. The validator reports EVERY missing/invalid entry at once\\n' +\n '(each with the snippet to paste), so a minimal first draft converges in about two passes.\\n' +\n 'OPTION 2 (pick this ONLY at an interactive terminal where you can answer its two prompts) - run\\n' +\n 'EXACTLY this command to seed the config: `pnpm exec wp-install-ai-hooks`. It goes on to wire the\\n' +\n 'Claude Code hooks and asks for a target twice, which hangs a non-interactive session.\\n' +\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.';\n\n// The first line of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail). Kept here so the fault table quotes the same text the runner emits.\nexport const CONFIG_OUT_OF_SYNC_HEADER =\n `${CONFIG_FILENAME} is out of sync — new built-in rules are present that have no entry in ${CONFIG_FILENAME}.`;\n\n// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation\n// invariant in guards/L0-tooling.md: every config problem cures to \"make the file right\", the validator\n// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits\n// this edit unconditionally. (That section is the authority — do not restate its reasoning here.)\nconst CONFIG_WRITE_CURE = new L0Cure(\n CONFIG_FILENAME, new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`), true,\n 'this fault fires at all — it is the only cure that needs no other tool, and it is never denied',\n);\n\n// Cure calls are spelled exactly as the deny messages spell them, so the mention assertion is a real\n// string search rather than a paraphrase.\n// webpieces-disable no-function-outside-class -- pure constructor helper for the L0_FAULTS literal below, in this data module\nfunction bashCure(command: string, preferred: boolean, discriminator: string): L0Cure {\n return new L0Cure(command, new L0Call('Bash', command, ''), preferred, discriminator);\n}\n\n/**\n * THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a\n * stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside\n * the bin, in JS. One model, two enforcement points.\n */\nexport const L0_FAULTS: readonly L0Fault[] = [\n new L0Fault(L0_FAULT_DRIFT, 'version drift — root package.json pin != installed version',\n 'sh, before the bin runs', 'sh',\n [\n // `pnpm install` clears D in BOTH directions — it makes installed == pin by definition — so\n // it is always the preferred cure. The direction only decides whether the PIN is the version\n // you WANT, which is what the second cure is for.\n bashCure('pnpm install', true,\n 'node_modules is OLDER than the pin, OR you are on a feature branch and want YOUR '\n + 'branch pin (usually the case) — it always clears the drift'),\n bashCure('git pull', false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'pull first and install second; a bare install would downgrade you'),\n ], renderShim()),\n new L0Fault(L0_FAULT_BIN_MISSING, 'guard bin missing (fresh clone / new worktree / package removed)',\n 'sh, before the bin runs', 'sh',\n [bashCure('pnpm install', true,\n 'this fault fires at all — nothing is installed in THIS tree, and a new git worktree '\n + 'copies no node_modules')],\n renderShim()),\n // U is X with the ONE input that inverts X's cure, which is why it is a separate fault and not a\n // sentence inside X's message: when nothing declares the package, `pnpm install` is not a weaker fix,\n // it is a PROVABLE no-op, and an agent that trusts the X text will run it until it gives up. See the\n // ADD_HOOK_PKG entry in l0-allowlist.ts for the incident.\n new L0Fault(L0_FAULT_UNDECLARED, `guard bin missing AND ${HOOK_PKG} is not declared in package.json`,\n 'sh, before the bin runs', 'sh',\n [bashCure(ADD_HOOK_PKG_CMD, true,\n 'this fault fires at all — package.json asks for nothing, so pnpm install reports '\n + '\"Lockfile is up to date\" and leaves the tree exactly as broken as it found it')],\n renderShim()),\n new L0Fault(L0_FAULT_BIN_BROKEN, 'guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)',\n 'sh, before the bin runs', 'sh',\n [bashCure(RECOVERY_CMD, true,\n 'this fault fires at all — a BARE pnpm install SKIPS the corrupt package, because pnpm sees '\n + 'the right version on disk and considers it installed; only the delete forces a rewrite')],\n renderShim()),\n // S covers the WHOLE managed hook surface, not just the shim: the committed ai-hook.sh, the\n // .claude/settings.json entries that register them AND the managed `env` entry that pins the Bash\n // cwd so the hooks resolve identically for every subagent. They only work as a set — a settings file left\n // on a superseded form silently changes who governs, re-pinning every tree to the\n // primary's release — and nothing validated the registration at all before it joined this fault.\n new L0Fault(L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file, the .claude/settings.json registration or its managed env entry does not match this release',\n 'the guard bin', 'JS',\n [\n // wp-upgrade-shim leads because it is the ONLY cure that repairs all three, and it is\n // still surgical: it rewrites ai-hook.sh, the registration and the managed env entry\n // and touches no config, and it imports only fs/path so it runs on a tree too broken to load\n // the rule engine. The INSTALLER is deliberately NOT a cure here: it also migrates the config\n // and prompts for a target twice, which hangs a non-interactive agent.\n bashCure(UPGRADE_SHIM_CMD, true,\n 'this fault fires at all — it is the only cure that repairs all three managed things '\n + '(ai-hook.sh, the settings.json registration and its managed env entry), and it also '\n + 'deletes the retired guarantee-root.sh and any entry still naming it, and it '\n + 'touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer'),\n // 2026-07-21: the version gap below caused a real \"command not found\" deadlock.\n bashCure(RESTORE_SHIM_CMD, false,\n 'the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does '\n + 'not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade '\n + '@webpieces afterwards and run Option 1 to finish'),\n ],\n shimStaleDenyReason('', '', [SHIM_MARKER, REGISTRATION_SURFACE, ENV_SURFACE], false)),\n new L0Fault(L0_FAULT_CONFIG_MISSING, `${CONFIG_FILENAME} missing`,\n 'the guard bin', 'JS',\n [\n CONFIG_WRITE_CURE,\n // Kept, but demoted: it seeds the file and then PROMPTS twice for a hook target.\n bashCure(INSTALL_HOOKS_CMD, false,\n 'you are at an INTERACTIVE terminal and can answer its two hook-target prompts'),\n ], CONFIG_MISSING_REPORT),\n new L0Fault(L0_FAULT_CONFIG_OUT_OF_SYNC, `a loaded rule has no ${CONFIG_FILENAME} key`,\n 'the guard bin', 'JS',\n [CONFIG_WRITE_CURE], CONFIG_OUT_OF_SYNC_HEADER),\n];\n\n/**\n * One fault's FIX section, rendered from its `cures` array — literal commands only, never prose.\n *\n * This is the half that used to live in hand-written docs and drift. The three fields of L0Cure map\n * onto the three things a blocked reader needs and nothing else: WHAT to type (the call), WHETHER it is\n * the default (preferred), and WHEN to pick a sibling instead (discriminator). A cure with no\n * discriminator would render as a menu of equals, which is how an agent picks the wrong one.\n */\n// webpieces-disable no-function-outside-class -- pure string builder for renderGuardMatrixDoc below, beside the arrays it reads\nfunction renderFixSection(fault: L0Fault): string[] {\n const options = fault.cures.map((cure: L0Cure, i: number): string => {\n // A Bash cure is the command verbatim; a tool-shaped one is the file it edits (allowlist entry 2).\n const literal = cure.isCommand() ? `\\`${cure.call.command}\\`` : `edit \\`${cure.mention}\\` yourself`;\n const label = cure.preferred ? `Option ${i + 1} (preferred)` : `Option ${i + 1}`;\n return `- **${label}**: ${literal} ← pick this when ${cure.discriminator}`;\n });\n return [`### \\`${fault.code}\\` — ${fault.name}`, '', ...options, ''];\n}\n\n/**\n * Render webpieces.guard-matrix.md from L0_FAULTS + L0_ALLOWLIST.\n *\n * A unit test locks the committed template byte-identical to this output, the same way\n * templates/ai-hook.sh is locked to renderShim(). That is what makes the doc assertable instead of\n * aspirational: the table in the doc IS the array the guard consults.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over the two exported arrays, beside them in this module\nexport function renderGuardMatrixDoc(): string {\n return [\n '# webpieces guard matrix — L0 (tooling integrity)',\n '',\n 'GENERATED from `L0_FAULTS` + `L0_ALLOWLIST` in `@webpieces/ai-hook-rules`. Do not hand-edit —',\n 'a unit test locks this file byte-identical to `renderGuardMatrixDoc()`, so the table below is',\n 'the array the guard actually consults, not a description of it.',\n '',\n 'L0 is the OUTERMOST guard layer. It blocks work while `node_modules`, the committed shim, or',\n '`webpieces.config.json` are in a state that makes every other guard untrustworthy. If you are',\n 'reading this, one of the faults below fired and named this file.',\n '',\n '## The faults',\n '',\n '| code | fault | detected by | enforced in |',\n '|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string => `| \\`${f.code}\\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),\n '',\n 'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',\n 'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',\n '',\n '## The fix, per fault',\n '',\n 'Every command below is rendered from that fault\\'s `cures` array and is asserted, by unit test,',\n 'to be accepted by `isAllowed()` — so nothing here can be a command the guard then rejects. Type',\n 'the option you pick EXACTLY as written and run nothing else on that line.',\n '',\n ...L0_FAULTS.flatMap(renderFixSection),\n ...renderMatrixAndAllowlist(),\n ].join('\\n');\n}\n\n/**\n * The second half of the doc: the three-row matrix and the ONE allowlist. Split out of\n * renderGuardMatrixDoc solely to keep it inside the method-line budget — the join order is what makes\n * the two halves one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- second half of renderGuardMatrixDoc's string, beside it in this module\nfunction renderMatrixAndAllowlist(): string[] {\n return [\n '## The matrix',\n '',\n 'L0 has NO genuine second dimension. Every branch reduces to one question:',\n '',\n '| # | fault | on the allowlist? | outcome |',\n '|---|---|---|---|',\n '| 1 | none | — | hand down to the next guard layer |',\n '| 2 | any | yes | PASS or ALLOW (see the entry) |',\n '| 3 | any | no | BLOCK — **only the message varies by fault** |',\n '',\n 'The tool is not a dimension either: \"any Read\" is an allowlist ENTRY, not a tool check.',\n '',\n '## The allowlist',\n '',\n 'ONE list, consulted identically by every fault. A cure that cannot help a given fault also',\n 'cannot hurt it, and gating each entry on a fault is what produced four real defects (a stale',\n 'shim that denied `pnpm install` and `git pull`; faults that denied every Read; a config fault',\n 'that denied `rm -rf node_modules && pnpm install` while allowing a bare `pnpm install`).',\n '',\n '| # | allowed | outcome |',\n '|---|---|---|',\n ...L0_ALLOWLIST.map((e: L0AllowEntry, i: number): string => `| ${i + 1} | ${e.label} | ${e.kind.toUpperCase()} |`),\n '',\n '- **PASS** — L0 has no objection; the call falls THROUGH so the 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`',\n 'and a pipe into `tail`/`head` are tolerated; nothing else is. Appending `&& git status` makes it',\n 'a DIFFERENT command and it is rejected again — that is not the guard refusing its own cure.',\n '',\n '`git merge` is deliberately NOT on this list. Main is merged ONLY through the 3-point fork merge',\n '(`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is already open).',\n '',\n '## Known asymmetry',\n '',\n 'Under `S`/`C`/`Y` the guard bin IS running, so a PASS really does fall through to the downstream',\n 'guards. Under `D`/`X`/`U`/`K` the bin is never executed, so there is nothing to fall through to and a',\n 'PASS degenerates into a terminal allow — reads are unguarded during those three faults.',\n '',\n '## Widening L0',\n '',\n 'Add an entry to `L0_ALLOWLIST` in `packages/tooling/ai-hook-rules/src/bin/shim.ts`. That array is',\n 'the single source for the JS allowlist, the `grep -E` inside the rendered shim, and this file.',\n '',\n ];\n}\n\n/**\n * Drop the matrix doc where the AI can read it, and return its absolute path ('' if it could not be\n * written). Called from the L0 BLOCK path so the deny can say `READ <path>`.\n *\n * Best-effort by design: this runs while the tree is already known-broken, and a missing template (an\n * @webpieces/rules-config older than this package) must degrade the deny message, never replace it\n * with a crash.\n */\n// webpieces-disable no-function-outside-class -- sibling of renderGuardMatrixDoc in this module\nexport function writeGuardMatrixDoc(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return writeTemplate(workspaceRoot, GUARD_MATRIX_DOC);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: no doc → the deny simply omits the pointer\n return '';\n }\n}\n\n/** The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written. */\n// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module\nexport function guardMatrixPointer(docPath: string): string {\n if (docPath === '') return '';\n return ` The full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"l0-matrix.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-matrix.ts"],"names":[],"mappings":"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","sourcesContent":["import { CONFIG_FILENAME, writeTemplate } from '@webpieces/rules-config';\n\nimport {\n ADD_HOOK_PKG_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call, L0_ALLOWLIST, RECOVERY_CMD,\n RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, renderShim,\n} from '../bin/shim';\nimport { ENV_SURFACE, REGISTRATION_SURFACE } from '../bin/hook-registration';\nimport { shimStaleDenyReason } from '../bin/shim-deny-reason';\nimport {\n L0_FAULT_BIN_BROKEN, L0_FAULT_BIN_MISSING, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n L0_FAULT_DRIFT, L0_FAULT_NAMES, L0_FAULT_SHIM_STALE, L0_FAULT_UNDECLARED, L0FaultCode,\n L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED, l0GuardHeader, l0MatrixCitation,\n} from './l0-fault-codes';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// L0 — the TOOLING-INTEGRITY layer, as data.\n//\n// L0 is the outermost guard: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its faults are\n// enumerated in L0_FAULTS below and — drawn as a decision matrix — have NO genuine second dimension:\n//\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n//\n// so the only thing that varies per fault is the MESSAGE. This module holds the fault table and\n// renders it, together with L0_ALLOWLIST (../bin/shim), into webpieces.guard-matrix.md — the doc the\n// deny messages point the AI at. Doc and code come from the SAME arrays, so they cannot drift.\n// ---------------------------------------------------------------------------\n\n/**\n * The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,\n * and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.\n *\n * That generated doc is the AUTHORITY for the fault table and the allowlist (same arrays, cannot\n * drift). guards/L0-tooling.md is the hand-written companion: it adds L0's evaluation\n * ORDER, the use cases and the known gaps, and it documents L1, none of which are rendered from code.\n * Change L0_FAULTS or L0_ALLOWLIST and the generated doc follows automatically — guards/L0-tooling.md does\n * not, so update it in the same PR.\n */\nexport const GUARD_MATRIX_DOC = 'webpieces.guard-matrix.md';\n\n/**\n * One CURE for a fault: the exact call, plus the `mention` that must appear in that fault's deny text.\n *\n * Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny\n * message must actually name it. That pairing is the anti-deadlock invariant — a message that\n * prescribes a command the allowlist rejects is exactly the shape of the three deadlocks CLAUDE.md\n * records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.\n */\nexport class L0Cure {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly mention: string,\n readonly call: L0Call,\n /**\n * The one to reach for first when a fault has several. Exactly one cure per fault carries it,\n * so the rendered Fix section never asks the reader to choose between equals.\n */\n readonly preferred: boolean,\n /**\n * WHEN to pick this cure over its siblings — the sentence that makes a list of commands\n * actionable instead of a menu (\"when the PIN is the stale side\", not \"an alternative\").\n */\n readonly discriminator: string,\n ) {}\n\n /** A Bash cure renders as a literal command; a tool-shaped one renders as the edit it stands for. */\n isCommand(): boolean {\n return this.call.toolName === 'Bash';\n }\n}\n\n/** One L0 fault. Data-only → a class, per CLAUDE.md. */\nexport class L0Fault {\n constructor(\n /**\n * The codebook's letter — typed as the UNION of every declared code, not `string`, so\n * `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a\n * `?? 'unknown'` fallback. A fault added without a name fails to compile.\n */\n readonly code: L0FaultCode,\n readonly name: string,\n readonly detectedBy: string,\n readonly enforcedIn: string,\n readonly cures: readonly L0Cure[],\n /**\n * The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for\n * D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —\n * the same bytes the consumer runs, which is what the mention assertion needs to search.\n */\n readonly denyText: string,\n ) {}\n}\n\n// The deny for fault C, and the ONLY message L0 has for a repo with no webpieces.config.json at all.\n// Moved here from runner.ts so the fault table and the runner cannot state different cures.\n//\n// It used to name `./node_modules/.bin/wp-setup-ai-hooks` — a bin that HAS NOT EXISTED since it was\n// renamed to wp-install-ai-hooks. So the one command this deny prescribed was (a) not installable and\n// (b) not on the L0 allowlist in that spelling, i.e. the AI was handed a cure it could neither run nor\n// get past the guard. Now it names the installer that actually seeds the config AND is entry 8 of the\n// allowlist, and it says out loud that writing the config yourself is allowed through.\n//\n// ORDERING (2026-08-02): writing the file yourself now LEADS. The bare installer used to be OPTION 1,\n// but it seeds the config and then PROMPTS twice for a hook target, which hangs a non-interactive\n// agent. Writing the file is the one cure that always works, and it is the same cure every other config\n// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every\n// error at once, so the write/validate loop converges in a couple of passes.\nexport const CONFIG_MISSING_REPORT = [\n `❌ webpieces ai-hooks blocked this call: ${CONFIG_FILENAME} not found.`,\n '',\n l0GuardHeader(L0_FAULT_CONFIG_MISSING, '1 violation'),\n ` ${CONFIG_FILENAME}`,\n ' → the webpieces guards cannot run without it, so every OTHER tool call is blocked.',\n ` → ${l0MatrixCitation(L0_FAULT_CONFIG_MISSING)}`,\n '',\n 'Still allowed while this block is up:',\n ' - any Read',\n ` - any Write/Edit whose target is ${CONFIG_FILENAME}`,\n ' - every command on the L0 allowlist, including the Fix Options below',\n ' THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human.',\n '',\n ` Fix Option 1: (preferred) it needs no other tool and it never prompts - create ${CONFIG_FILENAME}`,\n ' yourself. The validator reports EVERY missing/invalid entry at once (each with the snippet to',\n ' paste), so a minimal first draft converges in about two passes.',\n ' Fix Option 2: pick this ONLY at an interactive terminal where you can answer its two prompts - it',\n ' goes on to wire the Claude Code hooks and asks for a target twice, which hangs a non-interactive',\n ' session.',\n ' run EXACTLY: `pnpm exec wp-install-ai-hooks`',\n '',\n 'Do not append anything to the option you pick — the allowlist is anchored to the whole command.',\n].join('\\n');\n\n// The HEADER of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule\n// detail as the `[…]` block's offenders). Kept here so the fault table quotes the same text the runner\n// emits, and so Y opens with the same `[guard-name] (layer=L0 fault=Y row=3)` coordinates as every\n// other L0 fault — that triple is what joins the deny to the audit line and to the matrix row.\nexport const CONFIG_OUT_OF_SYNC_HEADER = [\n `❌ webpieces ai-hooks blocked this call: ${CONFIG_FILENAME} is out of sync.`,\n '',\n l0GuardHeader(L0_FAULT_CONFIG_OUT_OF_SYNC, '1 violation'),\n ` new built-in rules are present that have no entry in ${CONFIG_FILENAME}`,\n ` → ${l0MatrixCitation(L0_FAULT_CONFIG_OUT_OF_SYNC)}`,\n].join('\\n');\n\n// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation\n// invariant in guards/L0-tooling.md: every config problem cures to \"make the file right\", the validator\n// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits\n// this edit unconditionally. (That section is the authority — do not restate its reasoning here.)\nconst CONFIG_WRITE_CURE = new L0Cure(\n CONFIG_FILENAME, new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`), true,\n 'this fault fires at all — it is the only cure that needs no other tool, and it is never denied',\n);\n\n// Cure calls are spelled exactly as the deny messages spell them, so the mention assertion is a real\n// string search rather than a paraphrase.\n// webpieces-disable no-function-outside-class -- pure constructor helper for the L0_FAULTS literal below, in this data module\nfunction bashCure(command: string, preferred: boolean, discriminator: string): L0Cure {\n return new L0Cure(command, new L0Call('Bash', command, ''), preferred, discriminator);\n}\n\n/**\n * THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a\n * stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside\n * the bin, in JS. One model, two enforcement points.\n */\nexport const L0_FAULTS: readonly L0Fault[] = [\n new L0Fault(L0_FAULT_DRIFT, 'version drift — root package.json pin != installed version',\n 'sh, before the bin runs', 'sh',\n [\n // `pnpm install` clears D in BOTH directions — it makes installed == pin by definition — so\n // it is always the preferred cure. The direction only decides whether the PIN is the version\n // you WANT, which is what the second cure is for.\n bashCure('pnpm install', true,\n 'node_modules is OLDER than the pin, OR you are on a feature branch and want YOUR '\n + 'branch pin (usually the case) — it always clears the drift'),\n bashCure('git pull', false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'pull first and install second; a bare install would downgrade you'),\n ], renderShim()),\n new L0Fault(L0_FAULT_BIN_MISSING, 'guard bin missing (fresh clone / new worktree / package removed)',\n 'sh, before the bin runs', 'sh',\n [bashCure('pnpm install', true,\n 'this fault fires at all — nothing is installed in THIS tree, and a new git worktree '\n + 'copies no node_modules')],\n renderShim()),\n // U is X with the ONE input that inverts X's cure, which is why it is a separate fault and not a\n // sentence inside X's message: when nothing declares the package, `pnpm install` is not a weaker fix,\n // it is a PROVABLE no-op, and an agent that trusts the X text will run it until it gives up. See the\n // ADD_HOOK_PKG entry in l0-allowlist.ts for the incident.\n new L0Fault(L0_FAULT_UNDECLARED, `guard bin missing AND ${HOOK_PKG} is not declared in package.json`,\n 'sh, before the bin runs', 'sh',\n [bashCure(ADD_HOOK_PKG_CMD, true,\n 'this fault fires at all — package.json asks for nothing, so pnpm install reports '\n + '\"Lockfile is up to date\" and leaves the tree exactly as broken as it found it')],\n renderShim()),\n new L0Fault(L0_FAULT_BIN_BROKEN, 'guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)',\n 'sh, before the bin runs', 'sh',\n [bashCure(RECOVERY_CMD, true,\n 'this fault fires at all — a BARE pnpm install SKIPS the corrupt package, because pnpm sees '\n + 'the right version on disk and considers it installed; only the delete forces a rewrite')],\n renderShim()),\n // S covers the WHOLE managed hook surface, not just the shim: the committed ai-hook.sh, the\n // .claude/settings.json entries that register them AND the managed `env` entry that pins the Bash\n // cwd so the hooks resolve identically for every subagent. They only work as a set — a settings file left\n // on a superseded form silently changes who governs, re-pinning every tree to the\n // primary's release — and nothing validated the registration at all before it joined this fault.\n new L0Fault(L0_FAULT_SHIM_STALE, 'a webpieces-managed hook file, the .claude/settings.json registration or its managed env entry does not match this release',\n 'the guard bin', 'JS',\n [\n // wp-upgrade-shim leads because it is the ONLY cure that repairs all three, and it is\n // still surgical: it rewrites ai-hook.sh, the registration and the managed env entry\n // and touches no config, and it imports only fs/path so it runs on a tree too broken to load\n // the rule engine. The INSTALLER is deliberately NOT a cure here: it also migrates the config\n // and prompts for a target twice, which hangs a non-interactive agent.\n bashCure(UPGRADE_SHIM_CMD, true,\n 'this fault fires at all — it is the only cure that repairs all three managed things '\n + '(ai-hook.sh, the settings.json registration and its managed env entry), and it also '\n + 'deletes the retired guarantee-root.sh and any entry still naming it, and it '\n + 'touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer'),\n // 2026-07-21: the version gap below caused a real \"command not found\" deadlock.\n bashCure(RESTORE_SHIM_CMD, false,\n 'the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does '\n + 'not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade '\n + '@webpieces afterwards and run Option 1 to finish'),\n ],\n shimStaleDenyReason('', '', [SHIM_MARKER, REGISTRATION_SURFACE, ENV_SURFACE], false)),\n new L0Fault(L0_FAULT_CONFIG_MISSING, `${CONFIG_FILENAME} missing`,\n 'the guard bin', 'JS',\n [\n CONFIG_WRITE_CURE,\n // Kept, but demoted: it seeds the file and then PROMPTS twice for a hook target.\n bashCure(INSTALL_HOOKS_CMD, false,\n 'you are at an INTERACTIVE terminal and can answer its two hook-target prompts'),\n ], CONFIG_MISSING_REPORT),\n new L0Fault(L0_FAULT_CONFIG_OUT_OF_SYNC, `a loaded rule has no ${CONFIG_FILENAME} key`,\n 'the guard bin', 'JS',\n [CONFIG_WRITE_CURE], CONFIG_OUT_OF_SYNC_HEADER),\n];\n\n/**\n * One fault's FIX section, rendered from its `cures` array — literal commands only, never prose.\n *\n * This is the half that used to live in hand-written docs and drift. The three fields of L0Cure map\n * onto the three things a blocked reader needs and nothing else: WHAT to type (the call), WHETHER it is\n * the default (preferred), and WHEN to pick a sibling instead (discriminator). A cure with no\n * discriminator would render as a menu of equals, which is how an agent picks the wrong one.\n */\n// webpieces-disable no-function-outside-class -- pure string builder for renderGuardMatrixDoc below, beside the arrays it reads\nfunction renderFixSection(fault: L0Fault): string[] {\n const options = fault.cures.map((cure: L0Cure, i: number): string => {\n // A Bash cure is the command verbatim; a tool-shaped one is the file it edits (allowlist entry 2).\n const literal = cure.isCommand() ? `\\`${cure.call.command}\\`` : `edit \\`${cure.mention}\\` yourself`;\n const label = cure.preferred ? `Option ${i + 1} (preferred)` : `Option ${i + 1}`;\n return `- **${label}**: ${literal} ← pick this when ${cure.discriminator}`;\n });\n return [`### \\`${fault.code}\\` — ${fault.name}`, '', ...options, ''];\n}\n\n/**\n * Render webpieces.guard-matrix.md from L0_FAULTS + L0_ALLOWLIST.\n *\n * A unit test locks the committed template byte-identical to this output, the same way\n * templates/ai-hook.sh is locked to renderShim(). That is what makes the doc assertable instead of\n * aspirational: the table in the doc IS the array the guard consults.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over the two exported arrays, beside them in this module\nexport function renderGuardMatrixDoc(): string {\n return [\n '# webpieces guard matrix — L0 (tooling integrity)',\n '',\n 'GENERATED from `L0_FAULTS` + `L0_ALLOWLIST` in `@webpieces/ai-hook-rules`. Do not hand-edit —',\n 'a unit test locks this file byte-identical to `renderGuardMatrixDoc()`, so the table below is',\n 'the array the guard actually consults, not a description of it.',\n '',\n 'L0 is the OUTERMOST guard layer. It blocks work while `node_modules`, the committed shim, or',\n '`webpieces.config.json` are in a state that makes every other guard untrustworthy. If you are',\n 'reading this, one of the faults below fired and named this file.',\n '',\n '## The faults',\n '',\n 'THE JOIN KEYS ARE `guard`, `fault=` and `row=`. Every L0 deny opens',\n '`[<guard>] (layer=L0 fault=<code> row=3, …)`, and every audit line — from BOTH halves of L0, the',\n '`sh` shim and the guard bin — carries `layer=L0 row=<n> fault=<code>`. So one grep lands you in',\n 'the deny, the log line and the row below. The guard names come from `L0_FAULT_NAMES` and the row',\n 'numbers from `L0_ROW_*`, both spelled in exactly one place (`core/l0-fault-codes.ts`).',\n '',\n '| code | guard | fault | detected by | enforced in |',\n '|---|---|---|---|---|',\n ...L0_FAULTS.map((f: L0Fault): string =>\n `| \\`${f.code}\\` | \\`${L0_FAULT_NAMES[f.code]}\\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),\n '',\n 'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',\n 'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',\n '',\n '## The fix, per fault',\n '',\n 'Every command below is rendered from that fault\\'s `cures` array and is asserted, by unit test,',\n 'to be accepted by `isAllowed()` — so nothing here can be a command the guard then rejects. Type',\n 'the option you pick EXACTLY as written and run nothing else on that line.',\n '',\n ...L0_FAULTS.flatMap(renderFixSection),\n ...renderMatrixAndAllowlist(),\n ].join('\\n');\n}\n\n/**\n * The second half of the doc: the three-row matrix and the ONE allowlist. Split out of\n * renderGuardMatrixDoc solely to keep it inside the method-line budget — the join order is what makes\n * the two halves one file, so keep them adjacent and keep the byte-lock test as the arbiter.\n */\n// webpieces-disable no-function-outside-class -- second half of renderGuardMatrixDoc's string, beside it in this module\nfunction renderMatrixAndAllowlist(): string[] {\n return [\n '## The matrix',\n '',\n 'L0 has NO genuine second dimension. Every branch reduces to one question:',\n '',\n '| # | fault | on the allowlist? | outcome |',\n '|---|---|---|---|',\n '| 1 | none | — | hand down to the next guard layer |',\n `| ${L0_ROW_ALLOWLISTED} | any | yes | PASS or ALLOW (see the entry) |`,\n `| ${L0_ROW_BLOCKED} | any | no | BLOCK — **only the message varies by fault** |`,\n '',\n `Row ${L0_ROW_BLOCKED} is the only row that blocks, so every L0 deny cites it — \\`row=${L0_ROW_BLOCKED}\\` in the`,\n 'deny header, `row=' + L0_ROW_BLOCKED + '` on the audit line, and this row here. Same numbers as L1 uses for',\n 'its own rows (see `L1_ROWS`), and for the same reason: a row number is IDENTITY, so it is never reused.',\n '',\n 'The tool is not a dimension either: \"any Read\" is an allowlist ENTRY, not a tool check.',\n '',\n '## The allowlist',\n '',\n 'ONE list, consulted identically by every fault. A cure that cannot help a given fault also',\n 'cannot hurt it, and gating each entry on a fault is what produced four real defects (a stale',\n 'shim that denied `pnpm install` and `git pull`; faults that denied every Read; a config fault',\n 'that denied `rm -rf node_modules && pnpm install` while allowing a bare `pnpm install`).',\n '',\n '| # | allowed | outcome |',\n '|---|---|---|',\n ...L0_ALLOWLIST.map((e: L0AllowEntry, i: number): string => `| ${i + 1} | ${e.label} | ${e.kind.toUpperCase()} |`),\n '',\n '- **PASS** — L0 has no objection; the call falls THROUGH so the 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`',\n 'and a pipe into `tail`/`head` are tolerated; nothing else is. Appending `&& git status` makes it',\n 'a DIFFERENT command and it is rejected again — that is not the guard refusing its own cure.',\n '',\n '`git merge` is deliberately NOT on this list. Main is merged ONLY through the 3-point fork merge',\n '(`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is already open).',\n '',\n '## Known asymmetry',\n '',\n 'Under `S`/`C`/`Y` the guard bin IS running, so a PASS really does fall through to the downstream',\n 'guards. Under `D`/`X`/`U`/`K` the bin is never executed, so there is nothing to fall through to and a',\n 'PASS degenerates into a terminal allow — reads are unguarded during those three faults.',\n '',\n '## Widening L0',\n '',\n 'Add an entry to `L0_ALLOWLIST` in `packages/tooling/ai-hook-rules/src/bin/shim.ts`. That array is',\n 'the single source for the JS allowlist, the `grep -E` inside the rendered shim, and this file.',\n '',\n ];\n}\n\n/**\n * Drop the matrix doc where the AI can read it, and return its absolute path ('' if it could not be\n * written). Called from the L0 BLOCK path so the deny can say `READ <path>`.\n *\n * Best-effort by design: this runs while the tree is already known-broken, and a missing template (an\n * @webpieces/rules-config older than this package) must degrade the deny message, never replace it\n * with a crash.\n */\n// webpieces-disable no-function-outside-class -- sibling of renderGuardMatrixDoc in this module\nexport function writeGuardMatrixDoc(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return writeTemplate(workspaceRoot, GUARD_MATRIX_DOC);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: no doc → the deny simply omits the pointer\n return '';\n }\n}\n\n/**\n * The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written.\n *\n * It opens with a NEWLINE, not a space: the JS-side L0 denies render in the house format now (a header,\n * a `[guard-name]` block, `Fix Option N:` lines), so a pointer glued onto the end of the last line would\n * be the one place the shape broke. A real newline is safe on both call paths — denyJson() JSON.stringifies\n * it, exactly as it does for every multi-line L1 report.\n */\n// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module\nexport function guardMatrixPointer(docPath: string): string {\n if (docPath === '') return '';\n return `\\nThe full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CommandScanner } from '../command-scan';
|
|
2
|
+
/**
|
|
3
|
+
* The branch a checkout/switch lands on, and whether that command CREATES it.
|
|
4
|
+
*
|
|
5
|
+
* `created` is load-bearing rather than cosmetic: `git checkout -b x origin/main` lands on a branch
|
|
6
|
+
* that is current by construction (nothing to be stale about), whereas landing on an EXISTING local
|
|
7
|
+
* `main` is exactly the stale-checkout hazard. Data-only, so a class (per CLAUDE.md).
|
|
8
|
+
*/
|
|
9
|
+
export declare class BranchSwitch {
|
|
10
|
+
branch: string;
|
|
11
|
+
created: boolean;
|
|
12
|
+
constructor(branch: string, created: boolean);
|
|
13
|
+
}
|
|
14
|
+
export declare class BranchSwitchScan {
|
|
15
|
+
private readonly scanner;
|
|
16
|
+
constructor(scanner?: CommandScanner);
|
|
17
|
+
/**
|
|
18
|
+
* The branch this segment switches to, or null when the segment does not land on a branch at all:
|
|
19
|
+
* a different command, no target word, `git checkout -` (the previous branch — unknowable here),
|
|
20
|
+
* or anything after `--` (which ends option parsing, making the rest PATHSPECS, so
|
|
21
|
+
* `git checkout -- main` restores a FILE named main and moves no branch).
|
|
22
|
+
*
|
|
23
|
+
* Flag-tolerant by construction: every leading flag is walked past, so `-q`, `--quiet`,
|
|
24
|
+
* `--no-track`, `--detach` and friends change nothing about which word is the target.
|
|
25
|
+
*/
|
|
26
|
+
targetOf(segment: string): BranchSwitch | null;
|
|
27
|
+
/**
|
|
28
|
+
* True only for landing on an EXISTING branch named `main` — the form that can hand you a stale
|
|
29
|
+
* tree, and the form `git checkout main && git pull origin main` uses. `-b`/`-B`/`-c`/`-C` are
|
|
30
|
+
* excluded because they create the branch here and now.
|
|
31
|
+
*/
|
|
32
|
+
landsOnExistingMain(segment: string): boolean;
|
|
33
|
+
/** The same question for a target already parsed (switchesIn). One spelling, two entry shapes. */
|
|
34
|
+
isExistingMain(target: BranchSwitch): boolean;
|
|
35
|
+
/** Every segment of a whole command that lands on a branch, in order. */
|
|
36
|
+
switchesIn(command: string): readonly BranchSwitch[];
|
|
37
|
+
private createdBranch;
|
|
38
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BranchSwitchScan = exports.BranchSwitch = void 0;
|
|
4
|
+
const command_scan_1 = require("../command-scan");
|
|
5
|
+
/**
|
|
6
|
+
* "Which branch does this `git checkout` / `git switch` land me on?" — the ONE place that question is
|
|
7
|
+
* answered, for every guard that asks it.
|
|
8
|
+
*
|
|
9
|
+
* It used to be answered by two regexes in redirect-how-to-merge-main
|
|
10
|
+
* (`/git\s+(?:checkout|switch)\s+main\b/` to EXEMPT main, and a negative-lookahead twin to catch a
|
|
11
|
+
* feature switch) and by a third, hand-rolled word walk in stale-main-bash-guard. The regex pair
|
|
12
|
+
* assumed the branch name is the token immediately after the subcommand, so ONE flag broke it:
|
|
13
|
+
*
|
|
14
|
+
* git checkout main → exempted (correct)
|
|
15
|
+
* git checkout -q main → `main` no longer follows `checkout`, so the exemption MISSED, the
|
|
16
|
+
* lookahead concluded the target was not main, and the command was
|
|
17
|
+
* blocked with "this switches to a feature branch and then pulls main
|
|
18
|
+
* into it" — the exact opposite of what it does.
|
|
19
|
+
*
|
|
20
|
+
* That block landed on the cure stale-main-bash-guard itself prescribes, so the two guards
|
|
21
|
+
* contradicted each other and a human had to route around both to update main. The fix is not a
|
|
22
|
+
* cleverer regex: it is to TOKENIZE (CommandScanner already does the hard part) and walk the flags,
|
|
23
|
+
* which is also why it must exist once rather than three times.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT "does the string contain main" — over-matching here is worse than the original
|
|
26
|
+
* bug, because `git checkout -b feature/main-thing` and `git checkout -- main.ts` would then read as
|
|
27
|
+
* "switching to main" and be EXEMPTED from a guard whose whole job is to catch a feature-branch pull.
|
|
28
|
+
*/
|
|
29
|
+
const CREATE_FLAGS = new Set(['-b', '-B', '-c', '-C', '--orphan']);
|
|
30
|
+
// Non-creating flags that consume the FOLLOWING token, so its value is never mistaken for the target.
|
|
31
|
+
const FLAGS_WITH_VALUE = new Set(['--conflict', '--pathspec-from-file', '--start-point']);
|
|
32
|
+
/**
|
|
33
|
+
* The branch a checkout/switch lands on, and whether that command CREATES it.
|
|
34
|
+
*
|
|
35
|
+
* `created` is load-bearing rather than cosmetic: `git checkout -b x origin/main` lands on a branch
|
|
36
|
+
* that is current by construction (nothing to be stale about), whereas landing on an EXISTING local
|
|
37
|
+
* `main` is exactly the stale-checkout hazard. Data-only, so a class (per CLAUDE.md).
|
|
38
|
+
*/
|
|
39
|
+
class BranchSwitch {
|
|
40
|
+
branch;
|
|
41
|
+
created;
|
|
42
|
+
constructor(branch, created) {
|
|
43
|
+
this.branch = branch;
|
|
44
|
+
this.created = created;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.BranchSwitch = BranchSwitch;
|
|
48
|
+
class BranchSwitchScan {
|
|
49
|
+
scanner;
|
|
50
|
+
constructor(scanner = new command_scan_1.CommandScanner()) {
|
|
51
|
+
this.scanner = scanner;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The branch this segment switches to, or null when the segment does not land on a branch at all:
|
|
55
|
+
* a different command, no target word, `git checkout -` (the previous branch — unknowable here),
|
|
56
|
+
* or anything after `--` (which ends option parsing, making the rest PATHSPECS, so
|
|
57
|
+
* `git checkout -- main` restores a FILE named main and moves no branch).
|
|
58
|
+
*
|
|
59
|
+
* Flag-tolerant by construction: every leading flag is walked past, so `-q`, `--quiet`,
|
|
60
|
+
* `--no-track`, `--detach` and friends change nothing about which word is the target.
|
|
61
|
+
*/
|
|
62
|
+
targetOf(segment) {
|
|
63
|
+
const subcommand = this.scanner.gitSubcommand(segment);
|
|
64
|
+
if (subcommand !== 'checkout' && subcommand !== 'switch')
|
|
65
|
+
return null;
|
|
66
|
+
const words = this.scanner.words(segment);
|
|
67
|
+
const args = words.slice(words.indexOf(subcommand) + 1);
|
|
68
|
+
for (let i = 0; i < args.length; i++) {
|
|
69
|
+
const word = args[i];
|
|
70
|
+
if (word === '--')
|
|
71
|
+
return null;
|
|
72
|
+
if (word === '-')
|
|
73
|
+
return null;
|
|
74
|
+
const created = this.createdBranch(args, i);
|
|
75
|
+
if (created !== null)
|
|
76
|
+
return created;
|
|
77
|
+
if (FLAGS_WITH_VALUE.has(word)) {
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (word.startsWith('-'))
|
|
82
|
+
continue;
|
|
83
|
+
return new BranchSwitch(word, false);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* True only for landing on an EXISTING branch named `main` — the form that can hand you a stale
|
|
89
|
+
* tree, and the form `git checkout main && git pull origin main` uses. `-b`/`-B`/`-c`/`-C` are
|
|
90
|
+
* excluded because they create the branch here and now.
|
|
91
|
+
*/
|
|
92
|
+
landsOnExistingMain(segment) {
|
|
93
|
+
const target = this.targetOf(segment);
|
|
94
|
+
return target !== null && this.isExistingMain(target);
|
|
95
|
+
}
|
|
96
|
+
/** The same question for a target already parsed (switchesIn). One spelling, two entry shapes. */
|
|
97
|
+
isExistingMain(target) {
|
|
98
|
+
return target.branch === 'main' && !target.created;
|
|
99
|
+
}
|
|
100
|
+
/** Every segment of a whole command that lands on a branch, in order. */
|
|
101
|
+
switchesIn(command) {
|
|
102
|
+
const found = [];
|
|
103
|
+
for (const segment of this.scanner.commandSegments(command)) {
|
|
104
|
+
const target = this.targetOf(segment);
|
|
105
|
+
if (target !== null)
|
|
106
|
+
found.push(target);
|
|
107
|
+
}
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
// `-b <name>` / `--orphan=<name>` — the new branch's name, or null when this word creates nothing.
|
|
111
|
+
createdBranch(args, i) {
|
|
112
|
+
const word = args[i];
|
|
113
|
+
if (CREATE_FLAGS.has(word)) {
|
|
114
|
+
const name = args[i + 1];
|
|
115
|
+
if (name === undefined || name.startsWith('-'))
|
|
116
|
+
return null;
|
|
117
|
+
return new BranchSwitch(name, true);
|
|
118
|
+
}
|
|
119
|
+
const equals = word.indexOf('=');
|
|
120
|
+
if (equals > 0 && CREATE_FLAGS.has(word.slice(0, equals))) {
|
|
121
|
+
return new BranchSwitch(word.slice(equals + 1), true);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.BranchSwitchScan = BranchSwitchScan;
|
|
127
|
+
//# sourceMappingURL=branch-switch-scan.js.map
|