@webpieces/ai-hook-rules 0.4.725 → 0.4.727
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/hook-app.d.ts +2 -2
- package/src/adapters/hook-app.js +2 -2
- package/src/adapters/hook-app.js.map +1 -1
- package/src/bin/l0-entry.d.ts +1 -1
- package/src/bin/l0-entry.js +1 -1
- package/src/bin/l0-entry.js.map +1 -1
- package/src/core/decision-log.d.ts +2 -1
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-matrix.d.ts +2 -2
- package/src/core/l0-matrix.js +2 -2
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l2-rows.d.ts +1 -1
- package/src/core/l2-rows.js +1 -1
- package/src/core/l2-rows.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.727",
|
|
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.
|
|
28
|
+
"@webpieces/rules-config": "0.4.727",
|
|
29
29
|
"inversify": "7.10.4",
|
|
30
30
|
"reflect-metadata": "0.2.2"
|
|
31
31
|
},
|
|
@@ -15,8 +15,8 @@ import { HookStdinSource, HookStdoutSink, HookProcessExit } from './hook-ports';
|
|
|
15
15
|
*
|
|
16
16
|
* What this replaces: `runMain(mode)`, which read stdin itself and reached `process.stdout.write` /
|
|
17
17
|
* `process.exit` from a dozen frames down. `runMain` is DELETED, not kept alongside — two spellings of
|
|
18
|
-
* one entry point is the shim shape this repo rejects outright (see
|
|
19
|
-
*
|
|
18
|
+
* one entry point is the shim shape this repo rejects outright (see
|
|
19
|
+
* `.claude/rules/no-backwards-compat.md`). Nothing outside this file names it any more.
|
|
20
20
|
*
|
|
21
21
|
* The order of observable effects is unchanged from `runMain`: the invocation's audit line is flushed
|
|
22
22
|
* at the emit boundary inside the pipeline, the decision bytes are written next, and the process exits
|
package/src/adapters/hook-app.js
CHANGED
|
@@ -26,8 +26,8 @@ const CRASH_PREFIX = '[ai-hooks] hook crashed unexpectedly — failing closed: '
|
|
|
26
26
|
*
|
|
27
27
|
* What this replaces: `runMain(mode)`, which read stdin itself and reached `process.stdout.write` /
|
|
28
28
|
* `process.exit` from a dozen frames down. `runMain` is DELETED, not kept alongside — two spellings of
|
|
29
|
-
* one entry point is the shim shape this repo rejects outright (see
|
|
30
|
-
*
|
|
29
|
+
* one entry point is the shim shape this repo rejects outright (see
|
|
30
|
+
* `.claude/rules/no-backwards-compat.md`). Nothing outside this file names it any more.
|
|
31
31
|
*
|
|
32
32
|
* The order of observable effects is unchanged from `runMain`: the invocation's audit line is flushed
|
|
33
33
|
* at the emit boundary inside the pipeline, the decision bytes are written next, and the process exits
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-app.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D,2CAA0C;AAC1C,qDAAyD;AAEzD,6CAAgF;AAChF,+CAA2C;AAE3C,kGAAkG;AAClG,uGAAuG;AACvG,aAAa;AACb,MAAM,YAAY,GAAG,yDAAyD,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEI,IAAM,OAAO,GAAb,MAAM,OAAO;IACC,KAAK,CAAkB;IACvB,MAAM,CAAiB;IACvB,WAAW,CAAkB;IAE9C,YAAY,KAAsB,EAAE,MAAsB,EAAE,WAA4B;QACpF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAc;QACpB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxC,4FAA4F;QAC5F,6FAA6F;QAC7F,6CAA6C;QAC7C,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,KAAK,CAAC,MAAM,CAAC,IAAc;QAC/B,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACpC,OAAO,IAAA,uBAAW,EAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,IAAA,4BAAW,EAAC,IAAI,EAAE,GAAG,YAAY,GAAG,KAAK,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;QAC9E,CAAC;IACL,CAAC;CACJ,CAAA;AA3CY,0BAAO;kBAAP,OAAO;IADnB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAMlB,4BAAe,EAAU,2BAAc,EAAe,4BAAe;GAL/E,OAAO,CA2CnB;AAED;;;;;;;;;;;;GAYG;AACH,MAAa,eAAe;IACxB,8IAA8I;IAC9I,MAAM,CAAC,GAAY;QACf,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,8FAA8F;QAC9F,qFAAqF;QACrF,sPAAsP;QACtP,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAA,yBAAQ,EAAC,IAAI,EAAE,GAAG,YAAY,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/E,mLAAmL;QACnL,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;CACJ;AAXD,0CAWC","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\nimport { runPipeline } from './hook-core';\nimport { denyJson, denyOutcome } from './agent-response';\nimport { HookArgs, HookOutcome } from './hook-outcome';\nimport { HookStdinSource, HookStdoutSink, HookProcessExit } from './hook-ports';\nimport { toError } from '../core/to-error';\n\n// The reason text a crash surfaces to the agent. ONE literal, used by both fail-closed boundaries\n// below and worded identically to `denyForCrash`'s so the audit trail reads the same whichever of them\n// caught it.\nconst CRASH_PREFIX = '[ai-hooks] hook crashed unexpectedly — failing closed: ';\n\n/**\n * THE COMPOSITION ROOT'S APP — one PreToolUse invocation, end to end.\n *\n * Production is three lines in `guards-hook.ts` / `rules-hook.ts`:\n *\n * const container = new Container({ autobind: true });\n * const app = container.get(HookApp);\n * await app.run(new HookArgs('guards'));\n *\n * and a test is the SAME three lines with the ports rebound to doubles — canned stdin, a captured\n * stdout, a recorded exit code. That is the whole difference, and it is the point: the test boundary\n * is cut JUST ABOVE the injection point, so the seam a test drives is the seam production drives.\n *\n * What this replaces: `runMain(mode)`, which read stdin itself and reached `process.stdout.write` /\n * `process.exit` from a dozen frames down. `runMain` is DELETED, not kept alongside — two spellings of\n * one entry point is the shim shape this repo rejects outright (see
|
|
1
|
+
{"version":3,"file":"hook-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-app.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D,2CAA0C;AAC1C,qDAAyD;AAEzD,6CAAgF;AAChF,+CAA2C;AAE3C,kGAAkG;AAClG,uGAAuG;AACvG,aAAa;AACb,MAAM,YAAY,GAAG,yDAAyD,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEI,IAAM,OAAO,GAAb,MAAM,OAAO;IACC,KAAK,CAAkB;IACvB,MAAM,CAAiB;IACvB,WAAW,CAAkB;IAE9C,YAAY,KAAsB,EAAE,MAAsB,EAAE,WAA4B;QACpF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAc;QACpB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxC,4FAA4F;QAC5F,6FAA6F;QAC7F,6CAA6C;QAC7C,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,KAAK,CAAC,MAAM,CAAC,IAAc;QAC/B,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACpC,OAAO,IAAA,uBAAW,EAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,IAAA,4BAAW,EAAC,IAAI,EAAE,GAAG,YAAY,GAAG,KAAK,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;QAC9E,CAAC;IACL,CAAC;CACJ,CAAA;AA3CY,0BAAO;kBAAP,OAAO;IADnB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAMlB,4BAAe,EAAU,2BAAc,EAAe,4BAAe;GAL/E,OAAO,CA2CnB;AAED;;;;;;;;;;;;GAYG;AACH,MAAa,eAAe;IACxB,8IAA8I;IAC9I,MAAM,CAAC,GAAY;QACf,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,8FAA8F;QAC9F,qFAAqF;QACrF,sPAAsP;QACtP,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAA,yBAAQ,EAAC,IAAI,EAAE,GAAG,YAAY,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/E,mLAAmL;QACnL,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;CACJ;AAXD,0CAWC","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\nimport { runPipeline } from './hook-core';\nimport { denyJson, denyOutcome } from './agent-response';\nimport { HookArgs, HookOutcome } from './hook-outcome';\nimport { HookStdinSource, HookStdoutSink, HookProcessExit } from './hook-ports';\nimport { toError } from '../core/to-error';\n\n// The reason text a crash surfaces to the agent. ONE literal, used by both fail-closed boundaries\n// below and worded identically to `denyForCrash`'s so the audit trail reads the same whichever of them\n// caught it.\nconst CRASH_PREFIX = '[ai-hooks] hook crashed unexpectedly — failing closed: ';\n\n/**\n * THE COMPOSITION ROOT'S APP — one PreToolUse invocation, end to end.\n *\n * Production is three lines in `guards-hook.ts` / `rules-hook.ts`:\n *\n * const container = new Container({ autobind: true });\n * const app = container.get(HookApp);\n * await app.run(new HookArgs('guards'));\n *\n * and a test is the SAME three lines with the ports rebound to doubles — canned stdin, a captured\n * stdout, a recorded exit code. That is the whole difference, and it is the point: the test boundary\n * is cut JUST ABOVE the injection point, so the seam a test drives is the seam production drives.\n *\n * What this replaces: `runMain(mode)`, which read stdin itself and reached `process.stdout.write` /\n * `process.exit` from a dozen frames down. `runMain` is DELETED, not kept alongside — two spellings of\n * one entry point is the shim shape this repo rejects outright (see\n * `.claude/rules/no-backwards-compat.md`). Nothing outside this file names it any more.\n *\n * The order of observable effects is unchanged from `runMain`: the invocation's audit line is flushed\n * at the emit boundary inside the pipeline, the decision bytes are written next, and the process exits\n * last through the injected exit port.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class HookApp {\n private readonly stdin: HookStdinSource;\n private readonly stdout: HookStdoutSink;\n private readonly processExit: HookProcessExit;\n\n constructor(stdin: HookStdinSource, stdout: HookStdoutSink, processExit: HookProcessExit) {\n this.stdin = stdin;\n this.stdout = stdout;\n this.processExit = processExit;\n }\n\n async run(args: HookArgs): Promise<void> {\n const outcome = await this.decide(args);\n // An ALLOW writes NOTHING — a silent exit 0 is the allow in the PreToolUse protocol, and an\n // empty write would still be a write on a pipe somebody is parsing. Guarded here rather than\n // in the sink so the sink stays a dumb port.\n if (outcome.stdout !== '') this.stdout.write(outcome.stdout);\n this.processExit.exit(outcome.exitCode);\n }\n\n /**\n * THE FAIL-CLOSED BOUNDARY FOR THE READ ITSELF, and the reason this is a separate method.\n *\n * `runMain` had the stdin read INSIDE the try whose catch produced a deny, so a failure there was\n * still a structured block. Moving the read behind a port would have quietly narrowed that: a\n * rejected read (or anything else thrown before the pipeline starts) would escape `run`, land as an\n * unhandled rejection, and exit non-zero — which PreToolUse reads as a NON-BLOCKING error and lets\n * the tool call THROUGH. That is the exact inversion of \"a broken hook never silently lets an edit\n * through\", and no golden could catch it, because the goldens substitute this very port.\n *\n * So the try is restored one level out, around the read AND the pipeline, and it emits the same\n * bytes `denyForCrash` emits for a null event.\n */\n private async decide(args: HookArgs): Promise<HookOutcome> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = await this.stdin.read();\n return runPipeline(raw, args.mode);\n } catch (err: unknown) {\n const error = toError(err);\n return denyOutcome(null, `${CRASH_PREFIX}${error.message}`, 'hook-crash');\n }\n }\n}\n\n/**\n * THE LAST-RESORT FAIL-CLOSED BOUNDARY: the composition root itself could not run.\n *\n * `new Container(...)` and `container.get(HookApp)` happen BEFORE any HookApp exists to catch for\n * them, so an unresolvable binding (a missing decorator, a stripped `design:paramtypes`) would exit\n * non-zero with a stack on stderr — and a non-zero exit is a non-blocking error, so every guarded tool\n * call would sail through unjudged for as long as the defect lasted. Low probability; total\n * consequence. One shared class rather than a copy in each bin, so the two can never drift.\n *\n * It writes through `process` directly, and that is correct rather than a leak: by construction there\n * is no container here to have handed it a port, and this is the same designated terminal boundary the\n * ports themselves wrap.\n */\nexport class HookBootFailure {\n // webpieces-disable no-any-unknown -- a rejection value is `unknown` by construction; toError() below is the one place that narrowing belongs\n report(err: unknown): void {\n const error = toError(err);\n // `null` event ⇒ no `systemMessage`. Deliberate: we never parsed a payload, so we do not know\n // whether this was a Bash call, and inventing the Bash-shaped deny would be a guess.\n // webpieces-disable no-process-exit-outside-main -- the hook's exit code IS the Claude Code PreToolUse protocol (exit 0 + JSON = a block); this is the last-resort terminal boundary, reached only when no container could be built to inject a port.\n process.stdout.write(denyJson(null, `${CRASH_PREFIX}${error.message}`) + '\\n');\n // webpieces-disable no-process-exit-outside-main -- same terminal boundary; exiting 0 is what makes this a BLOCK rather than a non-blocking error that lets the tool call through.\n process.exit(0);\n }\n}\n"]}
|
package/src/bin/l0-entry.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { AiType } from '../core/agent-event';
|
|
|
13
13
|
/**
|
|
14
14
|
* The token an L0 entry uses to say it serves EVERY harness — SAID OUT LOUD.
|
|
15
15
|
*
|
|
16
|
-
* This used to be `null`, and `null` is shim shape #5 from
|
|
16
|
+
* This used to be `null`, and `null` is shim shape #5 from `.claude/rules/no-backwards-compat.md`: a widening expressed as an
|
|
17
17
|
* ABSENCE. It made the widest setting the shortest thing to type and, worse, impossible to grep —
|
|
18
18
|
* there is no search that lists "the entries every harness gets", because they were identified by
|
|
19
19
|
* a field that was not there. `grep -n EVERY_HARNESS` now lists every one of them.
|
package/src/bin/l0-entry.js
CHANGED
|
@@ -15,7 +15,7 @@ exports.L0AllowEntry = exports.L0Call = exports.EVERY_HARNESS = void 0;
|
|
|
15
15
|
/**
|
|
16
16
|
* The token an L0 entry uses to say it serves EVERY harness — SAID OUT LOUD.
|
|
17
17
|
*
|
|
18
|
-
* This used to be `null`, and `null` is shim shape #5 from
|
|
18
|
+
* This used to be `null`, and `null` is shim shape #5 from `.claude/rules/no-backwards-compat.md`: a widening expressed as an
|
|
19
19
|
* ABSENCE. It made the widest setting the shortest thing to type and, worse, impossible to grep —
|
|
20
20
|
* there is no search that lists "the entries every harness gets", because they were identified by
|
|
21
21
|
* a field that was not there. `grep -n EVERY_HARNESS` now lists every one of them.
|
package/src/bin/l0-entry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l0-entry.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/l0-entry.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAIH;;;;;;;;;GASG;AACU,QAAA,aAAa,GAAG,eAAe,CAAC;AAY7C,uGAAuG;AACvG,MAAa,MAAM;IAEF;IACA;IACA;IAHb,YACa,QAAgB,EAChB,OAAe,EACf,QAAgB;QAFhB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,YAAO,GAAP,OAAO,CAAQ;QACf,aAAQ,GAAR,QAAQ,CAAQ;IAC1B,CAAC;CACP;AAND,wBAMC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAa,YAAY;IAGR;IACA;IAOA;IACA;IACA;IAYA;IACA;IACA;IA1Bb,yDAAyD;IACzD,YACa,KAAa,EACb,IAAsB;IAC/B;;;;;OAKG;IACM,IAAa,EACb,GAAkB,EAClB,EAAiB;IAC1B;;;;;;;;;;OAUG;IACM,OAAkB,EAClB,MAAc,EACd,eAAkC,EAAE;QAxBpC,UAAK,GAAL,KAAK,CAAQ;QACb,SAAI,GAAJ,IAAI,CAAkB;QAOtB,SAAI,GAAJ,IAAI,CAAS;QACb,QAAG,GAAH,GAAG,CAAe;QAClB,OAAE,GAAF,EAAE,CAAe;QAYjB,YAAO,GAAP,OAAO,CAAW;QAClB,WAAM,GAAN,MAAM,CAAQ;QACd,iBAAY,GAAZ,YAAY,CAAwB;IAC9C,CAAC;IAEJ,kFAAkF;IAClF,UAAU;QACN,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;IAC/C,CAAC;CACJ;AAlCD,oCAkCC","sourcesContent":["/**\n * THE L0 ENTRY TYPES — the shape of one allowlist entry, split out of ./l0-allowlist.\n *\n * Same reason `./l0-codex-read` and `./l0-ignored-tools` are their own modules: the LIST is long and\n * every entry carries a paragraph of reasoning, so the types that describe an entry get crowded out\n * by the entries themselves. Nothing here knows what is on the list; `./l0-allowlist` owns that and\n * imports these, so the dependency runs one way and there is no cycle.\n *\n * NOT re-exported from here onward — `./shim` is the ONE name that gathers L0, and it stars this\n * module alongside the others.\n */\n\nimport { AiType } from '../core/agent-event';\n\n/**\n * The token an L0 entry uses to say it serves EVERY harness — SAID OUT LOUD.\n *\n * This used to be `null`, and `null` is shim shape #5 from
|
|
1
|
+
{"version":3,"file":"l0-entry.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/l0-entry.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAIH;;;;;;;;;GASG;AACU,QAAA,aAAa,GAAG,eAAe,CAAC;AAY7C,uGAAuG;AACvG,MAAa,MAAM;IAEF;IACA;IACA;IAHb,YACa,QAAgB,EAChB,OAAe,EACf,QAAgB;QAFhB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,YAAO,GAAP,OAAO,CAAQ;QACf,aAAQ,GAAR,QAAQ,CAAQ;IAC1B,CAAC;CACP;AAND,wBAMC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAa,YAAY;IAGR;IACA;IAOA;IACA;IACA;IAYA;IACA;IACA;IA1Bb,yDAAyD;IACzD,YACa,KAAa,EACb,IAAsB;IAC/B;;;;;OAKG;IACM,IAAa,EACb,GAAkB,EAClB,EAAiB;IAC1B;;;;;;;;;;OAUG;IACM,OAAkB,EAClB,MAAc,EACd,eAAkC,EAAE;QAxBpC,UAAK,GAAL,KAAK,CAAQ;QACb,SAAI,GAAJ,IAAI,CAAkB;QAOtB,SAAI,GAAJ,IAAI,CAAS;QACb,QAAG,GAAH,GAAG,CAAe;QAClB,OAAE,GAAF,EAAE,CAAe;QAYjB,YAAO,GAAP,OAAO,CAAW;QAClB,WAAM,GAAN,MAAM,CAAQ;QACd,iBAAY,GAAZ,YAAY,CAAwB;IAC9C,CAAC;IAEJ,kFAAkF;IAClF,UAAU;QACN,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;IAC/C,CAAC;CACJ;AAlCD,oCAkCC","sourcesContent":["/**\n * THE L0 ENTRY TYPES — the shape of one allowlist entry, split out of ./l0-allowlist.\n *\n * Same reason `./l0-codex-read` and `./l0-ignored-tools` are their own modules: the LIST is long and\n * every entry carries a paragraph of reasoning, so the types that describe an entry get crowded out\n * by the entries themselves. Nothing here knows what is on the list; `./l0-allowlist` owns that and\n * imports these, so the dependency runs one way and there is no cycle.\n *\n * NOT re-exported from here onward — `./shim` is the ONE name that gathers L0, and it stars this\n * module alongside the others.\n */\n\nimport { AiType } from '../core/agent-event';\n\n/**\n * The token an L0 entry uses to say it serves EVERY harness — SAID OUT LOUD.\n *\n * This used to be `null`, and `null` is shim shape #5 from `.claude/rules/no-backwards-compat.md`: a widening expressed as an\n * ABSENCE. It made the widest setting the shortest thing to type and, worse, impossible to grep —\n * there is no search that lists \"the entries every harness gets\", because they were identified by\n * a field that was not there. `grep -n EVERY_HARNESS` now lists every one of them.\n *\n * It is a REAL VALUE in exactly the sense `AI_TYPE_UNKNOWN` is: countable, greppable, and typed.\n */\nexport const EVERY_HARNESS = 'every-harness';\n\n/**\n * Who one L0 entry is FOR: exactly one harness, or — named, never implied — all of them.\n *\n * Deliberately NOT `readonly AiType[]`. A list re-opens the hole this closed from the other side:\n * `[]` would be a second way to write \"nobody\", and `['claude-code', 'codex']` a second way to write\n * EVERY_HARNESS — two spellings of one decision, which is shim shape #1. Dean's rule is the whole\n * type: we only support codex or claudecode or FAIL.\n */\nexport type L0Harness = AiType | typeof EVERY_HARNESS;\n\n/** One tool call as L0 judges it: the tool name, the Bash command (or ''), the file target (or ''). */\nexport class L0Call {\n constructor(\n readonly toolName: string,\n readonly command: string,\n readonly filePath: string,\n ) {}\n}\n\n/**\n * One entry of THE L0 allowlist. Data-only → a class, per CLAUDE.md.\n *\n * `ere`/`js` are the twin regex BODIES for a Bash entry, or null for a tool-shaped entry (Read, the\n * webpieces.config.json target) that no regex can express. `sample` is a call this entry must accept —\n * it is what the matrix-coverage and cure-reachability tests drive isAllowed() with.\n *\n * `extraSamples` pins ADDITIONAL spellings the same entry must accept. A spelling that some deny\n * message prescribes belongs here, or nothing stops a later tightening of the pattern from making that\n * message's cure untypable again — which is the deadlock shape this whole module exists to prevent.\n *\n * `cure` is the ONE thing that is not uniform across the list, and it is not about L0 at all — see\n * L0_CURE_ALLOW_JS below. Every entry is judged identically while an L0 fault is up; `cure` decides\n * only whether the entry ALSO bypasses the downstream (L1) guards on a HEALTHY tree.\n */\nexport class L0AllowEntry {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly label: string,\n readonly kind: 'pass' | 'allow',\n /**\n * True when this entry REPAIRS the tooling (install, sync, shim restore). A cure has to run\n * before webpieces.config.json can even be loaded, so it bypasses everything, always. A\n * non-cure (read-only orientation) is allowed while a fault is up and is otherwise an ordinary\n * command the downstream guards still judge.\n */\n readonly cure: boolean,\n readonly ere: string | null,\n readonly js: string | null,\n /**\n * The ONE harness this entry exists for, or `EVERY_HARNESS` for the entries every harness gets.\n * There is no default and no absence: an entry must say which it is, or it does not compile.\n *\n * A gated entry is unreachable from any other harness — it is spliced into its own union, which\n * both halves of L0 consult only after answering \"which harness sent this call?\". It exists\n * because the two harnesses do not have the same TOOLS: `Read` is entry 1 for Claude Code and\n * Codex has no such tool, so read parity at L0 can only be expressed per harness. Anything added\n * here later must satisfy the property stated on AI_TYPE_SH: the sh half's answer is an\n * approximation, so a gated entry may never grant more than the OTHER harness already has.\n */\n readonly harness: L0Harness,\n readonly sample: L0Call,\n readonly extraSamples: readonly L0Call[] = [],\n ) {}\n\n /** Every call this entry pins: the canonical sample plus every extra spelling. */\n allSamples(): readonly L0Call[] {\n return [this.sample, ...this.extraSamples];\n }\n}\n\n\n"]}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* BLOCK_HUMAN blocked, and it needs a human decision — or a delegation (spawn a subagent) that
|
|
19
19
|
* the blocked agent cannot perform for itself.
|
|
20
20
|
*
|
|
21
|
-
* Hard cut, per
|
|
21
|
+
* Hard cut, per `.claude/rules/no-backwards-compat.md`: `'BLOCK'` is GONE rather than aliased, so every
|
|
22
|
+
* construction site fails to
|
|
22
23
|
* compile and has to say which kind of block it is. Before, that question had exactly one wrong
|
|
23
24
|
* answer available — silence.
|
|
24
25
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder, claudeEnv } from '@webpieces/rules-config';\nimport { L1_LOCATION_STREAM, L2_DECISIONS_STREAM, CALLS_STREAM } from './log-streams';\n\nimport { L0_FAULT_NONE, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED } from './l0-fault-codes';\nimport { toError } from './to-error';\nimport { logStream } from './log-stream';\nimport { aiTypeContext } from './ai-type-context';\nimport { l2RowForReason } from './l2-rows';\nimport { cureForMatrix } from './matrix-cures';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (the `async-refresh/` stream, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/logs/L2-decisions/<writer>.log` —\n// the LAYER is the directory, the WRITER is the file (see log-streams.ts and LogStream).\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\n/**\n * THE ACTION CODEBOOK, as a type. These are the five actions GUARD_MATRIX.md numbers 1-5, and they\n * are the vocabulary EVERY layer reports in — so one grep spans L-1, L0, L1 and L2.\n *\n * The three distinctions this exists to make, none of which `'ALLOW' | 'BLOCK'` could:\n *\n * ALLOW no objection — the call was HANDED DOWN to the next layer. A layer saying ALLOW\n * is NOT saying the call ran: the layer below it, or the OTHER parallel hook, may\n * still deny. This is L1's `ACT_DOWN`.\n * ALLOW_EXEMPT out of scope by construction — allowed, and evaluation STOPS here. L1's\n * `ACT_EXEMPT`.\n * ALLOW_FAIL_OPEN state could not be established, so nothing was judged. Keeping this distinct\n * from ALLOW is the entire point of the type: a fail-open allow and a real allow\n * that look identical make it impossible to tell whether the guards are protecting\n * anything or quietly abstaining. It used to be a `' (fail-open)'` SUBSTRING on the\n * reason field, which is exactly why the abstentions were never countable.\n * BLOCK_AI_CURE blocked, and the printed cure is a command the AI can run itself.\n * BLOCK_HUMAN blocked, and it needs a human decision — or a delegation (spawn a subagent) that\n * the blocked agent cannot perform for itself.\n *\n * Hard cut, per CLAUDE.md: `'BLOCK'` is GONE rather than aliased, so every construction site fails to\n * compile and has to say which kind of block it is. Before, that question had exactly one wrong\n * answer available — silence.\n */\nexport type Verdict = 'ALLOW' | 'ALLOW_EXEMPT' | 'ALLOW_FAIL_OPEN' | 'BLOCK_AI_CURE' | 'BLOCK_HUMAN';\n\n/**\n * WHICH ROW of WHICH layer's decision table produced this line. Data-only → a class, per CLAUDE.md.\n *\n * `row` is the row NUMBER from the layer's row array (`L1_ROWS[i].num`, `L2_ROWS[i].num`) — the\n * same number the generated doc prints, because the doc is rendered from that same array. So a log\n * line joins to its matrix row BY NUMBER, and checking observed behaviour against the documented use\n * cases becomes a lookup rather than an investigation.\n */\nexport class MatrixRef {\n constructor(readonly layer: string, readonly row: string) {}\n}\n\n/**\n * The layer tokens. EVERY layer now cites a row: L0 through its two decision-matrix rows, L1 through\n * L1_ROWS, and L2 through L2_ROWS (see `matrixL2Row`). This used to say `'-'` was for \"a layer with no\n * row array YET (L2 is the un-converted one)\" — L2 is converted, and a comment describing a state the\n * code left behind is exactly the kind of doc a reader trusts and should not.\n *\n * `'-'` survives for ONE case, and it is a real one: an L2 reason that no row claims. See matrixL2Row.\n *\n * These are REQUIRED at the constructor, not defaulted: a defaulted `MatrixRef` would make the\n * uncited case reachable by doing nothing and impossible to grep — the same defect this file's own\n * docblock argues against for `'BLOCK'`, where silence was the one wrong answer available.\n *\n * L0 NOW CITES ITS ROW TOO. It used to be `'-'` on the grounds that \"L0's faults are a table of letters\n * rather than numbered rows\" — but L0 has BOTH: the letter says WHICH fault, and the numbered decision\n * matrix (`renderGuardMatrixDoc`) says which of its three rows was taken. There is one ALLOW row and one\n * BLOCK row, so the pair `row=` + `fault=` pins the decision exactly, and the deny an agent reads now\n * carries the identical pair. That is the join: one grep spans the deny, the log line and the doc.\n * Two constants, not one `MATRIX_L0`, because the two call sites are a cure-bypass ALLOW and an L0\n * BLOCK — one token covering both is what made the row unciteable in the first place.\n */\nexport const MATRIX_L0_ALLOW = new MatrixRef('L0', L0_ROW_ALLOWLISTED);\nexport const MATRIX_L0_BLOCK = new MatrixRef('L0', L0_ROW_BLOCKED);\n\n/**\n * The L2 reference for one decision, with the row DERIVED FROM THE REASON.\n *\n * There is no `MATRIX_L2` constant any more, and its absence is the point: a single shared instance\n * meant every L2 line in the repo carried `row=-`, which reads as \"L2 has no rows\" rather than \"this\n * decision was not classified\". Deleting it makes every construction site name a reason, and the reason\n * is the only thing a call site has that identifies which row it is an instance of.\n *\n * `l2RowForReason` returns null for a reason no row claims, and that renders as `'-'` — visible in the\n * log, not silently absorbed into a default row. l2-matrix.spec.ts reads the four guard sources and\n * fails the build if any reason literal in them is unmapped, so `-` should never appear in practice;\n * when it does, it is a genuine hole in the table and the log says so.\n */\n// webpieces-disable no-function-outside-class -- a named constructor for MatrixRef beside it, in this module of module-scope writers\nexport function matrixL2Row(reason: string): MatrixRef {\n const row = l2RowForReason(reason);\n return new MatrixRef('L2', row === null ? '-' : String(row));\n}\n\n/**\n * The L2 stream, with NO row — for the two kinds of line that genuinely are not an instance of a row.\n *\n * 1. The runner's AGGREGATE bash lines (\"no bash-guard block\" / \"bash-guard block\"). They summarise\n * the whole guard set's answer for one command, not one row's verdict; the per-guard lines that\n * DO cite rows are written alongside them by the guards themselves.\n * 2. `whole-repo-build-guard`, which is not a branch-state policy at all — it is the experimental\n * home-config guard, has no webpieces.config.json entry, and shares this stream only because the\n * stream is \"bash decisions\", not \"L2 rows\".\n *\n * A NAMED constant rather than an inline `new MatrixRef('L2', '-')`, so `grep MATRIX_L2_UNROWED` lists\n * every uncited line and the list stays short and arguable. It is deliberately NOT called `MATRIX_L2`:\n * the old name was used by everything and made \"L2 has no rows\" indistinguishable from \"this line is\n * not a row\".\n */\nexport const MATRIX_L2_UNROWED = new MatrixRef('L2', '-');\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n /**\n * The L0 fault this decision IS, in the codebook's letter (core/l0-fault-codes.ts), or `-` for an\n * ordinary rule decision. The `sh` shim has always stamped `fault=` on its own stream; the three\n * JS-side faults (S/C/Y) reached this one with no label at all, so `grep fault=S` found nothing\n * even while an S storm was blocking every call.\n */\n fault: string;\n /** Which layer + row decided this. See MatrixRef — it is what joins a log line to the doc. */\n matrix: MatrixRef;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-', fault: string, matrix: MatrixRef) {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n this.fault = fault;\n this.matrix = matrix;\n }\n}\n\n/**\n * Append one tab-separated line per L2 decision to `.webpieces/logs/L2-decisions/<writer>.log`, where\n * <writer> is LogStream's `<sessionId>-<agentId|coordinator>-<hook>` key (a caller that never\n * identified renders as `unknown-coordinator-hook` — there is no un-keyed name).\n * `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\n// webpieces-disable no-function-outside-class -- the module-scope writer this log has always been, beside branchForLog/oneLine/rotateLogFile; it must stay callable from a tree too broken to build a DI container\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n appendDecision(root, L2_DECISIONS_STREAM, decision);\n}\n\n/**\n * The L1 stream — `.webpieces/logs/L1-location/<writer>.log`.\n *\n * L1 had NO stream. Its three blocking paths wrote into L2's file under an implementation name\n * (`force-to-root`, `trinary-version-skew`, `cd-must-be-first`), and its NON-blocking outcomes —\n * the exempt row and the three hand-down rows — wrote nothing at all. So \"L1 had no objection\" was\n * unobservable, and \"show me every L1 decision\" had no answer: L1 existed in the trail only as the\n * `root=` / `projectDir=` / `tree=` columns stapled onto somebody else's line.\n *\n * A SIBLING rather than a `base` parameter on logGuardDecision, deliberately: that signature is what\n * the process-wide `logStream` singleton exists to keep unchanged (see LogStream's docblock), and\n * `INVOCATION_LOG_FILE` already establishes the pattern of a second stream owning its own name in\n * this same module.\n */\n// webpieces-disable no-function-outside-class -- sibling of logGuardDecision, same module-scope writer shape and same reason\nexport function logL1Decision(root: string, decision: GuardDecision): void {\n appendDecision(root, L1_LOCATION_STREAM, decision);\n}\n\n// The one appender both streams share. `streamDir` is the LAYER; the writer key inside it is\n// logStream's session/agent/hook, which is what keeps one writer per file.\n// webpieces-disable no-function-outside-class -- the shared body of the two module-scope writers above\nfunction appendDecision(root: string, streamDir: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged. WHO made the call is answered\n // by the filename, which logStream names with session/agent/hook.\n const logsDir = dotWebpieces.logsFile(root, streamDir);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n // WHICH ROW of WHICH table decided this. The directory already carries the layer, but a\n // line quoted out of its file must still say what judged it — and `row=` is the join key\n // to the generated doc, which is the point of the whole exercise.\n `layer=${decision.matrix.layer}`,\n `row=${decision.matrix.row}`,\n // The tree this decision was actually made against, and what Claude Code told the hook the\n // project was. Appended (never reordered) for the same reason as on the invocation line —\n // see ClaudeEnv: when these two disagree, that disagreement is the bug.\n `root=${root}`,\n `projectDir=${claudeEnv.projectDirForLog()}`,\n // git's name for that tree — `primary`, else the worktree name. Same literal and same\n // derivation as the L0 shim log's `tree=` (shim-audit-log.ts), so one grep spans both\n // streams: L0 carries tree without projectDir, L1 now carries both.\n `tree=${dotWebpieces.worktreeName(root) || 'primary'}`,\n // APPEND-ONLY, same spelling as the invocation line and the L0 shim log: which L0 fault\n // this was, or `-`.\n `fault=${decision.fault}`,\n // WHAT THE AGENT WAS TOLD TO DO — looked up from the row above, never passed in, so it is\n // by construction the same literal the generated matrix prints for that row (see\n // matrix-cures.ts). `row=` says which row judged the call; this says what that row\n // prescribed, which is what makes the trail auditable against the doc without opening it.\n // APPEND-ONLY, like every field before it.\n `cure=${oneLine(cureForMatrix(decision.matrix.layer, decision.matrix.row))}`,\n // WHICH HARNESS made the call, spelled exactly as the L0 sh shim spells it on its own\n // stream — so ONE grep (`ai=codex`) spans all five streams and \"is Codex actually being\n // guarded?\" is a question the trail can answer. APPEND-ONLY, like every field before it; a\n // row from a release that predates this carries no `ai=` and reads as `unknown`.\n `ai=${aiTypeContext.forLog()}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * What the guard SAW on one invocation, captured up front and held until the outcome is known.\n * Data-only (per CLAUDE.md: classes for data, explicit construction).\n */\nexport class GuardInvocation {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n public readonly root: string,\n public readonly timestamp: string,\n public readonly tool: string,\n public readonly target: string,\n public readonly branch: string,\n public readonly sync: string,\n public readonly projectDir: string,\n ) {}\n}\n\n/**\n * The per-INVOCATION stream — `.webpieces/logs/calls/<writer>.log` (see LogStream for the writer\n * key), one line for EVERY guards-hook\n * call (allow or block, bash or file), unlike `L2-decisions/` which records only the calls a\n * rule actually judged. It captures the tool, the command/file, the live git branch, the async-written\n * main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class\n * replaced a bare log-and-forget function — HOW THE CALL ENDED.\n *\n * WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry\n * a verdict: the decision had not been made yet. Answering \"what happened to this call?\" therefore\n * meant joining this file against the L2 decision stream BY TIMESTAMP, which is exactly the kind of\n * reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the\n * git/cache reads, which must still happen while the hook is running), and {@link finish} — called\n * from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the\n * outcome is known. The two streams stay distinct in purpose: this one is \"every call and how it\n * ended\", the decision log remains \"every judgement and why\".\n *\n * Every error is swallowed: logging must never block or fail a hook.\n */\nexport class InvocationLog {\n private pending: GuardInvocation | null = null;\n\n /**\n * Capture the context of one invocation. `cwd` is the AI's working dir; the repo root that owns\n * `.webpieces` is resolved from it. Writes NOTHING — {@link finish} does that.\n */\n begin(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing\n // on. 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n this.pending = new GuardInvocation(root, new Date().toISOString(), tool, oneLine(target), branch, sync, claudeEnv.projectDirForLog());\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Write the captured line, now stamped with the outcome. A no-op when nothing was captured (the\n * 'rules' hook, or a terminal boundary reached before begin()), and it clears the pending entry so\n * a second emit cannot double-log.\n *\n * `rule` is the rule that blocked, or '-' when there is none; `fault` is the L0 fault code when this\n * call ended on one (S/C/Y — the JS-side faults), else '-'. FIELD ORDER IS APPEND-ONLY: the five\n * original fields keep their positions (cleanup automation mines this file), and `guards=` /\n * `rule=` / … / `fault=` are added at the end.\n */\n finish(verdict: Verdict, rule: string, fault: string = L0_FAULT_NONE): void {\n const invocation = this.pending;\n this.pending = null;\n if (invocation === null) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const logsDir = dotWebpieces.logsFile(invocation.root, CALLS_STREAM);\n fs.mkdirSync(logsDir, { recursive: true });\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${invocation.timestamp}]`,\n invocation.tool,\n invocation.target,\n `branch=${invocation.branch}`,\n invocation.sync,\n // `guards=`, NOT `verdict=`. This hook can only report on ITSELF. Claude Code runs all\n // its PreToolUse hooks IN PARALLEL, so another hook process may deny a call this one\n // had no objection to, and neither can see the other's answer. Measured under the\n // RETIRED three-hook form: `cd <repo>/packages && ls` was DENIED by L-1 and recorded\n // here three times as `verdict=ALLOW`. The old field name promised an outcome it\n // structurally cannot know, so the name stayed even though L-1 is gone: the two\n // surviving hooks still run in parallel and still cannot see each other.\n `guards=${verdict}`,\n `rule=${oneLine(rule) || '-'}`,\n // The tree the guard ACTED in, next to what Claude Code said the project was. Both, on\n // every line, because the diagnostic value is entirely in comparing them — see\n // ClaudeEnv for the open question this field exists to settle empirically.\n `root=${invocation.root}`,\n `projectDir=${invocation.projectDir}`,\n // See logGuardDecision: the short tree label, so `tree=primary` with a matching\n // projectDir reads as healthy at a glance and `tree=<worktree>` beside a projectDir\n // pointing at the primary is the straddle, without diffing two absolute paths.\n `tree=${dotWebpieces.worktreeName(invocation.root) || 'primary'}`,\n // WHICH L0 fault ended this call, in the same letters and the same field name the L0 sh\n // shim uses (the `L0-shim/` stream) — so ONE grep spans the whole trail.\n `fault=${fault}`,\n // WHICH HARNESS made the call — same field name and same vocabulary as the L0 sh shim's\n // `ai=` and the decision stream's, so one grep spans the whole trail. APPEND-ONLY.\n `ai=${aiTypeContext.forLog()}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Process-wide instance: one hook process handles exactly one tool call, so a single pending entry is\n// the whole state there is. Module-scope (rather than DI) because the terminal boundary that flushes\n// it — emitAllow/emitDeny — is itself module-scope protocol code with no container in reach.\nexport const invocationLog = new InvocationLog();\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder, claudeEnv } from '@webpieces/rules-config';\nimport { L1_LOCATION_STREAM, L2_DECISIONS_STREAM, CALLS_STREAM } from './log-streams';\n\nimport { L0_FAULT_NONE, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED } from './l0-fault-codes';\nimport { toError } from './to-error';\nimport { logStream } from './log-stream';\nimport { aiTypeContext } from './ai-type-context';\nimport { l2RowForReason } from './l2-rows';\nimport { cureForMatrix } from './matrix-cures';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (the `async-refresh/` stream, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/logs/L2-decisions/<writer>.log` —\n// the LAYER is the directory, the WRITER is the file (see log-streams.ts and LogStream).\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\n/**\n * THE ACTION CODEBOOK, as a type. These are the five actions GUARD_MATRIX.md numbers 1-5, and they\n * are the vocabulary EVERY layer reports in — so one grep spans L-1, L0, L1 and L2.\n *\n * The three distinctions this exists to make, none of which `'ALLOW' | 'BLOCK'` could:\n *\n * ALLOW no objection — the call was HANDED DOWN to the next layer. A layer saying ALLOW\n * is NOT saying the call ran: the layer below it, or the OTHER parallel hook, may\n * still deny. This is L1's `ACT_DOWN`.\n * ALLOW_EXEMPT out of scope by construction — allowed, and evaluation STOPS here. L1's\n * `ACT_EXEMPT`.\n * ALLOW_FAIL_OPEN state could not be established, so nothing was judged. Keeping this distinct\n * from ALLOW is the entire point of the type: a fail-open allow and a real allow\n * that look identical make it impossible to tell whether the guards are protecting\n * anything or quietly abstaining. It used to be a `' (fail-open)'` SUBSTRING on the\n * reason field, which is exactly why the abstentions were never countable.\n * BLOCK_AI_CURE blocked, and the printed cure is a command the AI can run itself.\n * BLOCK_HUMAN blocked, and it needs a human decision — or a delegation (spawn a subagent) that\n * the blocked agent cannot perform for itself.\n *\n * Hard cut, per `.claude/rules/no-backwards-compat.md`: `'BLOCK'` is GONE rather than aliased, so every\n * construction site fails to\n * compile and has to say which kind of block it is. Before, that question had exactly one wrong\n * answer available — silence.\n */\nexport type Verdict = 'ALLOW' | 'ALLOW_EXEMPT' | 'ALLOW_FAIL_OPEN' | 'BLOCK_AI_CURE' | 'BLOCK_HUMAN';\n\n/**\n * WHICH ROW of WHICH layer's decision table produced this line. Data-only → a class, per CLAUDE.md.\n *\n * `row` is the row NUMBER from the layer's row array (`L1_ROWS[i].num`, `L2_ROWS[i].num`) — the\n * same number the generated doc prints, because the doc is rendered from that same array. So a log\n * line joins to its matrix row BY NUMBER, and checking observed behaviour against the documented use\n * cases becomes a lookup rather than an investigation.\n */\nexport class MatrixRef {\n constructor(readonly layer: string, readonly row: string) {}\n}\n\n/**\n * The layer tokens. EVERY layer now cites a row: L0 through its two decision-matrix rows, L1 through\n * L1_ROWS, and L2 through L2_ROWS (see `matrixL2Row`). This used to say `'-'` was for \"a layer with no\n * row array YET (L2 is the un-converted one)\" — L2 is converted, and a comment describing a state the\n * code left behind is exactly the kind of doc a reader trusts and should not.\n *\n * `'-'` survives for ONE case, and it is a real one: an L2 reason that no row claims. See matrixL2Row.\n *\n * These are REQUIRED at the constructor, not defaulted: a defaulted `MatrixRef` would make the\n * uncited case reachable by doing nothing and impossible to grep — the same defect this file's own\n * docblock argues against for `'BLOCK'`, where silence was the one wrong answer available.\n *\n * L0 NOW CITES ITS ROW TOO. It used to be `'-'` on the grounds that \"L0's faults are a table of letters\n * rather than numbered rows\" — but L0 has BOTH: the letter says WHICH fault, and the numbered decision\n * matrix (`renderGuardMatrixDoc`) says which of its three rows was taken. There is one ALLOW row and one\n * BLOCK row, so the pair `row=` + `fault=` pins the decision exactly, and the deny an agent reads now\n * carries the identical pair. That is the join: one grep spans the deny, the log line and the doc.\n * Two constants, not one `MATRIX_L0`, because the two call sites are a cure-bypass ALLOW and an L0\n * BLOCK — one token covering both is what made the row unciteable in the first place.\n */\nexport const MATRIX_L0_ALLOW = new MatrixRef('L0', L0_ROW_ALLOWLISTED);\nexport const MATRIX_L0_BLOCK = new MatrixRef('L0', L0_ROW_BLOCKED);\n\n/**\n * The L2 reference for one decision, with the row DERIVED FROM THE REASON.\n *\n * There is no `MATRIX_L2` constant any more, and its absence is the point: a single shared instance\n * meant every L2 line in the repo carried `row=-`, which reads as \"L2 has no rows\" rather than \"this\n * decision was not classified\". Deleting it makes every construction site name a reason, and the reason\n * is the only thing a call site has that identifies which row it is an instance of.\n *\n * `l2RowForReason` returns null for a reason no row claims, and that renders as `'-'` — visible in the\n * log, not silently absorbed into a default row. l2-matrix.spec.ts reads the four guard sources and\n * fails the build if any reason literal in them is unmapped, so `-` should never appear in practice;\n * when it does, it is a genuine hole in the table and the log says so.\n */\n// webpieces-disable no-function-outside-class -- a named constructor for MatrixRef beside it, in this module of module-scope writers\nexport function matrixL2Row(reason: string): MatrixRef {\n const row = l2RowForReason(reason);\n return new MatrixRef('L2', row === null ? '-' : String(row));\n}\n\n/**\n * The L2 stream, with NO row — for the two kinds of line that genuinely are not an instance of a row.\n *\n * 1. The runner's AGGREGATE bash lines (\"no bash-guard block\" / \"bash-guard block\"). They summarise\n * the whole guard set's answer for one command, not one row's verdict; the per-guard lines that\n * DO cite rows are written alongside them by the guards themselves.\n * 2. `whole-repo-build-guard`, which is not a branch-state policy at all — it is the experimental\n * home-config guard, has no webpieces.config.json entry, and shares this stream only because the\n * stream is \"bash decisions\", not \"L2 rows\".\n *\n * A NAMED constant rather than an inline `new MatrixRef('L2', '-')`, so `grep MATRIX_L2_UNROWED` lists\n * every uncited line and the list stays short and arguable. It is deliberately NOT called `MATRIX_L2`:\n * the old name was used by everything and made \"L2 has no rows\" indistinguishable from \"this line is\n * not a row\".\n */\nexport const MATRIX_L2_UNROWED = new MatrixRef('L2', '-');\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n /**\n * The L0 fault this decision IS, in the codebook's letter (core/l0-fault-codes.ts), or `-` for an\n * ordinary rule decision. The `sh` shim has always stamped `fault=` on its own stream; the three\n * JS-side faults (S/C/Y) reached this one with no label at all, so `grep fault=S` found nothing\n * even while an S storm was blocking every call.\n */\n fault: string;\n /** Which layer + row decided this. See MatrixRef — it is what joins a log line to the doc. */\n matrix: MatrixRef;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-', fault: string, matrix: MatrixRef) {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n this.fault = fault;\n this.matrix = matrix;\n }\n}\n\n/**\n * Append one tab-separated line per L2 decision to `.webpieces/logs/L2-decisions/<writer>.log`, where\n * <writer> is LogStream's `<sessionId>-<agentId|coordinator>-<hook>` key (a caller that never\n * identified renders as `unknown-coordinator-hook` — there is no un-keyed name).\n * `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\n// webpieces-disable no-function-outside-class -- the module-scope writer this log has always been, beside branchForLog/oneLine/rotateLogFile; it must stay callable from a tree too broken to build a DI container\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n appendDecision(root, L2_DECISIONS_STREAM, decision);\n}\n\n/**\n * The L1 stream — `.webpieces/logs/L1-location/<writer>.log`.\n *\n * L1 had NO stream. Its three blocking paths wrote into L2's file under an implementation name\n * (`force-to-root`, `trinary-version-skew`, `cd-must-be-first`), and its NON-blocking outcomes —\n * the exempt row and the three hand-down rows — wrote nothing at all. So \"L1 had no objection\" was\n * unobservable, and \"show me every L1 decision\" had no answer: L1 existed in the trail only as the\n * `root=` / `projectDir=` / `tree=` columns stapled onto somebody else's line.\n *\n * A SIBLING rather than a `base` parameter on logGuardDecision, deliberately: that signature is what\n * the process-wide `logStream` singleton exists to keep unchanged (see LogStream's docblock), and\n * `INVOCATION_LOG_FILE` already establishes the pattern of a second stream owning its own name in\n * this same module.\n */\n// webpieces-disable no-function-outside-class -- sibling of logGuardDecision, same module-scope writer shape and same reason\nexport function logL1Decision(root: string, decision: GuardDecision): void {\n appendDecision(root, L1_LOCATION_STREAM, decision);\n}\n\n// The one appender both streams share. `streamDir` is the LAYER; the writer key inside it is\n// logStream's session/agent/hook, which is what keeps one writer per file.\n// webpieces-disable no-function-outside-class -- the shared body of the two module-scope writers above\nfunction appendDecision(root: string, streamDir: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged. WHO made the call is answered\n // by the filename, which logStream names with session/agent/hook.\n const logsDir = dotWebpieces.logsFile(root, streamDir);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n // WHICH ROW of WHICH table decided this. The directory already carries the layer, but a\n // line quoted out of its file must still say what judged it — and `row=` is the join key\n // to the generated doc, which is the point of the whole exercise.\n `layer=${decision.matrix.layer}`,\n `row=${decision.matrix.row}`,\n // The tree this decision was actually made against, and what Claude Code told the hook the\n // project was. Appended (never reordered) for the same reason as on the invocation line —\n // see ClaudeEnv: when these two disagree, that disagreement is the bug.\n `root=${root}`,\n `projectDir=${claudeEnv.projectDirForLog()}`,\n // git's name for that tree — `primary`, else the worktree name. Same literal and same\n // derivation as the L0 shim log's `tree=` (shim-audit-log.ts), so one grep spans both\n // streams: L0 carries tree without projectDir, L1 now carries both.\n `tree=${dotWebpieces.worktreeName(root) || 'primary'}`,\n // APPEND-ONLY, same spelling as the invocation line and the L0 shim log: which L0 fault\n // this was, or `-`.\n `fault=${decision.fault}`,\n // WHAT THE AGENT WAS TOLD TO DO — looked up from the row above, never passed in, so it is\n // by construction the same literal the generated matrix prints for that row (see\n // matrix-cures.ts). `row=` says which row judged the call; this says what that row\n // prescribed, which is what makes the trail auditable against the doc without opening it.\n // APPEND-ONLY, like every field before it.\n `cure=${oneLine(cureForMatrix(decision.matrix.layer, decision.matrix.row))}`,\n // WHICH HARNESS made the call, spelled exactly as the L0 sh shim spells it on its own\n // stream — so ONE grep (`ai=codex`) spans all five streams and \"is Codex actually being\n // guarded?\" is a question the trail can answer. APPEND-ONLY, like every field before it; a\n // row from a release that predates this carries no `ai=` and reads as `unknown`.\n `ai=${aiTypeContext.forLog()}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * What the guard SAW on one invocation, captured up front and held until the outcome is known.\n * Data-only (per CLAUDE.md: classes for data, explicit construction).\n */\nexport class GuardInvocation {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n public readonly root: string,\n public readonly timestamp: string,\n public readonly tool: string,\n public readonly target: string,\n public readonly branch: string,\n public readonly sync: string,\n public readonly projectDir: string,\n ) {}\n}\n\n/**\n * The per-INVOCATION stream — `.webpieces/logs/calls/<writer>.log` (see LogStream for the writer\n * key), one line for EVERY guards-hook\n * call (allow or block, bash or file), unlike `L2-decisions/` which records only the calls a\n * rule actually judged. It captures the tool, the command/file, the live git branch, the async-written\n * main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class\n * replaced a bare log-and-forget function — HOW THE CALL ENDED.\n *\n * WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry\n * a verdict: the decision had not been made yet. Answering \"what happened to this call?\" therefore\n * meant joining this file against the L2 decision stream BY TIMESTAMP, which is exactly the kind of\n * reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the\n * git/cache reads, which must still happen while the hook is running), and {@link finish} — called\n * from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the\n * outcome is known. The two streams stay distinct in purpose: this one is \"every call and how it\n * ended\", the decision log remains \"every judgement and why\".\n *\n * Every error is swallowed: logging must never block or fail a hook.\n */\nexport class InvocationLog {\n private pending: GuardInvocation | null = null;\n\n /**\n * Capture the context of one invocation. `cwd` is the AI's working dir; the repo root that owns\n * `.webpieces` is resolved from it. Writes NOTHING — {@link finish} does that.\n */\n begin(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing\n // on. 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n this.pending = new GuardInvocation(root, new Date().toISOString(), tool, oneLine(target), branch, sync, claudeEnv.projectDirForLog());\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Write the captured line, now stamped with the outcome. A no-op when nothing was captured (the\n * 'rules' hook, or a terminal boundary reached before begin()), and it clears the pending entry so\n * a second emit cannot double-log.\n *\n * `rule` is the rule that blocked, or '-' when there is none; `fault` is the L0 fault code when this\n * call ended on one (S/C/Y — the JS-side faults), else '-'. FIELD ORDER IS APPEND-ONLY: the five\n * original fields keep their positions (cleanup automation mines this file), and `guards=` /\n * `rule=` / … / `fault=` are added at the end.\n */\n finish(verdict: Verdict, rule: string, fault: string = L0_FAULT_NONE): void {\n const invocation = this.pending;\n this.pending = null;\n if (invocation === null) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const logsDir = dotWebpieces.logsFile(invocation.root, CALLS_STREAM);\n fs.mkdirSync(logsDir, { recursive: true });\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${invocation.timestamp}]`,\n invocation.tool,\n invocation.target,\n `branch=${invocation.branch}`,\n invocation.sync,\n // `guards=`, NOT `verdict=`. This hook can only report on ITSELF. Claude Code runs all\n // its PreToolUse hooks IN PARALLEL, so another hook process may deny a call this one\n // had no objection to, and neither can see the other's answer. Measured under the\n // RETIRED three-hook form: `cd <repo>/packages && ls` was DENIED by L-1 and recorded\n // here three times as `verdict=ALLOW`. The old field name promised an outcome it\n // structurally cannot know, so the name stayed even though L-1 is gone: the two\n // surviving hooks still run in parallel and still cannot see each other.\n `guards=${verdict}`,\n `rule=${oneLine(rule) || '-'}`,\n // The tree the guard ACTED in, next to what Claude Code said the project was. Both, on\n // every line, because the diagnostic value is entirely in comparing them — see\n // ClaudeEnv for the open question this field exists to settle empirically.\n `root=${invocation.root}`,\n `projectDir=${invocation.projectDir}`,\n // See logGuardDecision: the short tree label, so `tree=primary` with a matching\n // projectDir reads as healthy at a glance and `tree=<worktree>` beside a projectDir\n // pointing at the primary is the straddle, without diffing two absolute paths.\n `tree=${dotWebpieces.worktreeName(invocation.root) || 'primary'}`,\n // WHICH L0 fault ended this call, in the same letters and the same field name the L0 sh\n // shim uses (the `L0-shim/` stream) — so ONE grep spans the whole trail.\n `fault=${fault}`,\n // WHICH HARNESS made the call — same field name and same vocabulary as the L0 sh shim's\n // `ai=` and the decision stream's, so one grep spans the whole trail. APPEND-ONLY.\n `ai=${aiTypeContext.forLog()}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Process-wide instance: one hook process handles exactly one tool call, so a single pending entry is\n// the whole state there is. Module-scope (rather than DI) because the terminal boundary that flushes\n// it — emitAllow/emitDeny — is itself module-scope protocol code with no container in reach.\nexport const invocationLog = new InvocationLog();\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
package/src/core/l0-matrix.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export declare const GUARD_MATRIX_DOC = "webpieces.guard-matrix.md";
|
|
|
16
16
|
*
|
|
17
17
|
* Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny
|
|
18
18
|
* message must actually name it. That pairing is the anti-deadlock invariant — a message that
|
|
19
|
-
* prescribes a command the allowlist rejects is exactly the shape of the three deadlocks
|
|
20
|
-
* records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.
|
|
19
|
+
* prescribes a command the allowlist rejects is exactly the shape of the three deadlocks
|
|
20
|
+
* `.claude/rules/*.md` records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.
|
|
21
21
|
*/
|
|
22
22
|
export declare class L0Cure {
|
|
23
23
|
readonly mention: string;
|
package/src/core/l0-matrix.js
CHANGED
|
@@ -40,8 +40,8 @@ exports.GUARD_MATRIX_DOC = 'webpieces.guard-matrix.md';
|
|
|
40
40
|
*
|
|
41
41
|
* Both halves are asserted (l0-matrix.spec.ts): the call must be accepted by isAllowed(), and the deny
|
|
42
42
|
* message must actually name it. That pairing is the anti-deadlock invariant — a message that
|
|
43
|
-
* prescribes a command the allowlist rejects is exactly the shape of the three deadlocks
|
|
44
|
-
* records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.
|
|
43
|
+
* prescribes a command the allowlist rejects is exactly the shape of the three deadlocks
|
|
44
|
+
* `.claude/rules/*.md` records, and it is how the dead `wp-setup-ai-hooks` bin in the config-missing text was caught.
|
|
45
45
|
*/
|
|
46
46
|
class L0Cure {
|
|
47
47
|
mention;
|
|
@@ -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, CHECKOUT_MAIN_PULL_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call,\n L0_ALLOWLIST, RECOVERY_CMD, RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, WORKSPACE_MANIFEST,\n renderShim,\n} from '../bin/shim';\nimport { CODEX_READ_STILL_ALLOWED } from '../bin/l0-codex-read';\nimport { ENV_SURFACE, HARNESS_REGISTRATIONS, HarnessRegistration } 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, and ${CODEX_READ_STILL_ALLOWED}`,\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 // The on-main sync is spelled `git checkout main && git pull origin main` and NOT `git pull`:\n // a raw pull on a FEATURE branch merges main into it and destroys the fork point, so it is\n // no longer on the L0 allowlist at all (see CHECKOUT_MAIN_PULL_BODY_ERE). This spelling ends\n // ON main, which is why it is safe from any branch — and it is a no-op checkout when you are\n // already there.\n //\n // And it is RAW GIT on purpose, where the workflow guards now say `pnpm wp-sync-main`\n // instead. The fault being cured here is `node_modules` disagreeing with the pin, so\n // `node_modules` is the untrustworthy thing — and every `pnpm wp-*` bin resolves through it.\n // An L0 cure may never be a command that has to load the package it is repairing. See the\n // long note on CHECKOUT_MAIN_PULL_BODY_ERE in bin/l0-allowlist.ts.\n bashCure(CHECKOUT_MAIN_PULL_CMD, false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'sync 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: a BARE `pnpm install` is not a weaker fix here, it is a PROVABLE\n // no-op, and an agent that trusts the X text will run it until it gives up. See the ADD_HOOK_PKG\n // entry in l0-allowlist.ts for the incident.\n //\n // The ORDER of these three is the correction bought by a second incident. `pnpm add` used to be the\n // preferred cure, and it is the one cure that CANNOT be committed: a direct root dependency on\n // HOOK_PKG violates the umbrella rule (the root manifest depends on the umbrella ALONE), and the\n // message never said to revert it — so it was followed, and the workaround landed in a real\n // package.json. The package normally arrives WITH the umbrella, so a tree that is merely BEHIND is\n // the common cause and syncing is the cure that leaves a committable tree. The add stays last,\n // reachable, and explicitly labelled as a session unblock rather than a fix.\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 [\n bashCure(CHECKOUT_MAIN_PULL_CMD, true,\n `${HOOK_PKG} arrives WITH the umbrella, so a tree that is behind explains this without `\n + 'anything being mis-declared — sync first, then install'),\n bashCure('pnpm install', false,\n 'the sync (or a raised catalog pin in ' + WORKSPACE_MANIFEST + ', which is editable '\n + 'while the block is up) gave the installer something new to do — a BARE install with '\n + 'nothing changed reports \"Lockfile is up to date\" and leaves the tree as broken as it '\n + 'found it'),\n bashCure(ADD_HOOK_PKG_CMD, false,\n 'nothing else worked and you need this session back — it is a session unblock, NOT a '\n + `fix: revert it with 'pnpm remove ${HOOK_PKG}' before committing, because a direct `\n + 'root dependency on it violates the umbrella rule'),\n ],\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, one of the harness hook registrations (.claude/settings.json, .codex/hooks.json) or the 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 EVERY managed surface, and\n // it is still surgical: it rewrites ai-hook.sh, each harness's registration and the 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 EVERY managed surface '\n + '(ai-hook.sh, each harness hook registration, and the Claude settings 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 // The SAMPLE deny renders EVERY managed surface, built from HARNESS_REGISTRATIONS rather than\n // a hand-written trio — a doc that shows a three-surface deny while the guard can report four\n // is exactly the drift this generated-doc arrangement exists to make impossible.\n shimStaleDenyReason('', '', [\n SHIM_MARKER,\n ...HARNESS_REGISTRATIONS.map((h: HarnessRegistration): string => h.registrationSurface),\n ENV_SURFACE,\n ], 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 every git sync; 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. 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 'On an UNGATED entry a leading `cd <dir> &&`, a trailing `2>&1` and a pipe into `tail`/`head` are',\n 'tolerated; nothing else is. **The harness-gated read row tolerates NONE of the three** — not the',\n '`cd` prefix, not the redirect, not the pipe — so `cat f 2>&1` and `cat f | head -20` are both',\n 'rejected. That is deliberate, not an oversight: the row means exactly what',\n '`core/shell-read-parity.ts` means by \"this is a read\", and that module treats a pipe or a redirect',\n 'as proof the command is more than one. Type the bare command.',\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"]}
|
|
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, CHECKOUT_MAIN_PULL_CMD, HOOK_PKG, INSTALL_HOOKS_CMD, L0AllowEntry, L0Call,\n L0_ALLOWLIST, RECOVERY_CMD, RESTORE_SHIM_CMD, SHIM_MARKER, UPGRADE_SHIM_CMD, WORKSPACE_MANIFEST,\n renderShim,\n} from '../bin/shim';\nimport { CODEX_READ_STILL_ALLOWED } from '../bin/l0-codex-read';\nimport { ENV_SURFACE, HARNESS_REGISTRATIONS, HarnessRegistration } 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\n * `.claude/rules/*.md` 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, and ${CODEX_READ_STILL_ALLOWED}`,\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 // The on-main sync is spelled `git checkout main && git pull origin main` and NOT `git pull`:\n // a raw pull on a FEATURE branch merges main into it and destroys the fork point, so it is\n // no longer on the L0 allowlist at all (see CHECKOUT_MAIN_PULL_BODY_ERE). This spelling ends\n // ON main, which is why it is safe from any branch — and it is a no-op checkout when you are\n // already there.\n //\n // And it is RAW GIT on purpose, where the workflow guards now say `pnpm wp-sync-main`\n // instead. The fault being cured here is `node_modules` disagreeing with the pin, so\n // `node_modules` is the untrustworthy thing — and every `pnpm wp-*` bin resolves through it.\n // An L0 cure may never be a command that has to load the package it is repairing. See the\n // long note on CHECKOUT_MAIN_PULL_BODY_ERE in bin/l0-allowlist.ts.\n bashCure(CHECKOUT_MAIN_PULL_CMD, false,\n 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '\n + 'sync 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: a BARE `pnpm install` is not a weaker fix here, it is a PROVABLE\n // no-op, and an agent that trusts the X text will run it until it gives up. See the ADD_HOOK_PKG\n // entry in l0-allowlist.ts for the incident.\n //\n // The ORDER of these three is the correction bought by a second incident. `pnpm add` used to be the\n // preferred cure, and it is the one cure that CANNOT be committed: a direct root dependency on\n // HOOK_PKG violates the umbrella rule (the root manifest depends on the umbrella ALONE), and the\n // message never said to revert it — so it was followed, and the workaround landed in a real\n // package.json. The package normally arrives WITH the umbrella, so a tree that is merely BEHIND is\n // the common cause and syncing is the cure that leaves a committable tree. The add stays last,\n // reachable, and explicitly labelled as a session unblock rather than a fix.\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 [\n bashCure(CHECKOUT_MAIN_PULL_CMD, true,\n `${HOOK_PKG} arrives WITH the umbrella, so a tree that is behind explains this without `\n + 'anything being mis-declared — sync first, then install'),\n bashCure('pnpm install', false,\n 'the sync (or a raised catalog pin in ' + WORKSPACE_MANIFEST + ', which is editable '\n + 'while the block is up) gave the installer something new to do — a BARE install with '\n + 'nothing changed reports \"Lockfile is up to date\" and leaves the tree as broken as it '\n + 'found it'),\n bashCure(ADD_HOOK_PKG_CMD, false,\n 'nothing else worked and you need this session back — it is a session unblock, NOT a '\n + `fix: revert it with 'pnpm remove ${HOOK_PKG}' before committing, because a direct `\n + 'root dependency on it violates the umbrella rule'),\n ],\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, one of the harness hook registrations (.claude/settings.json, .codex/hooks.json) or the 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 EVERY managed surface, and\n // it is still surgical: it rewrites ai-hook.sh, each harness's registration and the 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 EVERY managed surface '\n + '(ai-hook.sh, each harness hook registration, and the Claude settings 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 // The SAMPLE deny renders EVERY managed surface, built from HARNESS_REGISTRATIONS rather than\n // a hand-written trio — a doc that shows a three-surface deny while the guard can report four\n // is exactly the drift this generated-doc arrangement exists to make impossible.\n shimStaleDenyReason('', '', [\n SHIM_MARKER,\n ...HARNESS_REGISTRATIONS.map((h: HarnessRegistration): string => h.registrationSurface),\n ENV_SURFACE,\n ], 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 every git sync; 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. 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 'On an UNGATED entry a leading `cd <dir> &&`, a trailing `2>&1` and a pipe into `tail`/`head` are',\n 'tolerated; nothing else is. **The harness-gated read row tolerates NONE of the three** — not the',\n '`cd` prefix, not the redirect, not the pipe — so `cat f 2>&1` and `cat f | head -20` are both',\n 'rejected. That is deliberate, not an oversight: the row means exactly what',\n '`core/shell-read-parity.ts` means by \"this is a read\", and that module treats a pipe or a redirect',\n 'as proof the command is more than one. Type the bare command.',\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"]}
|
package/src/core/l2-rows.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare const L2_FAIL_OPEN: L2Action;
|
|
|
44
44
|
* `reason` is REQUIRED, and a case that exercises something which is not an L2 row exit says so with
|
|
45
45
|
* `NO_ROW_EXIT` rather than by omitting the argument. An optional field would make opting OUT of the
|
|
46
46
|
* only real enforcement here the shortest thing to type and impossible to grep — the widening-by-absence
|
|
47
|
-
* shape
|
|
47
|
+
* shape `.claude/rules/no-backwards-compat.md` rejects. `grep NO_ROW_EXIT` now lists every unenforced case.
|
|
48
48
|
*/
|
|
49
49
|
export declare class L2UseCase {
|
|
50
50
|
readonly num: number;
|
package/src/core/l2-rows.js
CHANGED
|
@@ -84,7 +84,7 @@ exports.L2_FAIL_OPEN = new L2Action('1 allow (fail-open)', 'fail-open');
|
|
|
84
84
|
* `reason` is REQUIRED, and a case that exercises something which is not an L2 row exit says so with
|
|
85
85
|
* `NO_ROW_EXIT` rather than by omitting the argument. An optional field would make opting OUT of the
|
|
86
86
|
* only real enforcement here the shortest thing to type and impossible to grep — the widening-by-absence
|
|
87
|
-
* shape
|
|
87
|
+
* shape `.claude/rules/no-backwards-compat.md` rejects. `grep NO_ROW_EXIT` now lists every unenforced case.
|
|
88
88
|
*/
|
|
89
89
|
class L2UseCase {
|
|
90
90
|
num;
|
package/src/core/l2-rows.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l2-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l2-rows.ts"],"names":[],"mappings":"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","sourcesContent":["// ---------------------------------------------------------------------------\n// L2 — the BRANCH-STATE layer, as data.\n//\n// L2 answers one question: *may I work here, and is what I read current?* Drawn as a decision matrix\n// that is TWELVE ordered rows plus one terminal fail-open row, first match wins.\n//\n// This module holds those rows, l2-doc.ts renders them into guards/L2-branch-state.md, and a unit test\n// (l2-matrix.spec.ts) locks that file byte-identical to the renderer — the same mechanism that already\n// makes L0's fault table and L1's location table undriftable. Before this existed the L2 doc was 100%\n// hand-written and said so: *\"Until that lands this text is hand-written and can drift.\"*\n//\n// ## HOW L2 JOINS TO THE ROWS TODAY — read this before assuming it works like L1\n//\n// L1 DISPATCHES from its array: `runner.l1LocationBlock` takes the first matching row and switches on\n// its `blockId`, so deleting a row deletes a block. L2 does NOT, and pretending otherwise would be the\n// drift this table exists to remove. The four L2 guard classes each own their own ladder, and those\n// ladders diverge on purpose (guards/L2-branch-state.md, \"Deliberate divergence\": the two Bash guards\n// differ in polarity, quantifier and empty-command handling all at once, so no single parameterised\n// function serves both). Unifying them is where a wrong edit turns an allow into a session-wedging\n// block, so it is deliberately not attempted here.\n//\n// What L2 does instead is a REASON→ROW join. Every exit of every L2 guard already carries a stable\n// reason string into the decision log; `L2_ROW_FOR_REASON` maps each of those to the row it is an\n// instance of, the guards stamp that number as `row=`, and l2-matrix.spec.ts asserts the map is\n// EXHAUSTIVE against the guard sources — a new reason with no row fails the build. So `row=8` in\n// `.webpieces/logs/L2-decisions` opens guards/L2-branch-state.md at row 8 and reads the state, the cure\n// and the tools that row covers, exactly as `row=5` already does for L1.\n//\n// The rows the guards cannot yet honour are named in the doc's \"Not done\" section rather than quietly\n// rendered as if they were live. That section is generated from NOT_DONE below, so it cannot rot.\n//\n// This module is deliberately import-free at runtime, so `pnpm guards:generate` can load it without the\n// package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/** Which tools a row covers. `B` Bash · `R` Read · `E` Write/Edit. */\nexport type L2Tool = 'B' | 'R' | 'E';\n\n/** What L2 does with a row — the same action codebook every layer reports in (GUARD_MATRIX.md). */\nexport type L2ActionKind = 'allow' | 'exempt' | 'block' | 'fail-open';\n\n/**\n * The TERMINAL fail-open row, and the one number in this table that is not from the 1-10 design.\n *\n * Everything in rows 6-10 needs the main-sync cache, and the cache is written by a fire-and-forget\n * refresher that populates it for the NEXT call — so the first tool call of every session has none.\n * \"Stop here and ALLOW\" was written as a DIVIDER in the design table, i.e. as prose between two blocks\n * of rows. Prose cannot be stamped into a log line, and this is the single most frequently taken exit\n * in the whole layer (every session's first call, every unreadable branch, every unreachable forge), so\n * it is a row with a number like any other.\n *\n * It is 11 rather than 6-with-a-renumber because row numbers are IDENTITY here: they are printed in the\n * doc and logged as `row=`, so shifting 6-10 down would silently re-point every reference. The doc\n * prints it in its true position, between rows 5 and 6, with its number shown — same treatment L1 gives\n * row 8, which is printed third and numbered 8.\n */\nexport const L2_FAIL_OPEN_ROW = 11;\n\n/** The `act` cell: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L2Action {\n constructor(readonly label: string, readonly kind: L2ActionKind) {}\n}\n\nexport const L2_ALLOW = new L2Action('1 allow', 'allow');\nexport const L2_EXEMPT = new L2Action('2 exempt', 'exempt');\nexport const L2_BLOCK = new L2Action('4 block', 'block');\nexport const L2_FAIL_OPEN = new L2Action('1 allow (fail-open)', 'fail-open');\n\n/**\n * One row of the \"L2 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * THE POINT OF THIS CLASS is that a use case is added HERE, in code, beside the row it exercises — not\n * into a hand-written doc section that drifts. When a new situation comes up in a session, it becomes\n * one more `new L2UseCase(...)` on the row that judged it, `pnpm guards:generate` re-renders the doc,\n * and the byte-lock spec fails if anyone edits the rendered table instead.\n *\n * The four text fields are rendered VERBATIM. `reason` is the ENFORCEMENT half and is never rendered:\n * it is the exact `reason` string the guard logs for this case, so a spec can push it back through\n * `l2RowForReason` and assert it lands on the row this use case is filed under. That closes the loop\n * the L2 decision log opens — `row=` in the trail, this table on the page, one join between them.\n *\n * `reason` is REQUIRED, and a case that exercises something which is not an L2 row exit says so with\n * `NO_ROW_EXIT` rather than by omitting the argument. An optional field would make opting OUT of the\n * only real enforcement here the shortest thing to type and impossible to grep — the widening-by-absence\n * shape CLAUDE.md rejects. `grep NO_ROW_EXIT` now lists every unenforced case.\n */\nexport class L2UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the reason behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly reason: string,\n ) {}\n}\n\n/**\n * The `reason` for a use case that is NOT an L2 row exit, and so has nothing to join back to.\n *\n * The only legitimate case today is row 3: merge-in-progress is L4's state, and L2 exempts it without\n * logging a reason of its own. Named rather than absent, so \"this case is not enforced\" is a value in\n * the table you can grep for instead of a missing argument nobody notices.\n */\nexport const NO_ROW_EXIT = 'NO_ROW_EXIT (not an L2 row exit — another layer owns this state)';\n\n/**\n * One row of L2's decision table.\n *\n * `cure` is rendered verbatim into the doc and is LITERAL by policy: L0's cure-reachability discipline\n * says a message pointing at documentation for its own remedy cannot be tested, and it caught a fault\n * prescribing a bin that had been renamed away. `—` is the only legal non-command cure, and only on a\n * row that allows.\n */\nexport class L2Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- the five cells of one doc row plus its use cases\n constructor(\n readonly num: number,\n readonly tools: readonly L2Tool[],\n /** The `state` cell, verbatim. */\n readonly state: string,\n readonly action: L2Action,\n /** The `cure` cell, verbatim. `—` when the row allows. */\n readonly cure: string,\n /**\n * The observed situations this row judges. Rendered as the \"L2 use cases\" table.\n *\n * A NON-EMPTY tuple, and required: a row nobody has ever seen fire is either dead or\n * undocumented, and both are worth knowing. Expressing that in the TYPE rather than as a\n * runtime assertion is the JwtRoles pattern — the invariant is enforced at the moment the row\n * is written, which is the only moment that changes what somebody types.\n */\n readonly useCases: readonly [L2UseCase, ...L2UseCase[]],\n ) {}\n\n /** `B R E`, the doc's own spelling of the tool cell. */\n toolCell(): string {\n return this.tools.join(' ');\n }\n}\n\n/**\n * THE THIRTEEN L2 ROWS, in first-match-wins order.\n *\n * Rows 1-5 need NO cache and fire on call #1: rows 1, 2 and 4 are text matches, row 3 is a marker-file\n * scan, row 5 is one `git rev-parse`. Row 11 is the cache divider. Rows 6-10 all read the cache.\n *\n * THE ORDER OF ROW 5 IS THE MOST LOAD-BEARING THING IN THIS TABLE. Put \"on main\" BELOW the divider and\n * WRITES on `main` are permitted for the whole first call of every session — and permanently in a\n * multi-worktree repo, where another tree may hold the cache lock indefinitely.\n *\n * `B` AND `E` PART COMPANY ON `main`, and rows 5/6/7 are where. A WRITE on `main` is wrong at any\n * freshness — the work lands somewhere unreviewable and unrevertable — so row 5 is `E` only, judged on\n * the branch alone, above the divider. A READ or a BUILD on a CURRENT `main` is harmless, and blocking\n * it strands the agent right after `pnpm wp-sync-main` put it there; so `B` joins `R` on the\n * FRESHNESS-gated pair below the divider (row 6 behind → block, row 7 current → allow), where \"cannot\n * tell\" fails open at row 11 by construction. `B` and `R` still differ in SHAPE inside row 6: a Read\n * names one file and is judged precisely, a Bash command is opaque and gets default-deny plus row 4.\n */\nexport const L2_ROWS: readonly L2Row[] = [\n new L2Row(1, ['B', 'R', 'E'], 'on the **global allowlist** (inert command, or a universal cure such as reading/editing `webpieces.config.json`)', L2_ALLOW, '—', [\n new L2UseCase(1,\n 'You are blocked by some other L2 row, and need to turn the policy off to get anything done',\n 'any state — this row is ahead of every block',\n 'ALLOW: reading and editing `webpieces.config.json` is never blocked, so the mode-OFF cure is always reachable',\n 'Edit `webpieces.config.json` → `hookGuards` → `branch-state-guard` → `\"mode\": \"OFF\"`',\n 'webpieces-config-read (escape hatch)'),\n new L2UseCase(2,\n 'A Write to `webpieces.config.json` while on `main`, which row 5 would otherwise block',\n 'on `main`, editing the one file that can disable the guard',\n 'ALLOW: the hook adapter bypasses feature-branch-guard for this path before any guard runs',\n 'None needed — the edit proceeds',\n 'config-bypass (feature-branch-guard skipped)'),\n ]),\n new L2Row(2, ['B'], 'bare `git checkout main`, with no `git pull` chained into the same command', L2_BLOCK, '`pnpm wp-sync-main`', [\n new L2UseCase(3,\n '`git checkout main` after a merge, to start the next piece of work',\n 'about to land on whatever local `main` you last had — 157 commits behind, in the incident',\n 'BLOCK: decided from command TEXT alone, before the checkout, because the only `main` this could measure is the one it is about to leave',\n '`pnpm wp-sync-main` — checkout, pull, reap dead branches/worktrees, sweep orphan directories, in one command (hand-rolled, the pull must be in the SAME command as the checkout)',\n 'bare checkout of main ('),\n new L2UseCase(4,\n 'The same command inside a linked worktree, where `git checkout main` fatals anyway',\n 'linked worktree — `main` is already checked out in the primary clone',\n 'BLOCK, and the message prints the worktree form rather than a cure git would refuse',\n '`git fetch origin main`, then work off `origin/main`',\n 'bare checkout of main ('),\n ]),\n new L2Row(3, ['B', 'R', 'E'], '**merge in progress** — L4 owns this state', L2_EXEMPT, 'finish the merge: `pnpm wp-finish-upsert-pr`', [\n new L2UseCase(5,\n 'Reading and editing conflicted files during a 3-point merge, on a branch row 9 would block',\n 'merge markers on disk — `pnpm wp-start-update` has run and not finished',\n 'EXEMPT: everything is permitted, which is exactly what lets row 9 be strict',\n 'Resolve the conflicts, then `pnpm wp-finish-upsert-pr`',\n NO_ROW_EXIT),\n ]),\n new L2Row(4, ['B'], 'on the **skip list** — it gets you OUT, or tells you where you are', L2_ALLOW, '—', [\n new L2UseCase(6,\n '`git status` / `gh pr view` while blocked, to work out where you are',\n 'any state — orientation is never \"working here\"',\n 'ALLOW: metadata tells you where you are without putting stale file CONTENT in context',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(7,\n '`git stash` when `git checkout -b <new> origin/main` refuses because `origin/main` touched the same files you edited',\n 'on a stale `main` or a merged branch, dirty tree, with an overlapping upstream change',\n 'ALLOW: the cure for the row that blocked you must itself never be blocked — and this is the residual step that makes rows 6 and 8 safe to block on a dirty tree',\n 'None needed — then re-run the checkout and `git stash pop`',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(28,\n '`gh pr close 123`, `gh pr comment`, `gh api …` or a `curl` while parked on a stale `main` or a merged branch',\n 'blocked state, running something that touches GitHub or a URL and nothing in this tree',\n 'ALLOW: the skip list asks one question — does this read or write repo CONTENT? `gh` talks to GitHub and `curl`/`wget` talk to a network, so the branch state has nothing to say about them. The forms that write a local file (`gh repo clone`, `gh pr checkout`, `curl -o`, any `> file`) are excluded, and `gh pr create`/`merge` remain governed by their own guards',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(8,\n '`pnpm wp-start-upsert-pr` on a branch whose fork point is broken',\n 'row 9 state, running the tool row 9 prescribes',\n 'ALLOW: every `wp-*` bin is on the skip list, so no row can block its own remedy',\n 'None needed',\n 'merged-branch recovery/inspection (allowlisted)'),\n ]),\n new L2Row(5, ['E'], 'on `main`', L2_BLOCK, '`git checkout -b <new> origin/main`', [\n new L2UseCase(9,\n 'An Edit or Write to any tracked file while `git rev-parse --abbrev-ref HEAD` says `main`',\n 'on `main`, any freshness',\n 'BLOCK: decided by one `git rev-parse`, with NO cache read, so it fires on the first tool call of the session',\n '`git checkout -b <new> origin/main` — uncommitted work comes with you',\n 'on-main'),\n new L2UseCase(25,\n 'The FIRST edit of a session, on `main`, before any cache exists',\n 'on `main`, cache absent — row 11 would fail open',\n 'BLOCK anyway: row 5 is ABOVE the cache divider and reads only `git rev-parse`, so it is armed on call #1. This is why the row is `E` only and must never be gated on the cache',\n '`git checkout -b <new> origin/main`',\n 'on-main'),\n ]),\n new L2Row(L2_FAIL_OPEN_ROW, ['B', 'R', 'E'], '**the state could not be established** — branch undeterminable, no cache yet, the cache holds another branch, `origin/main` unknown, or the forge unreachable', L2_FAIL_OPEN, '— (nothing to fix; the refresher populates the cache for the next call)', [\n new L2UseCase(11,\n 'The very first tool call of a session is allowed even on a badly stale `main`',\n 'no cache — the refresher is fire-and-forget and populates it for the NEXT call',\n 'ALLOW (fail-open), logged as `ALLOW_FAIL_OPEN` so abstentions stay countable',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(12,\n 'Guards quietly stand down on a plane, or when `gh` is unauthenticated or rate-limited',\n 'the forge could not be asked whether the PR is merged',\n 'ALLOW (fail-open) logged as `no-forge` — distinct from \"asked, and it is not merged\", which used to look identical in the trail',\n 'None — restore network/`gh auth` to re-arm the merged-branch policy',\n 'no-forge'),\n new L2UseCase(27,\n 'A build, a `cat` or a `curl` on `main`, on the first Bash call of a session',\n 'on `main`, cache absent — so whether `main` is behind is UNKNOWN',\n 'ALLOW (fail-open), logged `ALLOW_FAIL_OPEN`. `B` on `main` is judged by rows 6/7 and therefore lands here when the cache cannot answer; the WRITE half is not, which is why row 5 sits above this divider',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(14,\n 'Mid-rebase, every guard abstains',\n 'detached HEAD — there is no branch name to judge',\n 'ALLOW (fail-open), logged LOUDLY when the branch is unresolvable rather than merely detached',\n 'None — finish or abort the rebase',\n 'branch-undeterminable'),\n ]),\n // ROWS 12/13 sit ABOVE row 6 because they are judged inside its state: local `main` is established\n // BEHIND, and the only remaining question is whether the command already carries its own cure. They\n // are numbered 12/13 rather than slotted in as 6a/6b for the reason row 11 is numbered 11 — row\n // numbers are logged as `row=` and cited here, so renumbering re-points every existing reference.\n new L2Row(12, ['B'], 'on `main`, behind `origin/main`, and the command STARTS with a refresh-main cure joined to the work by `&&`', L2_ALLOW, '—', [\n new L2UseCase(29,\n '`git fetch --prune origin main -q && git pull --ff-only origin main 2>&1 | tail -1 && sed -n \\'30,75p\\' src/app.ts` — the agent cures and reads in one call',\n 'on `main`, behind `origin/main`, cure first, `&&` between',\n 'ALLOW: `&&` short-circuits, so the `sed` never runs if the pull fails — the guard was refusing a safety property the shell already enforces. Measured fleet-wide as `cure_bundled_and`, and filed as a TOOLING defect, not an agent one',\n 'None needed',\n 'cure-prefixed, && short-circuits the work'),\n ]),\n new L2Row(13, ['B'], 'on `main`, behind `origin/main`, and the cure is joined to the work by `;` (or `||`, `&`, a newline) — the work runs even if the cure fails', L2_BLOCK, '`pnpm wp-sync-main && <your command>`', [\n new L2UseCase(30,\n '`pnpm wp-sync-main >/dev/null 2>&1; git log --oneline -1; sed -n \\'598,612p\\' eslint.config.mjs` — and the agent then quotes an eslint rule out of a file 15 commits stale',\n 'on `main`, behind `origin/main`, cure first, `;` between',\n 'BLOCK: `;` discards the cure\\'s exit code, so a conflict, a dirty tree or no network leaves the `sed` reading still-stale content — and 7 of the 9 observed cases also silenced the cure with `>/dev/null 2>&1`, so the failure was invisible too. The two-step is safer because the NEXT tool call re-computes `localMain` against `originMain`, so a failed pull re-blocks; an allowed `;` compound never gets that second look',\n 'Swap the `;` for `&&` — `pnpm wp-sync-main && <your command>` — or run the cure alone and re-issue the command in the next call',\n 'cure-prefixed, work runs anyway'),\n ]),\n new L2Row(6, ['B', 'R'], 'on `main`, behind `origin/main`', L2_BLOCK, '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`', [\n new L2UseCase(13,\n 'The Read tool refuses a file on a stale `main` while you have UNCOMMITTED edits',\n 'on `main`, behind `origin/main`, dirty tree',\n 'BLOCK. This used to fail open, on the argument that the prescribed `git pull` is not a clean fast-forward when the tree is dirty. That was true of the MESSAGE, not the row: the cure cell always offered a second form, and it works dirty',\n '`git checkout -b <new> origin/main` — uncommitted changes come with you onto the new branch. If git refuses because `origin/main` touched the same files, `git stash` first (never blocked), then retry, then `git stash pop`',\n 'on-stale-main'),\n new L2UseCase(15,\n 'The Read tool refuses a file that exists, on a `main` 18 commits behind',\n 'on `main`, behind `origin/main`, clean tree',\n 'BLOCK: judged by live ancestry (`git merge-base --is-ancestor`), not hash equality, so a pull takes effect instantly',\n '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(16,\n 'Read is blocked, so the session reaches for `cat`, `grep` and `ls` instead — and describes a CI workflow set missing a whole workflow that existed upstream',\n 'the SIDE DOOR: same tree, same staleness, different tool',\n 'BLOCK: `B` is judged here beside `R`, so closing the Read tool no longer opens a shell-shaped hole. The log used to read \"read-stale-guard handled\", which is worse than no guard — it looks covered',\n '`git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(10,\n 'A Bash command that WRITES tracked files as a side effect — `npx expo install`, a formatter, codegen, `sed -i`, a `>` redirect',\n 'on a `main` known to be BEHIND, and the write is incidental to a command whose stated purpose is something else',\n 'BLOCK: inside this row `B` is default-DENY plus row 4\\'s skip list, never a blocklist of readers — a command nobody thought to enumerate is caught by not being on the list, which is the only shape that could have caught this one',\n '`git checkout -b <new> origin/main` BEFORE running anything that may write',\n 'on-stale-main'),\n ]),\n new L2Row(7, ['B', 'R'], 'on `main`, current', L2_ALLOW, '—', [\n new L2UseCase(17,\n 'Reading files on a `main` you just pulled',\n 'on `main`, and `origin/main` is an ancestor of HEAD',\n 'ALLOW: ancestry, not hash equality, so the allow arrives the instant the pull lands rather than when the detached refresher next runs',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n new L2UseCase(24,\n '`curl`, `gh pr close` or a test run, immediately after `pnpm wp-sync-main` landed you on a perfectly current `main`',\n 'on `main`, current — no staleness anywhere',\n 'ALLOW. This used to BLOCK, from the branch alone: the tool the repo prescribes put the agent here, and the guard whose name says STALE then refused everything off a narrow allowlist for a reason that had nothing to do with staleness. WRITES here are still blocked, by row 5 — that hazard is real at any freshness',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n ]),\n new L2Row(8, ['B', 'R', 'E'], 'on a branch whose PR is **already merged**', L2_BLOCK, '`git fetch origin main && git checkout -b <new> origin/main`', [\n new L2UseCase(18,\n 'You keep working on the branch after its PR merged, and the next PR reopens code review already landed',\n 'branch whose PR is merged — `merged` is monotonic, so the cached flag is trusted with no TTL',\n 'BLOCK across all three tools',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n new L2UseCase(26,\n 'You have uncommitted edits on a branch whose PR just merged',\n 'merged branch, dirty tree',\n 'BLOCK. This used to fail open too, and that valve never had an argument behind it — row 8\\'s cure carries uncommitted work onto the fresh branch, so nothing was ever trapped. It was drift from the documented design, which `read-stale-guard`\\'s own class comment still described correctly',\n '`git fetch origin main && git checkout -b <new> origin/main` — your edits come with you',\n 'already-merged PR#'),\n new L2UseCase(19,\n 'A shell-only session sails through on a merged branch',\n 'merged branch, Bash only — both FILE guards are file-scoped, so Bash reached neither',\n 'BLOCK: `merged-branch-bash-guard` exists because `branchAlreadyMerged` was being computed and logged on that very path, then thrown away',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n ]),\n new L2Row(9, ['B', 'R', 'E'], 'no fork point with `origin/main`, or `origin/main` moved and collided with your files', L2_BLOCK, '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open', [\n new L2UseCase(20,\n 'Your branch and `origin/main` share no merge base — usually a branch cut from a squashed-away tip',\n 'no fork point',\n 'BLOCK: nothing built on this branch can be reasoned about relative to main',\n '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open',\n 'no-fork-point'),\n new L2UseCase(21,\n '`origin/main` moved and changed the same files you edited',\n 'main-moved collision',\n 'BLOCK — and row 3 then exempts everything once the merge starts, which is what makes this safe',\n '`pnpm wp-start-update`, resolve, `pnpm wp-finish-upsert-pr`',\n 'main-moved-conflict'),\n ]),\n new L2Row(10, ['B', 'R', 'E'], 'healthy feature branch', L2_ALLOW, '—', [\n new L2UseCase(22,\n 'Ordinary work on a branch cut from a current `origin/main`',\n 'healthy feature branch',\n 'ALLOW — the state every other row exists to push you back into',\n 'None needed',\n 'clean-feature-branch'),\n new L2UseCase(23,\n '`stale-main-bash-guard` sees a feature branch and hands off',\n 'not on `main` — state B belongs to `merged-branch-bash-guard`',\n 'ALLOW: the same verdict about the same tree, logged by the guard that is not responsible for it',\n 'None needed',\n 'not-on-main (state B is another guard)'),\n ]),\n];\n\n/**\n * Every use case on every row, in row order, for the doc and for the exhaustiveness specs.\n *\n * Numbering is GLOBAL and is identity, exactly as the row numbers are: a use case is cited by number in\n * review and in the doc, so add new ones at the END of the highest number rather than renumbering to\n * keep a row's block contiguous.\n */\n// webpieces-disable no-function-outside-class -- pure accessor over L2_ROWS above, in this data module\nexport function allL2UseCases(): readonly L2UseCase[] {\n return L2_ROWS.flatMap((row: L2Row): readonly L2UseCase[] => row.useCases);\n}\n\n/**\n * REASON → ROW. The join between what a guard actually logged and the row it is an instance of.\n *\n * Keys are the exact `reason` strings the four L2 guards pass to their decision log. Two of them are\n * PREFIXES because the guard interpolates a PR number or a matched segment into the reason\n * (`already-merged PR#123`); those are matched by `l2RowForReason` on prefix, which is why they end in\n * a space or a `(`.\n *\n * l2-matrix.spec.ts reads the four guard sources and asserts every reason literal in them resolves\n * here, so a new exit with no row fails the build rather than logging `row=-` forever.\n */\nconst EXACT_REASON_ROWS: Record<string, number> = {\n // Row 1 — the universal cure that must stay reachable from inside any block.\n 'webpieces-config-read (escape hatch)': 1,\n // Row 2 — the preventive half, decided from command TEXT before any cache is read.\n // (prefix, see PREFIX_REASON_ROWS)\n // Row 4 — the skip list, in its two live spellings.\n 'merged-branch recovery/inspection (allowlisted)': 4,\n 'not-a-content-read (cure/build/metadata)': 4,\n // Row 5 — never WRITE on main, at any freshness. `E` only; the Bash half is rows 6/7.\n 'on-main': 5,\n // Row 6/7 — freshness, for `B` and `R` alike. Both guards log these two literals: read-stale-guard\n // for the Read tool, stale-main-bash-guard for Bash. Same cache, same ancestry test, one verdict.\n 'on-stale-main': 6,\n 'local-main-contains-origin (up to date)': 7,\n // Rows 12/13 — composition, judged INSIDE row 6's state: the tree is established behind, and the\n // only remaining question is whether the command carries its own cure and with which operator.\n 'cure-prefixed, && short-circuits the work': 12,\n 'cure-prefixed, work runs anyway': 13,\n // Row 9 — the two unhealthy-fork states.\n 'no-fork-point': 9,\n 'main-moved-conflict': 9,\n // Row 10 — healthy, and the state-B guard's \"this is not my state\" hand-off, which is the same\n // verdict about the same tree.\n 'clean-feature-branch': 10,\n 'not-on-main (state B is another guard)': 10,\n // Row 11 — every \"could not establish\", including the two dirty-tree valves the code still opens\n // (see NOT_DONE) and the unreachable forge.\n 'branch-undeterminable': L2_FAIL_OPEN_ROW,\n 'no-sync-cache': L2_FAIL_OPEN_ROW,\n 'stale-cross-branch-cache': L2_FAIL_OPEN_ROW,\n 'origin-main-unknown': L2_FAIL_OPEN_ROW,\n // `dirty-tree-on-main` and `dirty-merged-branch` used to live here. Both valves are deleted: rows\n // 6 and 8 now block on a dirty tree, because each row's cure carries uncommitted work with you.\n 'no-forge': L2_FAIL_OPEN_ROW,\n // The config-edit bypass logged by the hook adapter before any guard runs — row 1, same universal\n // cure as the config READ above.\n 'config-bypass (feature-branch-guard skipped)': 1,\n};\n\n/** Reasons the guards interpolate a value into. Matched by prefix, longest first. */\nconst PREFIX_REASON_ROWS: Record<string, number> = {\n 'already-merged PR#': 8,\n 'bare checkout of main (': 2,\n // `stale-main content read (` used to live here, mapping the Bash side of row 6. It is gone with\n // the guard exit that emitted it: on `main`, row 5 now blocks before any content-read scan runs,\n // so row 6 is what the table always said it was — the ROW-ONLY row.\n};\n\n/**\n * The row a logged reason belongs to, or null when nothing claims it.\n *\n * Null rather than a default row: a reason with no row is a HOLE in the table, and defaulting it to\n * \"fail-open\" would hide exactly the drift the exhaustiveness spec exists to catch. The guards render\n * null as `row=-`, so an unmapped reason is visible in the log too, not only in CI.\n */\n// webpieces-disable no-function-outside-class -- the matcher over the two tables above, beside them in this data module\nexport function l2RowForReason(reason: string): number | null {\n const exact = EXACT_REASON_ROWS[reason];\n if (exact !== undefined) return exact;\n for (const prefix of Object.keys(PREFIX_REASON_ROWS)) {\n if (reason.startsWith(prefix)) return PREFIX_REASON_ROWS[prefix];\n }\n return null;\n}\n\n/** Every reason string this table claims, for the exhaustiveness spec. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two tables above\nexport function l2MappedReasons(): readonly string[] {\n return [...Object.keys(EXACT_REASON_ROWS), ...Object.keys(PREFIX_REASON_ROWS)];\n}\n\n/** One documented gap between a row and what the guards actually do today. Data-only. */\nexport class L2NotDone {\n constructor(readonly row: number, readonly gap: string, readonly why: string) {}\n}\n\n/**\n * WHERE THE TABLE AND THE CODE DISAGREE, stated rather than papered over.\n *\n * The L1 precedent is `## Not done — \\`o\\` is not exempt yet`: a row the runner cannot reach, named in\n * the generated doc with the reason it has not shipped. The same treatment applies here, and it is what\n * makes it safe to publish a table the guards do not yet dispatch from — a reader is told exactly which\n * rows describe intent rather than behaviour, and the log's `row=` stamps land on row 11 for every one\n * of these, so the trail never claims the strict row fired.\n */\nexport const NOT_DONE: readonly L2NotDone[] = [\n // EMPTY, and that is the goal state: every row in the table is a row the guards actually honour.\n //\n // It held three entries. Row 5's `B` half shipped and then MOVED: on `main`, a write is judged from\n // the branch alone (row 5, above the divider) while Bash is judged on freshness beside the Read tool\n // (rows 6/7), because a build on a CURRENT `main` harms nothing and denying it stranded agents on\n // the very `main` `pnpm wp-sync-main` had just handed them. Rows 6 and 8 held DIRTY-TREE\n // valves, and both are now closed — each of\n // those rows cures with `git checkout -b <new> origin/main`, which carries uncommitted changes onto\n // the new branch, so a dirty tree never trapped anybody. The row 6 entry claimed the dirty argument\n // \"has teeth\" there because its cure is `git pull`; that was a fact about the MESSAGE, which printed\n // only the pull, and the fix was to print both cures rather than to suppress the block.\n //\n // Keep this array. An empty \"Not done\" is a claim worth making explicitly — the doc says so in as\n // many words — and the next divergence between a row and its code belongs here, not in prose.\n];\n"]}
|
|
1
|
+
{"version":3,"file":"l2-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l2-rows.ts"],"names":[],"mappings":"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","sourcesContent":["// ---------------------------------------------------------------------------\n// L2 — the BRANCH-STATE layer, as data.\n//\n// L2 answers one question: *may I work here, and is what I read current?* Drawn as a decision matrix\n// that is TWELVE ordered rows plus one terminal fail-open row, first match wins.\n//\n// This module holds those rows, l2-doc.ts renders them into guards/L2-branch-state.md, and a unit test\n// (l2-matrix.spec.ts) locks that file byte-identical to the renderer — the same mechanism that already\n// makes L0's fault table and L1's location table undriftable. Before this existed the L2 doc was 100%\n// hand-written and said so: *\"Until that lands this text is hand-written and can drift.\"*\n//\n// ## HOW L2 JOINS TO THE ROWS TODAY — read this before assuming it works like L1\n//\n// L1 DISPATCHES from its array: `runner.l1LocationBlock` takes the first matching row and switches on\n// its `blockId`, so deleting a row deletes a block. L2 does NOT, and pretending otherwise would be the\n// drift this table exists to remove. The four L2 guard classes each own their own ladder, and those\n// ladders diverge on purpose (guards/L2-branch-state.md, \"Deliberate divergence\": the two Bash guards\n// differ in polarity, quantifier and empty-command handling all at once, so no single parameterised\n// function serves both). Unifying them is where a wrong edit turns an allow into a session-wedging\n// block, so it is deliberately not attempted here.\n//\n// What L2 does instead is a REASON→ROW join. Every exit of every L2 guard already carries a stable\n// reason string into the decision log; `L2_ROW_FOR_REASON` maps each of those to the row it is an\n// instance of, the guards stamp that number as `row=`, and l2-matrix.spec.ts asserts the map is\n// EXHAUSTIVE against the guard sources — a new reason with no row fails the build. So `row=8` in\n// `.webpieces/logs/L2-decisions` opens guards/L2-branch-state.md at row 8 and reads the state, the cure\n// and the tools that row covers, exactly as `row=5` already does for L1.\n//\n// The rows the guards cannot yet honour are named in the doc's \"Not done\" section rather than quietly\n// rendered as if they were live. That section is generated from NOT_DONE below, so it cannot rot.\n//\n// This module is deliberately import-free at runtime, so `pnpm guards:generate` can load it without the\n// package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/** Which tools a row covers. `B` Bash · `R` Read · `E` Write/Edit. */\nexport type L2Tool = 'B' | 'R' | 'E';\n\n/** What L2 does with a row — the same action codebook every layer reports in (GUARD_MATRIX.md). */\nexport type L2ActionKind = 'allow' | 'exempt' | 'block' | 'fail-open';\n\n/**\n * The TERMINAL fail-open row, and the one number in this table that is not from the 1-10 design.\n *\n * Everything in rows 6-10 needs the main-sync cache, and the cache is written by a fire-and-forget\n * refresher that populates it for the NEXT call — so the first tool call of every session has none.\n * \"Stop here and ALLOW\" was written as a DIVIDER in the design table, i.e. as prose between two blocks\n * of rows. Prose cannot be stamped into a log line, and this is the single most frequently taken exit\n * in the whole layer (every session's first call, every unreadable branch, every unreachable forge), so\n * it is a row with a number like any other.\n *\n * It is 11 rather than 6-with-a-renumber because row numbers are IDENTITY here: they are printed in the\n * doc and logged as `row=`, so shifting 6-10 down would silently re-point every reference. The doc\n * prints it in its true position, between rows 5 and 6, with its number shown — same treatment L1 gives\n * row 8, which is printed third and numbered 8.\n */\nexport const L2_FAIL_OPEN_ROW = 11;\n\n/** The `act` cell: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L2Action {\n constructor(readonly label: string, readonly kind: L2ActionKind) {}\n}\n\nexport const L2_ALLOW = new L2Action('1 allow', 'allow');\nexport const L2_EXEMPT = new L2Action('2 exempt', 'exempt');\nexport const L2_BLOCK = new L2Action('4 block', 'block');\nexport const L2_FAIL_OPEN = new L2Action('1 allow (fail-open)', 'fail-open');\n\n/**\n * One row of the \"L2 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * THE POINT OF THIS CLASS is that a use case is added HERE, in code, beside the row it exercises — not\n * into a hand-written doc section that drifts. When a new situation comes up in a session, it becomes\n * one more `new L2UseCase(...)` on the row that judged it, `pnpm guards:generate` re-renders the doc,\n * and the byte-lock spec fails if anyone edits the rendered table instead.\n *\n * The four text fields are rendered VERBATIM. `reason` is the ENFORCEMENT half and is never rendered:\n * it is the exact `reason` string the guard logs for this case, so a spec can push it back through\n * `l2RowForReason` and assert it lands on the row this use case is filed under. That closes the loop\n * the L2 decision log opens — `row=` in the trail, this table on the page, one join between them.\n *\n * `reason` is REQUIRED, and a case that exercises something which is not an L2 row exit says so with\n * `NO_ROW_EXIT` rather than by omitting the argument. An optional field would make opting OUT of the\n * only real enforcement here the shortest thing to type and impossible to grep — the widening-by-absence\n * shape `.claude/rules/no-backwards-compat.md` rejects. `grep NO_ROW_EXIT` now lists every unenforced case.\n */\nexport class L2UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the reason behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly reason: string,\n ) {}\n}\n\n/**\n * The `reason` for a use case that is NOT an L2 row exit, and so has nothing to join back to.\n *\n * The only legitimate case today is row 3: merge-in-progress is L4's state, and L2 exempts it without\n * logging a reason of its own. Named rather than absent, so \"this case is not enforced\" is a value in\n * the table you can grep for instead of a missing argument nobody notices.\n */\nexport const NO_ROW_EXIT = 'NO_ROW_EXIT (not an L2 row exit — another layer owns this state)';\n\n/**\n * One row of L2's decision table.\n *\n * `cure` is rendered verbatim into the doc and is LITERAL by policy: L0's cure-reachability discipline\n * says a message pointing at documentation for its own remedy cannot be tested, and it caught a fault\n * prescribing a bin that had been renamed away. `—` is the only legal non-command cure, and only on a\n * row that allows.\n */\nexport class L2Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- the five cells of one doc row plus its use cases\n constructor(\n readonly num: number,\n readonly tools: readonly L2Tool[],\n /** The `state` cell, verbatim. */\n readonly state: string,\n readonly action: L2Action,\n /** The `cure` cell, verbatim. `—` when the row allows. */\n readonly cure: string,\n /**\n * The observed situations this row judges. Rendered as the \"L2 use cases\" table.\n *\n * A NON-EMPTY tuple, and required: a row nobody has ever seen fire is either dead or\n * undocumented, and both are worth knowing. Expressing that in the TYPE rather than as a\n * runtime assertion is the JwtRoles pattern — the invariant is enforced at the moment the row\n * is written, which is the only moment that changes what somebody types.\n */\n readonly useCases: readonly [L2UseCase, ...L2UseCase[]],\n ) {}\n\n /** `B R E`, the doc's own spelling of the tool cell. */\n toolCell(): string {\n return this.tools.join(' ');\n }\n}\n\n/**\n * THE THIRTEEN L2 ROWS, in first-match-wins order.\n *\n * Rows 1-5 need NO cache and fire on call #1: rows 1, 2 and 4 are text matches, row 3 is a marker-file\n * scan, row 5 is one `git rev-parse`. Row 11 is the cache divider. Rows 6-10 all read the cache.\n *\n * THE ORDER OF ROW 5 IS THE MOST LOAD-BEARING THING IN THIS TABLE. Put \"on main\" BELOW the divider and\n * WRITES on `main` are permitted for the whole first call of every session — and permanently in a\n * multi-worktree repo, where another tree may hold the cache lock indefinitely.\n *\n * `B` AND `E` PART COMPANY ON `main`, and rows 5/6/7 are where. A WRITE on `main` is wrong at any\n * freshness — the work lands somewhere unreviewable and unrevertable — so row 5 is `E` only, judged on\n * the branch alone, above the divider. A READ or a BUILD on a CURRENT `main` is harmless, and blocking\n * it strands the agent right after `pnpm wp-sync-main` put it there; so `B` joins `R` on the\n * FRESHNESS-gated pair below the divider (row 6 behind → block, row 7 current → allow), where \"cannot\n * tell\" fails open at row 11 by construction. `B` and `R` still differ in SHAPE inside row 6: a Read\n * names one file and is judged precisely, a Bash command is opaque and gets default-deny plus row 4.\n */\nexport const L2_ROWS: readonly L2Row[] = [\n new L2Row(1, ['B', 'R', 'E'], 'on the **global allowlist** (inert command, or a universal cure such as reading/editing `webpieces.config.json`)', L2_ALLOW, '—', [\n new L2UseCase(1,\n 'You are blocked by some other L2 row, and need to turn the policy off to get anything done',\n 'any state — this row is ahead of every block',\n 'ALLOW: reading and editing `webpieces.config.json` is never blocked, so the mode-OFF cure is always reachable',\n 'Edit `webpieces.config.json` → `hookGuards` → `branch-state-guard` → `\"mode\": \"OFF\"`',\n 'webpieces-config-read (escape hatch)'),\n new L2UseCase(2,\n 'A Write to `webpieces.config.json` while on `main`, which row 5 would otherwise block',\n 'on `main`, editing the one file that can disable the guard',\n 'ALLOW: the hook adapter bypasses feature-branch-guard for this path before any guard runs',\n 'None needed — the edit proceeds',\n 'config-bypass (feature-branch-guard skipped)'),\n ]),\n new L2Row(2, ['B'], 'bare `git checkout main`, with no `git pull` chained into the same command', L2_BLOCK, '`pnpm wp-sync-main`', [\n new L2UseCase(3,\n '`git checkout main` after a merge, to start the next piece of work',\n 'about to land on whatever local `main` you last had — 157 commits behind, in the incident',\n 'BLOCK: decided from command TEXT alone, before the checkout, because the only `main` this could measure is the one it is about to leave',\n '`pnpm wp-sync-main` — checkout, pull, reap dead branches/worktrees, sweep orphan directories, in one command (hand-rolled, the pull must be in the SAME command as the checkout)',\n 'bare checkout of main ('),\n new L2UseCase(4,\n 'The same command inside a linked worktree, where `git checkout main` fatals anyway',\n 'linked worktree — `main` is already checked out in the primary clone',\n 'BLOCK, and the message prints the worktree form rather than a cure git would refuse',\n '`git fetch origin main`, then work off `origin/main`',\n 'bare checkout of main ('),\n ]),\n new L2Row(3, ['B', 'R', 'E'], '**merge in progress** — L4 owns this state', L2_EXEMPT, 'finish the merge: `pnpm wp-finish-upsert-pr`', [\n new L2UseCase(5,\n 'Reading and editing conflicted files during a 3-point merge, on a branch row 9 would block',\n 'merge markers on disk — `pnpm wp-start-update` has run and not finished',\n 'EXEMPT: everything is permitted, which is exactly what lets row 9 be strict',\n 'Resolve the conflicts, then `pnpm wp-finish-upsert-pr`',\n NO_ROW_EXIT),\n ]),\n new L2Row(4, ['B'], 'on the **skip list** — it gets you OUT, or tells you where you are', L2_ALLOW, '—', [\n new L2UseCase(6,\n '`git status` / `gh pr view` while blocked, to work out where you are',\n 'any state — orientation is never \"working here\"',\n 'ALLOW: metadata tells you where you are without putting stale file CONTENT in context',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(7,\n '`git stash` when `git checkout -b <new> origin/main` refuses because `origin/main` touched the same files you edited',\n 'on a stale `main` or a merged branch, dirty tree, with an overlapping upstream change',\n 'ALLOW: the cure for the row that blocked you must itself never be blocked — and this is the residual step that makes rows 6 and 8 safe to block on a dirty tree',\n 'None needed — then re-run the checkout and `git stash pop`',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(28,\n '`gh pr close 123`, `gh pr comment`, `gh api …` or a `curl` while parked on a stale `main` or a merged branch',\n 'blocked state, running something that touches GitHub or a URL and nothing in this tree',\n 'ALLOW: the skip list asks one question — does this read or write repo CONTENT? `gh` talks to GitHub and `curl`/`wget` talk to a network, so the branch state has nothing to say about them. The forms that write a local file (`gh repo clone`, `gh pr checkout`, `curl -o`, any `> file`) are excluded, and `gh pr create`/`merge` remain governed by their own guards',\n 'None needed',\n 'not-a-content-read (cure/build/metadata)'),\n new L2UseCase(8,\n '`pnpm wp-start-upsert-pr` on a branch whose fork point is broken',\n 'row 9 state, running the tool row 9 prescribes',\n 'ALLOW: every `wp-*` bin is on the skip list, so no row can block its own remedy',\n 'None needed',\n 'merged-branch recovery/inspection (allowlisted)'),\n ]),\n new L2Row(5, ['E'], 'on `main`', L2_BLOCK, '`git checkout -b <new> origin/main`', [\n new L2UseCase(9,\n 'An Edit or Write to any tracked file while `git rev-parse --abbrev-ref HEAD` says `main`',\n 'on `main`, any freshness',\n 'BLOCK: decided by one `git rev-parse`, with NO cache read, so it fires on the first tool call of the session',\n '`git checkout -b <new> origin/main` — uncommitted work comes with you',\n 'on-main'),\n new L2UseCase(25,\n 'The FIRST edit of a session, on `main`, before any cache exists',\n 'on `main`, cache absent — row 11 would fail open',\n 'BLOCK anyway: row 5 is ABOVE the cache divider and reads only `git rev-parse`, so it is armed on call #1. This is why the row is `E` only and must never be gated on the cache',\n '`git checkout -b <new> origin/main`',\n 'on-main'),\n ]),\n new L2Row(L2_FAIL_OPEN_ROW, ['B', 'R', 'E'], '**the state could not be established** — branch undeterminable, no cache yet, the cache holds another branch, `origin/main` unknown, or the forge unreachable', L2_FAIL_OPEN, '— (nothing to fix; the refresher populates the cache for the next call)', [\n new L2UseCase(11,\n 'The very first tool call of a session is allowed even on a badly stale `main`',\n 'no cache — the refresher is fire-and-forget and populates it for the NEXT call',\n 'ALLOW (fail-open), logged as `ALLOW_FAIL_OPEN` so abstentions stay countable',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(12,\n 'Guards quietly stand down on a plane, or when `gh` is unauthenticated or rate-limited',\n 'the forge could not be asked whether the PR is merged',\n 'ALLOW (fail-open) logged as `no-forge` — distinct from \"asked, and it is not merged\", which used to look identical in the trail',\n 'None — restore network/`gh auth` to re-arm the merged-branch policy',\n 'no-forge'),\n new L2UseCase(27,\n 'A build, a `cat` or a `curl` on `main`, on the first Bash call of a session',\n 'on `main`, cache absent — so whether `main` is behind is UNKNOWN',\n 'ALLOW (fail-open), logged `ALLOW_FAIL_OPEN`. `B` on `main` is judged by rows 6/7 and therefore lands here when the cache cannot answer; the WRITE half is not, which is why row 5 sits above this divider',\n 'None — the second call is judged normally',\n 'no-sync-cache'),\n new L2UseCase(14,\n 'Mid-rebase, every guard abstains',\n 'detached HEAD — there is no branch name to judge',\n 'ALLOW (fail-open), logged LOUDLY when the branch is unresolvable rather than merely detached',\n 'None — finish or abort the rebase',\n 'branch-undeterminable'),\n ]),\n // ROWS 12/13 sit ABOVE row 6 because they are judged inside its state: local `main` is established\n // BEHIND, and the only remaining question is whether the command already carries its own cure. They\n // are numbered 12/13 rather than slotted in as 6a/6b for the reason row 11 is numbered 11 — row\n // numbers are logged as `row=` and cited here, so renumbering re-points every existing reference.\n new L2Row(12, ['B'], 'on `main`, behind `origin/main`, and the command STARTS with a refresh-main cure joined to the work by `&&`', L2_ALLOW, '—', [\n new L2UseCase(29,\n '`git fetch --prune origin main -q && git pull --ff-only origin main 2>&1 | tail -1 && sed -n \\'30,75p\\' src/app.ts` — the agent cures and reads in one call',\n 'on `main`, behind `origin/main`, cure first, `&&` between',\n 'ALLOW: `&&` short-circuits, so the `sed` never runs if the pull fails — the guard was refusing a safety property the shell already enforces. Measured fleet-wide as `cure_bundled_and`, and filed as a TOOLING defect, not an agent one',\n 'None needed',\n 'cure-prefixed, && short-circuits the work'),\n ]),\n new L2Row(13, ['B'], 'on `main`, behind `origin/main`, and the cure is joined to the work by `;` (or `||`, `&`, a newline) — the work runs even if the cure fails', L2_BLOCK, '`pnpm wp-sync-main && <your command>`', [\n new L2UseCase(30,\n '`pnpm wp-sync-main >/dev/null 2>&1; git log --oneline -1; sed -n \\'598,612p\\' eslint.config.mjs` — and the agent then quotes an eslint rule out of a file 15 commits stale',\n 'on `main`, behind `origin/main`, cure first, `;` between',\n 'BLOCK: `;` discards the cure\\'s exit code, so a conflict, a dirty tree or no network leaves the `sed` reading still-stale content — and 7 of the 9 observed cases also silenced the cure with `>/dev/null 2>&1`, so the failure was invisible too. The two-step is safer because the NEXT tool call re-computes `localMain` against `originMain`, so a failed pull re-blocks; an allowed `;` compound never gets that second look',\n 'Swap the `;` for `&&` — `pnpm wp-sync-main && <your command>` — or run the cure alone and re-issue the command in the next call',\n 'cure-prefixed, work runs anyway'),\n ]),\n new L2Row(6, ['B', 'R'], 'on `main`, behind `origin/main`', L2_BLOCK, '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`', [\n new L2UseCase(13,\n 'The Read tool refuses a file on a stale `main` while you have UNCOMMITTED edits',\n 'on `main`, behind `origin/main`, dirty tree',\n 'BLOCK. This used to fail open, on the argument that the prescribed `git pull` is not a clean fast-forward when the tree is dirty. That was true of the MESSAGE, not the row: the cure cell always offered a second form, and it works dirty',\n '`git checkout -b <new> origin/main` — uncommitted changes come with you onto the new branch. If git refuses because `origin/main` touched the same files, `git stash` first (never blocked), then retry, then `git stash pop`',\n 'on-stale-main'),\n new L2UseCase(15,\n 'The Read tool refuses a file that exists, on a `main` 18 commits behind',\n 'on `main`, behind `origin/main`, clean tree',\n 'BLOCK: judged by live ancestry (`git merge-base --is-ancestor`), not hash equality, so a pull takes effect instantly',\n '`pnpm wp-sync-main`, or `git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(16,\n 'Read is blocked, so the session reaches for `cat`, `grep` and `ls` instead — and describes a CI workflow set missing a whole workflow that existed upstream',\n 'the SIDE DOOR: same tree, same staleness, different tool',\n 'BLOCK: `B` is judged here beside `R`, so closing the Read tool no longer opens a shell-shaped hole. The log used to read \"read-stale-guard handled\", which is worse than no guard — it looks covered',\n '`git checkout -b <new> origin/main`',\n 'on-stale-main'),\n new L2UseCase(10,\n 'A Bash command that WRITES tracked files as a side effect — `npx expo install`, a formatter, codegen, `sed -i`, a `>` redirect',\n 'on a `main` known to be BEHIND, and the write is incidental to a command whose stated purpose is something else',\n 'BLOCK: inside this row `B` is default-DENY plus row 4\\'s skip list, never a blocklist of readers — a command nobody thought to enumerate is caught by not being on the list, which is the only shape that could have caught this one',\n '`git checkout -b <new> origin/main` BEFORE running anything that may write',\n 'on-stale-main'),\n ]),\n new L2Row(7, ['B', 'R'], 'on `main`, current', L2_ALLOW, '—', [\n new L2UseCase(17,\n 'Reading files on a `main` you just pulled',\n 'on `main`, and `origin/main` is an ancestor of HEAD',\n 'ALLOW: ancestry, not hash equality, so the allow arrives the instant the pull lands rather than when the detached refresher next runs',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n new L2UseCase(24,\n '`curl`, `gh pr close` or a test run, immediately after `pnpm wp-sync-main` landed you on a perfectly current `main`',\n 'on `main`, current — no staleness anywhere',\n 'ALLOW. This used to BLOCK, from the branch alone: the tool the repo prescribes put the agent here, and the guard whose name says STALE then refused everything off a narrow allowlist for a reason that had nothing to do with staleness. WRITES here are still blocked, by row 5 — that hazard is real at any freshness',\n 'None needed',\n 'local-main-contains-origin (up to date)'),\n ]),\n new L2Row(8, ['B', 'R', 'E'], 'on a branch whose PR is **already merged**', L2_BLOCK, '`git fetch origin main && git checkout -b <new> origin/main`', [\n new L2UseCase(18,\n 'You keep working on the branch after its PR merged, and the next PR reopens code review already landed',\n 'branch whose PR is merged — `merged` is monotonic, so the cached flag is trusted with no TTL',\n 'BLOCK across all three tools',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n new L2UseCase(26,\n 'You have uncommitted edits on a branch whose PR just merged',\n 'merged branch, dirty tree',\n 'BLOCK. This used to fail open too, and that valve never had an argument behind it — row 8\\'s cure carries uncommitted work onto the fresh branch, so nothing was ever trapped. It was drift from the documented design, which `read-stale-guard`\\'s own class comment still described correctly',\n '`git fetch origin main && git checkout -b <new> origin/main` — your edits come with you',\n 'already-merged PR#'),\n new L2UseCase(19,\n 'A shell-only session sails through on a merged branch',\n 'merged branch, Bash only — both FILE guards are file-scoped, so Bash reached neither',\n 'BLOCK: `merged-branch-bash-guard` exists because `branchAlreadyMerged` was being computed and logged on that very path, then thrown away',\n '`git fetch origin main && git checkout -b <new> origin/main`',\n 'already-merged PR#'),\n ]),\n new L2Row(9, ['B', 'R', 'E'], 'no fork point with `origin/main`, or `origin/main` moved and collided with your files', L2_BLOCK, '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open', [\n new L2UseCase(20,\n 'Your branch and `origin/main` share no merge base — usually a branch cut from a squashed-away tip',\n 'no fork point',\n 'BLOCK: nothing built on this branch can be reasoned about relative to main',\n '`pnpm wp-start-update`, or `pnpm wp-start-upsert-pr` when a PR is open',\n 'no-fork-point'),\n new L2UseCase(21,\n '`origin/main` moved and changed the same files you edited',\n 'main-moved collision',\n 'BLOCK — and row 3 then exempts everything once the merge starts, which is what makes this safe',\n '`pnpm wp-start-update`, resolve, `pnpm wp-finish-upsert-pr`',\n 'main-moved-conflict'),\n ]),\n new L2Row(10, ['B', 'R', 'E'], 'healthy feature branch', L2_ALLOW, '—', [\n new L2UseCase(22,\n 'Ordinary work on a branch cut from a current `origin/main`',\n 'healthy feature branch',\n 'ALLOW — the state every other row exists to push you back into',\n 'None needed',\n 'clean-feature-branch'),\n new L2UseCase(23,\n '`stale-main-bash-guard` sees a feature branch and hands off',\n 'not on `main` — state B belongs to `merged-branch-bash-guard`',\n 'ALLOW: the same verdict about the same tree, logged by the guard that is not responsible for it',\n 'None needed',\n 'not-on-main (state B is another guard)'),\n ]),\n];\n\n/**\n * Every use case on every row, in row order, for the doc and for the exhaustiveness specs.\n *\n * Numbering is GLOBAL and is identity, exactly as the row numbers are: a use case is cited by number in\n * review and in the doc, so add new ones at the END of the highest number rather than renumbering to\n * keep a row's block contiguous.\n */\n// webpieces-disable no-function-outside-class -- pure accessor over L2_ROWS above, in this data module\nexport function allL2UseCases(): readonly L2UseCase[] {\n return L2_ROWS.flatMap((row: L2Row): readonly L2UseCase[] => row.useCases);\n}\n\n/**\n * REASON → ROW. The join between what a guard actually logged and the row it is an instance of.\n *\n * Keys are the exact `reason` strings the four L2 guards pass to their decision log. Two of them are\n * PREFIXES because the guard interpolates a PR number or a matched segment into the reason\n * (`already-merged PR#123`); those are matched by `l2RowForReason` on prefix, which is why they end in\n * a space or a `(`.\n *\n * l2-matrix.spec.ts reads the four guard sources and asserts every reason literal in them resolves\n * here, so a new exit with no row fails the build rather than logging `row=-` forever.\n */\nconst EXACT_REASON_ROWS: Record<string, number> = {\n // Row 1 — the universal cure that must stay reachable from inside any block.\n 'webpieces-config-read (escape hatch)': 1,\n // Row 2 — the preventive half, decided from command TEXT before any cache is read.\n // (prefix, see PREFIX_REASON_ROWS)\n // Row 4 — the skip list, in its two live spellings.\n 'merged-branch recovery/inspection (allowlisted)': 4,\n 'not-a-content-read (cure/build/metadata)': 4,\n // Row 5 — never WRITE on main, at any freshness. `E` only; the Bash half is rows 6/7.\n 'on-main': 5,\n // Row 6/7 — freshness, for `B` and `R` alike. Both guards log these two literals: read-stale-guard\n // for the Read tool, stale-main-bash-guard for Bash. Same cache, same ancestry test, one verdict.\n 'on-stale-main': 6,\n 'local-main-contains-origin (up to date)': 7,\n // Rows 12/13 — composition, judged INSIDE row 6's state: the tree is established behind, and the\n // only remaining question is whether the command carries its own cure and with which operator.\n 'cure-prefixed, && short-circuits the work': 12,\n 'cure-prefixed, work runs anyway': 13,\n // Row 9 — the two unhealthy-fork states.\n 'no-fork-point': 9,\n 'main-moved-conflict': 9,\n // Row 10 — healthy, and the state-B guard's \"this is not my state\" hand-off, which is the same\n // verdict about the same tree.\n 'clean-feature-branch': 10,\n 'not-on-main (state B is another guard)': 10,\n // Row 11 — every \"could not establish\", including the two dirty-tree valves the code still opens\n // (see NOT_DONE) and the unreachable forge.\n 'branch-undeterminable': L2_FAIL_OPEN_ROW,\n 'no-sync-cache': L2_FAIL_OPEN_ROW,\n 'stale-cross-branch-cache': L2_FAIL_OPEN_ROW,\n 'origin-main-unknown': L2_FAIL_OPEN_ROW,\n // `dirty-tree-on-main` and `dirty-merged-branch` used to live here. Both valves are deleted: rows\n // 6 and 8 now block on a dirty tree, because each row's cure carries uncommitted work with you.\n 'no-forge': L2_FAIL_OPEN_ROW,\n // The config-edit bypass logged by the hook adapter before any guard runs — row 1, same universal\n // cure as the config READ above.\n 'config-bypass (feature-branch-guard skipped)': 1,\n};\n\n/** Reasons the guards interpolate a value into. Matched by prefix, longest first. */\nconst PREFIX_REASON_ROWS: Record<string, number> = {\n 'already-merged PR#': 8,\n 'bare checkout of main (': 2,\n // `stale-main content read (` used to live here, mapping the Bash side of row 6. It is gone with\n // the guard exit that emitted it: on `main`, row 5 now blocks before any content-read scan runs,\n // so row 6 is what the table always said it was — the ROW-ONLY row.\n};\n\n/**\n * The row a logged reason belongs to, or null when nothing claims it.\n *\n * Null rather than a default row: a reason with no row is a HOLE in the table, and defaulting it to\n * \"fail-open\" would hide exactly the drift the exhaustiveness spec exists to catch. The guards render\n * null as `row=-`, so an unmapped reason is visible in the log too, not only in CI.\n */\n// webpieces-disable no-function-outside-class -- the matcher over the two tables above, beside them in this data module\nexport function l2RowForReason(reason: string): number | null {\n const exact = EXACT_REASON_ROWS[reason];\n if (exact !== undefined) return exact;\n for (const prefix of Object.keys(PREFIX_REASON_ROWS)) {\n if (reason.startsWith(prefix)) return PREFIX_REASON_ROWS[prefix];\n }\n return null;\n}\n\n/** Every reason string this table claims, for the exhaustiveness spec. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two tables above\nexport function l2MappedReasons(): readonly string[] {\n return [...Object.keys(EXACT_REASON_ROWS), ...Object.keys(PREFIX_REASON_ROWS)];\n}\n\n/** One documented gap between a row and what the guards actually do today. Data-only. */\nexport class L2NotDone {\n constructor(readonly row: number, readonly gap: string, readonly why: string) {}\n}\n\n/**\n * WHERE THE TABLE AND THE CODE DISAGREE, stated rather than papered over.\n *\n * The L1 precedent is `## Not done — \\`o\\` is not exempt yet`: a row the runner cannot reach, named in\n * the generated doc with the reason it has not shipped. The same treatment applies here, and it is what\n * makes it safe to publish a table the guards do not yet dispatch from — a reader is told exactly which\n * rows describe intent rather than behaviour, and the log's `row=` stamps land on row 11 for every one\n * of these, so the trail never claims the strict row fired.\n */\nexport const NOT_DONE: readonly L2NotDone[] = [\n // EMPTY, and that is the goal state: every row in the table is a row the guards actually honour.\n //\n // It held three entries. Row 5's `B` half shipped and then MOVED: on `main`, a write is judged from\n // the branch alone (row 5, above the divider) while Bash is judged on freshness beside the Read tool\n // (rows 6/7), because a build on a CURRENT `main` harms nothing and denying it stranded agents on\n // the very `main` `pnpm wp-sync-main` had just handed them. Rows 6 and 8 held DIRTY-TREE\n // valves, and both are now closed — each of\n // those rows cures with `git checkout -b <new> origin/main`, which carries uncommitted changes onto\n // the new branch, so a dirty tree never trapped anybody. The row 6 entry claimed the dirty argument\n // \"has teeth\" there because its cure is `git pull`; that was a fact about the MESSAGE, which printed\n // only the pull, and the fix was to print both cures rather than to suppress the block.\n //\n // Keep this array. An empty \"Not done\" is a claim worth making explicitly — the doc says so in as\n // many words — and the next divergence between a row and its code belongs here, not in prose.\n];\n"]}
|