@webpieces/ai-hook-rules 0.4.593 → 0.4.595
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/adapters/claude-code-response.d.ts +1 -1
- package/src/adapters/claude-code-response.js +7 -2
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +18 -8
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +5 -3
- package/src/bin/shim-audit-log.js +7 -4
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim.js +5 -4
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +12 -4
- package/src/core/decision-log.js +25 -6
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-fault-codes.d.ts +33 -0
- package/src/core/l0-fault-codes.js +61 -0
- package/src/core/l0-fault-codes.js.map +1 -0
- package/src/core/l0-matrix.js +8 -7
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l1-doc.d.ts +7 -0
- package/src/core/l1-doc.js +259 -0
- package/src/core/l1-doc.js.map +1 -0
- package/src/core/l1-rows.d.ts +141 -0
- package/src/core/l1-rows.js +229 -0
- package/src/core/l1-rows.js.map +1 -0
- package/src/core/rejection-log.js +5 -1
- package/src/core/rejection-log.js.map +1 -1
- package/src/core/rules/pr-merge-guard.js +8 -6
- package/src/core/rules/pr-merge-guard.js.map +1 -1
- package/src/core/runner.js +45 -8
- package/src/core/runner.js.map +1 -1
- package/src/core/types.d.ts +11 -1
- package/src/core/types.js +13 -1
- package/src/core/types.js.map +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { TreeKind } from './effective-tree';
|
|
2
|
+
/**
|
|
3
|
+
* The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.
|
|
4
|
+
*
|
|
5
|
+
* `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the
|
|
6
|
+
* pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.
|
|
7
|
+
*/
|
|
8
|
+
export type L1Kind = 'f' | 'o' | 'p' | 'w';
|
|
9
|
+
/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */
|
|
10
|
+
export type L1KindMatch = 'f' | 'o' | 'w' | 'pw' | '-';
|
|
11
|
+
/** R and G are yes/no, written `y`/`n` in the doc, with `-` for "does not matter". */
|
|
12
|
+
export type L1Flag = 'y' | 'n' | '-';
|
|
13
|
+
/** A is the same one boolean wearing the doc's own letters: `c` the coordinator, `s` a subagent. */
|
|
14
|
+
export type L1Agent = 'c' | 's' | '-';
|
|
15
|
+
/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */
|
|
16
|
+
export type L1ActionKind = 'exempt' | 'down' | 'block';
|
|
17
|
+
/**
|
|
18
|
+
* WHICH structural block a blocking row dispatches to. This is the field that makes the array
|
|
19
|
+
* load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,
|
|
20
|
+
* so deleting a row from the array removes the block.
|
|
21
|
+
*/
|
|
22
|
+
export type L1BlockId = 'coordinator-in-worktree' | 'force-to-root';
|
|
23
|
+
/**
|
|
24
|
+
* One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
|
|
25
|
+
*
|
|
26
|
+
* The dimensions are exactly the doc's legend: K (tree kind of the resolved target), A (coordinator or
|
|
27
|
+
* subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).
|
|
28
|
+
*/
|
|
29
|
+
export declare class L1Classification {
|
|
30
|
+
readonly kind: L1Kind;
|
|
31
|
+
readonly coordinator: boolean;
|
|
32
|
+
readonly readOnly: boolean;
|
|
33
|
+
readonly git: boolean;
|
|
34
|
+
readonly atRoot: boolean;
|
|
35
|
+
constructor(kind: L1Kind, coordinator: boolean, readOnly: boolean, git: boolean, atRoot: boolean);
|
|
36
|
+
/**
|
|
37
|
+
* The classification the RUNNER enforces on, built from the resolved tree and the caller.
|
|
38
|
+
*
|
|
39
|
+
* `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by
|
|
40
|
+
* effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is
|
|
41
|
+
* judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what
|
|
42
|
+
* SHOULD happen and is deliberately unreachable from here until the "Not done" fix in
|
|
43
|
+
* guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,
|
|
44
|
+
* so the two ship together or neither does. Mapping it to `p` here is what preserves today's
|
|
45
|
+
* behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.
|
|
46
|
+
*/
|
|
47
|
+
static forEnforcement(treeKind: TreeKind, coordinator: boolean, readOnly: boolean, git: boolean, atRoot: boolean): L1Classification;
|
|
48
|
+
}
|
|
49
|
+
/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */
|
|
50
|
+
export declare class L1Action {
|
|
51
|
+
readonly label: string;
|
|
52
|
+
readonly kind: L1ActionKind;
|
|
53
|
+
constructor(label: string, kind: L1ActionKind);
|
|
54
|
+
}
|
|
55
|
+
export declare const ACT_EXEMPT: L1Action;
|
|
56
|
+
export declare const ACT_DOWN: L1Action;
|
|
57
|
+
export declare const ACT_BLOCK: L1Action;
|
|
58
|
+
/**
|
|
59
|
+
* The CURE a blocking row prescribes.
|
|
60
|
+
*
|
|
61
|
+
* `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,
|
|
62
|
+
* actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — "spawn a
|
|
63
|
+
* subagent bound to the worktree" — which no allowlist can accept and no reclassification can model,
|
|
64
|
+
* so it declares `runnable: false` and is asserted only on the deny text.
|
|
65
|
+
*/
|
|
66
|
+
export declare class L1Cure {
|
|
67
|
+
/** How the doc's `why` column spells it. */
|
|
68
|
+
readonly summary: string;
|
|
69
|
+
/** A substring that MUST appear in the deny text the guard emits for this row. */
|
|
70
|
+
readonly denyMention: string;
|
|
71
|
+
readonly runnable: boolean;
|
|
72
|
+
constructor(
|
|
73
|
+
/** How the doc's `why` column spells it. */
|
|
74
|
+
summary: string,
|
|
75
|
+
/** A substring that MUST appear in the deny text the guard emits for this row. */
|
|
76
|
+
denyMention: string, runnable: boolean);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One row of the "L1 use cases" table: what you SEE, the state it puts you in, the verdict, the fix.
|
|
80
|
+
*
|
|
81
|
+
* The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed
|
|
82
|
+
* in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement
|
|
83
|
+
* data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither
|
|
84
|
+
* of which is a row) can carry `null` there.
|
|
85
|
+
*/
|
|
86
|
+
export declare class L1UseCase {
|
|
87
|
+
readonly num: number;
|
|
88
|
+
readonly symptom: string;
|
|
89
|
+
readonly state: string;
|
|
90
|
+
readonly verdict: string;
|
|
91
|
+
readonly fix: string;
|
|
92
|
+
readonly classification: L1Classification | null;
|
|
93
|
+
constructor(num: number, symptom: string, state: string, verdict: string, fix: string, classification?: L1Classification | null);
|
|
94
|
+
}
|
|
95
|
+
/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */
|
|
96
|
+
export declare class L1Row {
|
|
97
|
+
readonly num: number;
|
|
98
|
+
readonly k: L1KindMatch;
|
|
99
|
+
readonly a: L1Agent;
|
|
100
|
+
readonly r: L1Flag;
|
|
101
|
+
readonly g: L1Flag;
|
|
102
|
+
readonly p: 'root' | 'sub' | '-';
|
|
103
|
+
readonly action: L1Action;
|
|
104
|
+
/** The `why` cell, verbatim. */
|
|
105
|
+
readonly why: string;
|
|
106
|
+
readonly cure: L1Cure | null;
|
|
107
|
+
readonly blockId: L1BlockId | null;
|
|
108
|
+
readonly useCases: readonly L1UseCase[];
|
|
109
|
+
constructor(num: number, k: L1KindMatch, a: L1Agent, r: L1Flag, g: L1Flag, p: 'root' | 'sub' | '-', action: L1Action,
|
|
110
|
+
/** The `why` cell, verbatim. */
|
|
111
|
+
why: string, cure: L1Cure | null, blockId: L1BlockId | null, useCases: readonly L1UseCase[]);
|
|
112
|
+
matches(c: L1Classification): boolean;
|
|
113
|
+
private kindMatches;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* THE six L1 rows, in first-match-wins order.
|
|
117
|
+
*
|
|
118
|
+
* Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they
|
|
119
|
+
* can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. "L1 has no
|
|
120
|
+
* objection" — which is why only rows 3 and 5 carry a blockId.
|
|
121
|
+
*/
|
|
122
|
+
export declare const L1_ROWS: readonly L1Row[];
|
|
123
|
+
/**
|
|
124
|
+
* The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0
|
|
125
|
+
* allowlist that runs ahead of L1 (15).
|
|
126
|
+
*
|
|
127
|
+
* They are use cases of L1 all the same — "exempt" is what emerges when the filter empties the rule
|
|
128
|
+
* list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the
|
|
129
|
+
* doc's one numbered table. They carry no classification because no row classifies them.
|
|
130
|
+
*/
|
|
131
|
+
export declare const L1_UNROWED_USE_CASES: readonly L1UseCase[];
|
|
132
|
+
/** Every use case, in the doc's numbering — the order the table is rendered and read in. */
|
|
133
|
+
export declare function allL1UseCases(): readonly L1UseCase[];
|
|
134
|
+
/**
|
|
135
|
+
* FIRST MATCH WINS — the one lookup the guard and the tests share.
|
|
136
|
+
*
|
|
137
|
+
* Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a
|
|
138
|
+
* classification that matched nothing would be a hole in the matrix. The totality test asserts exactly
|
|
139
|
+
* that, which is why this returns L1Row rather than L1Row | null.
|
|
140
|
+
*/
|
|
141
|
+
export declare function firstMatchingL1Row(c: L1Classification): L1Row;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.L1_UNROWED_USE_CASES = exports.L1_ROWS = exports.L1Row = exports.L1UseCase = exports.L1Cure = exports.ACT_BLOCK = exports.ACT_DOWN = exports.ACT_EXEMPT = exports.L1Action = exports.L1Classification = void 0;
|
|
4
|
+
exports.allL1UseCases = allL1UseCases;
|
|
5
|
+
exports.firstMatchingL1Row = firstMatchingL1Row;
|
|
6
|
+
/**
|
|
7
|
+
* One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
|
|
8
|
+
*
|
|
9
|
+
* The dimensions are exactly the doc's legend: K (tree kind of the resolved target), A (coordinator or
|
|
10
|
+
* subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).
|
|
11
|
+
*/
|
|
12
|
+
class L1Classification {
|
|
13
|
+
kind;
|
|
14
|
+
coordinator;
|
|
15
|
+
readOnly;
|
|
16
|
+
git;
|
|
17
|
+
atRoot;
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape
|
|
19
|
+
constructor(kind, coordinator, readOnly, git, atRoot) {
|
|
20
|
+
this.kind = kind;
|
|
21
|
+
this.coordinator = coordinator;
|
|
22
|
+
this.readOnly = readOnly;
|
|
23
|
+
this.git = git;
|
|
24
|
+
this.atRoot = atRoot;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The classification the RUNNER enforces on, built from the resolved tree and the caller.
|
|
28
|
+
*
|
|
29
|
+
* `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by
|
|
30
|
+
* effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is
|
|
31
|
+
* judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what
|
|
32
|
+
* SHOULD happen and is deliberately unreachable from here until the "Not done" fix in
|
|
33
|
+
* guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,
|
|
34
|
+
* so the two ship together or neither does. Mapping it to `p` here is what preserves today's
|
|
35
|
+
* behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.
|
|
36
|
+
*/
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to
|
|
38
|
+
// webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject
|
|
39
|
+
static forEnforcement(treeKind, coordinator, readOnly, git, atRoot) {
|
|
40
|
+
const kind = treeKind === 'foreign' ? 'f' : treeKind === 'worktree' ? 'w' : 'p';
|
|
41
|
+
return new L1Classification(kind, coordinator, readOnly, git, atRoot);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.L1Classification = L1Classification;
|
|
45
|
+
/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */
|
|
46
|
+
class L1Action {
|
|
47
|
+
label;
|
|
48
|
+
kind;
|
|
49
|
+
constructor(label, kind) {
|
|
50
|
+
this.label = label;
|
|
51
|
+
this.kind = kind;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.L1Action = L1Action;
|
|
55
|
+
exports.ACT_EXEMPT = new L1Action('2 exempt', 'exempt');
|
|
56
|
+
exports.ACT_DOWN = new L1Action('→ L2', 'down');
|
|
57
|
+
exports.ACT_BLOCK = new L1Action('4 block', 'block');
|
|
58
|
+
/**
|
|
59
|
+
* The CURE a blocking row prescribes.
|
|
60
|
+
*
|
|
61
|
+
* `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,
|
|
62
|
+
* actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — "spawn a
|
|
63
|
+
* subagent bound to the worktree" — which no allowlist can accept and no reclassification can model,
|
|
64
|
+
* so it declares `runnable: false` and is asserted only on the deny text.
|
|
65
|
+
*/
|
|
66
|
+
class L1Cure {
|
|
67
|
+
summary;
|
|
68
|
+
denyMention;
|
|
69
|
+
runnable;
|
|
70
|
+
constructor(
|
|
71
|
+
/** How the doc's `why` column spells it. */
|
|
72
|
+
summary,
|
|
73
|
+
/** A substring that MUST appear in the deny text the guard emits for this row. */
|
|
74
|
+
denyMention, runnable) {
|
|
75
|
+
this.summary = summary;
|
|
76
|
+
this.denyMention = denyMention;
|
|
77
|
+
this.runnable = runnable;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.L1Cure = L1Cure;
|
|
81
|
+
/**
|
|
82
|
+
* One row of the "L1 use cases" table: what you SEE, the state it puts you in, the verdict, the fix.
|
|
83
|
+
*
|
|
84
|
+
* The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed
|
|
85
|
+
* in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement
|
|
86
|
+
* data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither
|
|
87
|
+
* of which is a row) can carry `null` there.
|
|
88
|
+
*/
|
|
89
|
+
class L1UseCase {
|
|
90
|
+
num;
|
|
91
|
+
symptom;
|
|
92
|
+
state;
|
|
93
|
+
verdict;
|
|
94
|
+
fix;
|
|
95
|
+
classification;
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification behind them
|
|
97
|
+
constructor(num, symptom, state, verdict, fix, classification = null) {
|
|
98
|
+
this.num = num;
|
|
99
|
+
this.symptom = symptom;
|
|
100
|
+
this.state = state;
|
|
101
|
+
this.verdict = verdict;
|
|
102
|
+
this.fix = fix;
|
|
103
|
+
this.classification = classification;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.L1UseCase = L1UseCase;
|
|
107
|
+
/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */
|
|
108
|
+
class L1Row {
|
|
109
|
+
num;
|
|
110
|
+
k;
|
|
111
|
+
a;
|
|
112
|
+
r;
|
|
113
|
+
g;
|
|
114
|
+
p;
|
|
115
|
+
action;
|
|
116
|
+
why;
|
|
117
|
+
cure;
|
|
118
|
+
blockId;
|
|
119
|
+
useCases;
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases
|
|
121
|
+
constructor(num, k, a, r, g, p, action,
|
|
122
|
+
/** The `why` cell, verbatim. */
|
|
123
|
+
why, cure, blockId, useCases) {
|
|
124
|
+
this.num = num;
|
|
125
|
+
this.k = k;
|
|
126
|
+
this.a = a;
|
|
127
|
+
this.r = r;
|
|
128
|
+
this.g = g;
|
|
129
|
+
this.p = p;
|
|
130
|
+
this.action = action;
|
|
131
|
+
this.why = why;
|
|
132
|
+
this.cure = cure;
|
|
133
|
+
this.blockId = blockId;
|
|
134
|
+
this.useCases = useCases;
|
|
135
|
+
}
|
|
136
|
+
matches(c) {
|
|
137
|
+
if (!this.kindMatches(c.kind))
|
|
138
|
+
return false;
|
|
139
|
+
if (this.a !== '-' && (this.a === 'c') !== c.coordinator)
|
|
140
|
+
return false;
|
|
141
|
+
if (!flagMatches(this.r, c.readOnly))
|
|
142
|
+
return false;
|
|
143
|
+
if (!flagMatches(this.g, c.git))
|
|
144
|
+
return false;
|
|
145
|
+
return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');
|
|
146
|
+
}
|
|
147
|
+
kindMatches(kind) {
|
|
148
|
+
if (this.k === '-')
|
|
149
|
+
return true;
|
|
150
|
+
if (this.k === 'pw')
|
|
151
|
+
return kind === 'p' || kind === 'w';
|
|
152
|
+
return this.k === kind;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.L1Row = L1Row;
|
|
156
|
+
// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the
|
|
157
|
+
// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)
|
|
158
|
+
// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module
|
|
159
|
+
function flagMatches(cell, value) {
|
|
160
|
+
if (cell === '-')
|
|
161
|
+
return true;
|
|
162
|
+
return (cell === 'y') === value;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* THE six L1 rows, in first-match-wins order.
|
|
166
|
+
*
|
|
167
|
+
* Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they
|
|
168
|
+
* can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. "L1 has no
|
|
169
|
+
* objection" — which is why only rows 3 and 5 carry a blockId.
|
|
170
|
+
*/
|
|
171
|
+
exports.L1_ROWS = [
|
|
172
|
+
new L1Row(1, 'f', '-', '-', '-', '-', exports.ACT_EXEMPT, 'different git repo — hands off', null, null, [
|
|
173
|
+
new L1UseCase(1, '`cd repositories/vendored && git commit` goes through untouched', '`f` / `y` / - — row 1', 'ALLOW_EXEMPT', 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off', new L1Classification('f', false, false, true, false)),
|
|
174
|
+
]),
|
|
175
|
+
new L1Row(2, 'o', '-', '-', '-', '-', exports.ACT_DOWN, 'see "Not done" below', null, null, []),
|
|
176
|
+
new L1Row(3, 'w', 'c', 'n', '-', '-', exports.ACT_BLOCK, 'the coordinator\'s guards do not follow its `cd` — delegate to a subagent bound to the worktree', new L1Cure('delegate to a subagent bound to the worktree', 'Spawn a subagent bound to that worktree', false), 'coordinator-in-worktree', [
|
|
177
|
+
new L1UseCase(12, 'you are the **coordinator**, you ran `git worktree add ../wt`, and `cd ../wt && pnpm build` is blocked', '`w` / `c` / `n` — row 3', 'BLOCK', 'Option 1 (preferred): spawn a subagent bound to `<worktree>` — the Agent tool with worktree isolation, or have the subagent call `EnterWorktree` with `path: <worktree>` (it accepts a worktree you already created)<br>Do NOT: re-type the command, or conclude the harness ate your `cd` — it did not; your GUARDS did not follow it', new L1Classification('w', true, false, false, false)),
|
|
178
|
+
]),
|
|
179
|
+
new L1Row(4, 'pw', '-', '-', 'n', '-', exports.ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [
|
|
180
|
+
new L1UseCase(5, '`ls` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', '→ L2', 'none — force-to-root has no jurisdiction over non-git commands', new L1Classification('p', false, true, false, false)),
|
|
181
|
+
new L1UseCase(6, '`pnpm test` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', '→ L2', 'none — deliberately untouched, so package-local test runs stay natural', new L1Classification('p', false, false, false, false)),
|
|
182
|
+
new L1UseCase(10, '`echo "cd sub && git push"` passes', '`pw` / `n` / `root` — row 4', '→ L2', 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape', new L1Classification('p', false, false, false, true)),
|
|
183
|
+
new L1UseCase(13, 'the same command from a **subagent** runs normally', '`w` / `s` — row 3 does not match', '→ L2', 'none — a subagent pinned to a worktree is the correct pattern', new L1Classification('w', false, false, false, false)),
|
|
184
|
+
new L1UseCase(14, 'the coordinator\'s `cd <worktree> && ls`/`cat`/`grep` still runs', '`w` / `c` / `y` — row 3 does not match', '→ L2', 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you', new L1Classification('w', true, true, false, false)),
|
|
185
|
+
]),
|
|
186
|
+
new L1Row(5, 'pw', '-', '-', 'y', 'sub', exports.ACT_BLOCK, '`cd <root> && <original>`', new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true), 'force-to-root', [
|
|
187
|
+
new L1UseCase(7, '`git status` from `packages/http/` is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK', 'Option 1 (preferred): `cd <root> && git status`', new L1Classification('p', false, false, true, false)),
|
|
188
|
+
new L1UseCase(8, '`cd packages/http && git status` **typed from the root** is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK', 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION', new L1Classification('p', false, false, true, false)),
|
|
189
|
+
new L1UseCase(11, '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one', '`pw` / `y` / `sub` — row 5; force-to-root runs first', 'BLOCK', 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\'s real answer ← costs one extra turn by design; still blocked', new L1Classification('p', false, false, true, false)),
|
|
190
|
+
]),
|
|
191
|
+
new L1Row(6, 'pw', '-', '-', 'y', 'root', exports.ACT_DOWN, '', null, null, [
|
|
192
|
+
new L1UseCase(9, '`cd <root> && git status` passes from anywhere', '`pw` / `y` / `root` — row 6', '→ L2', 'none — this IS the prescribed cure', new L1Classification('p', false, false, true, true)),
|
|
193
|
+
]),
|
|
194
|
+
];
|
|
195
|
+
/**
|
|
196
|
+
* The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0
|
|
197
|
+
* allowlist that runs ahead of L1 (15).
|
|
198
|
+
*
|
|
199
|
+
* They are use cases of L1 all the same — "exempt" is what emerges when the filter empties the rule
|
|
200
|
+
* list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the
|
|
201
|
+
* doc's one numbered table. They carry no classification because no row classifies them.
|
|
202
|
+
*/
|
|
203
|
+
exports.L1_UNROWED_USE_CASES = [
|
|
204
|
+
new L1UseCase(2, 'Edit `repositories/vendored/foo.ts` allowed even on stale main', 'filter — the path is in `excludePaths`', 'ALLOW_EXEMPT', 'none needed'),
|
|
205
|
+
new L1UseCase(3, 'Edit `packages/http/foo.ts` blocked on stale main', 'filter keeps the rules → L2 fires', 'BLOCK (at L2)', 'that is L2\'s write-on-main verdict, not L1\'s — follow the L2 message'),
|
|
206
|
+
new L1UseCase(4, 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`', 'filter, on the TARGET path', '→ L2', 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to "fix" it'),
|
|
207
|
+
new L1UseCase(15, 'the coordinator\'s `cd <worktree> && pnpm install` still runs while row 3 is live', 'L0 allowlist, ahead of L1', 'ALLOW', 'none — a cure must stay reachable from every tree'),
|
|
208
|
+
];
|
|
209
|
+
/** Every use case, in the doc's numbering — the order the table is rendered and read in. */
|
|
210
|
+
// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module
|
|
211
|
+
function allL1UseCases() {
|
|
212
|
+
const all = [...exports.L1_ROWS.flatMap((row) => row.useCases), ...exports.L1_UNROWED_USE_CASES];
|
|
213
|
+
return all.sort((a, b) => a.num - b.num);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* FIRST MATCH WINS — the one lookup the guard and the tests share.
|
|
217
|
+
*
|
|
218
|
+
* Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a
|
|
219
|
+
* classification that matched nothing would be a hole in the matrix. The totality test asserts exactly
|
|
220
|
+
* that, which is why this returns L1Row rather than L1Row | null.
|
|
221
|
+
*/
|
|
222
|
+
// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads
|
|
223
|
+
function firstMatchingL1Row(c) {
|
|
224
|
+
const row = exports.L1_ROWS.find((r) => r.matches(c));
|
|
225
|
+
if (row === undefined)
|
|
226
|
+
throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);
|
|
227
|
+
return row;
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=l1-rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers three questions: do we govern this call at all, is the WRONG AGENT standing here, and is\n// the agent stranded away from the root? Drawn as a decision matrix that is SIX ordered rows over five\n// dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/** A is the same one boolean wearing the doc's own letters: `c` the coordinator, `s` a subagent. */\nexport type L1Agent = 'c' | 's' | '-';\n\n/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */\nexport type L1ActionKind = 'exempt' | 'down' | 'block';\n\n/**\n * WHICH structural block a blocking row dispatches to. This is the field that makes the array\n * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,\n * so deleting a row from the array removes the block.\n */\nexport type L1BlockId = 'coordinator-in-worktree' | 'force-to-root';\n\n/**\n * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.\n *\n * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), A (coordinator or\n * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).\n */\nexport class L1Classification {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape\n constructor(\n readonly kind: L1Kind,\n readonly coordinator: boolean,\n readonly readOnly: boolean,\n readonly git: boolean,\n readonly atRoot: boolean,\n ) {}\n\n /**\n * The classification the RUNNER enforces on, built from the resolved tree and the caller.\n *\n * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by\n * effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is\n * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what\n * SHOULD happen and is deliberately unreachable from here until the \"Not done\" fix in\n * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,\n * so the two ship together or neither does. Mapping it to `p` here is what preserves today's\n * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.\n */\n // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to\n // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject\n static forEnforcement(\n treeKind: TreeKind,\n coordinator: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f' : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, coordinator, readOnly, git, atRoot);\n }\n}\n\n/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L1Action {\n constructor(readonly label: string, readonly kind: L1ActionKind) {}\n}\n\nexport const ACT_EXEMPT = new L1Action('2 exempt', 'exempt');\nexport const ACT_DOWN = new L1Action('→ L2', 'down');\nexport const ACT_BLOCK = new L1Action('4 block', 'block');\n\n/**\n * The CURE a blocking row prescribes.\n *\n * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,\n * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — \"spawn a\n * subagent bound to the worktree\" — which no allowlist can accept and no reclassification can model,\n * so it declares `runnable: false` and is asserted only on the deny text.\n */\nexport class L1Cure {\n constructor(\n /** How the doc's `why` column spells it. */\n readonly summary: string,\n /** A substring that MUST appear in the deny text the guard emits for this row. */\n readonly denyMention: string,\n readonly runnable: boolean,\n ) {}\n}\n\n/**\n * One row of the \"L1 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed\n * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement\n * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither\n * of which is a row) can carry `null` there.\n */\nexport class L1UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly classification: L1Classification | null = null,\n ) {}\n}\n\n/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */\nexport class L1Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases\n constructor(\n readonly num: number,\n readonly k: L1KindMatch,\n readonly a: L1Agent,\n readonly r: L1Flag,\n readonly g: L1Flag,\n readonly p: 'root' | 'sub' | '-',\n readonly action: L1Action,\n /** The `why` cell, verbatim. */\n readonly why: string,\n readonly cure: L1Cure | null,\n readonly blockId: L1BlockId | null,\n readonly useCases: readonly L1UseCase[],\n ) {}\n\n matches(c: L1Classification): boolean {\n if (!this.kindMatches(c.kind)) return false;\n if (this.a !== '-' && (this.a === 'c') !== c.coordinator) return false;\n if (!flagMatches(this.r, c.readOnly)) return false;\n if (!flagMatches(this.g, c.git)) return false;\n return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');\n }\n\n private kindMatches(kind: L1Kind): boolean {\n if (this.k === '-') return true;\n if (this.k === 'pw') return kind === 'p' || kind === 'w';\n return this.k === kind;\n }\n}\n\n// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the\n// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)\n// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module\nfunction flagMatches(cell: L1Flag, value: boolean): boolean {\n if (cell === '-') return true;\n return (cell === 'y') === value;\n}\n\n/**\n * THE six L1 rows, in first-match-wins order.\n *\n * Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they\n * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. \"L1 has no\n * objection\" — which is why only rows 3 and 5 carry a blockId.\n */\nexport const L1_ROWS: readonly L1Row[] = [\n new L1Row(1, 'f', '-', '-', '-', '-', ACT_EXEMPT, 'different git repo — hands off', null, null, [\n new L1UseCase(1,\n '`cd repositories/vendored && git commit` goes through untouched',\n '`f` / `y` / - — row 1',\n 'ALLOW_EXEMPT',\n 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off',\n new L1Classification('f', false, false, true, false)),\n ]),\n new L1Row(2, 'o', '-', '-', '-', '-', ACT_DOWN, 'see \"Not done\" below', null, null, []),\n new L1Row(3, 'w', 'c', 'n', '-', '-', ACT_BLOCK,\n 'the coordinator\\'s guards do not follow its `cd` — delegate to a subagent bound to the worktree',\n new L1Cure('delegate to a subagent bound to the worktree', 'Spawn a subagent bound to that worktree', false),\n 'coordinator-in-worktree', [\n new L1UseCase(12,\n 'you are the **coordinator**, you ran `git worktree add ../wt`, and `cd ../wt && pnpm build` is blocked',\n '`w` / `c` / `n` — row 3',\n 'BLOCK',\n 'Option 1 (preferred): spawn a subagent bound to `<worktree>` — the Agent tool with worktree isolation, or have the subagent call `EnterWorktree` with `path: <worktree>` (it accepts a worktree you already created)<br>Do NOT: re-type the command, or conclude the harness ate your `cd` — it did not; your GUARDS did not follow it',\n new L1Classification('w', true, false, false, false)),\n ]),\n new L1Row(4, 'pw', '-', '-', 'n', '-', ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [\n new L1UseCase(5,\n '`ls` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n '→ L2',\n 'none — force-to-root has no jurisdiction over non-git commands',\n new L1Classification('p', false, true, false, false)),\n new L1UseCase(6,\n '`pnpm test` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n '→ L2',\n 'none — deliberately untouched, so package-local test runs stay natural',\n new L1Classification('p', false, false, false, false)),\n new L1UseCase(10,\n '`echo \"cd sub && git push\"` passes',\n '`pw` / `n` / `root` — row 4',\n '→ L2',\n 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape',\n new L1Classification('p', false, false, false, true)),\n new L1UseCase(13,\n 'the same command from a **subagent** runs normally',\n '`w` / `s` — row 3 does not match',\n '→ L2',\n 'none — a subagent pinned to a worktree is the correct pattern',\n new L1Classification('w', false, false, false, false)),\n new L1UseCase(14,\n 'the coordinator\\'s `cd <worktree> && ls`/`cat`/`grep` still runs',\n '`w` / `c` / `y` — row 3 does not match',\n '→ L2',\n 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you',\n new L1Classification('w', true, true, false, false)),\n ]),\n new L1Row(5, 'pw', '-', '-', 'y', 'sub', ACT_BLOCK, '`cd <root> && <original>`',\n new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true),\n 'force-to-root', [\n new L1UseCase(7,\n '`git status` from `packages/http/` is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK',\n 'Option 1 (preferred): `cd <root> && git status`',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(8,\n '`cd packages/http && git status` **typed from the root** is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK',\n 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(11,\n '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one',\n '`pw` / `y` / `sub` — row 5; force-to-root runs first',\n 'BLOCK',\n 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\\'s real answer ← costs one extra turn by design; still blocked',\n new L1Classification('p', false, false, true, false)),\n ]),\n new L1Row(6, 'pw', '-', '-', 'y', 'root', ACT_DOWN, '', null, null, [\n new L1UseCase(9,\n '`cd <root> && git status` passes from anywhere',\n '`pw` / `y` / `root` — row 6',\n '→ L2',\n 'none — this IS the prescribed cure',\n new L1Classification('p', false, false, true, true)),\n ]),\n];\n\n/**\n * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0\n * allowlist that runs ahead of L1 (15).\n *\n * They are use cases of L1 all the same — \"exempt\" is what emerges when the filter empties the rule\n * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the\n * doc's one numbered table. They carry no classification because no row classifies them.\n */\nexport const L1_UNROWED_USE_CASES: readonly L1UseCase[] = [\n new L1UseCase(2,\n 'Edit `repositories/vendored/foo.ts` allowed even on stale main',\n 'filter — the path is in `excludePaths`',\n 'ALLOW_EXEMPT',\n 'none needed'),\n new L1UseCase(3,\n 'Edit `packages/http/foo.ts` blocked on stale main',\n 'filter keeps the rules → L2 fires',\n 'BLOCK (at L2)',\n 'that is L2\\'s write-on-main verdict, not L1\\'s — follow the L2 message'),\n new L1UseCase(4,\n 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`',\n 'filter, on the TARGET path',\n '→ L2',\n 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to \"fix\" it'),\n new L1UseCase(15,\n 'the coordinator\\'s `cd <worktree> && pnpm install` still runs while row 3 is live',\n 'L0 allowlist, ahead of L1',\n 'ALLOW',\n 'none — a cure must stay reachable from every tree'),\n];\n\n/** Every use case, in the doc's numbering — the order the table is rendered and read in. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module\nexport function allL1UseCases(): readonly L1UseCase[] {\n const all = [...L1_ROWS.flatMap((row: L1Row): readonly L1UseCase[] => row.useCases), ...L1_UNROWED_USE_CASES];\n return all.sort((a: L1UseCase, b: L1UseCase): number => a.num - b.num);\n}\n\n/**\n * FIRST MATCH WINS — the one lookup the guard and the tests share.\n *\n * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a\n * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly\n * that, which is why this returns L1Row rather than L1Row | null.\n */\n// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads\nexport function firstMatchingL1Row(c: L1Classification): L1Row {\n const row = L1_ROWS.find((r: L1Row): boolean => r.matches(c));\n if (row === undefined) throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);\n return row;\n}\n"]}
|
|
@@ -43,7 +43,11 @@ function logRejection(toolKind, input, result, cwd) {
|
|
|
43
43
|
fs.writeFileSync(path.join(dayDir, detailFileName), detail);
|
|
44
44
|
const logPath = path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE));
|
|
45
45
|
rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE_PREV)));
|
|
46
|
-
|
|
46
|
+
// APPEND-ONLY, and `fault=` is spelled exactly as the L0 sh log spells it. A fault-S storm
|
|
47
|
+
// previously landed here as a couple of lines attributed to whatever rule the report happened
|
|
48
|
+
// to cite, with nothing anywhere identifying L0 as the cause. (WHO made the call is answered by
|
|
49
|
+
// the filename, which logStream prefixes with session/agent/hook — not by a column here.)
|
|
50
|
+
const logLine = `[${timestamp}]\t${toolKind}\t${relativePath}\t[${ruleNames.join(',')}]\t${detailRelPath}\tfault=${result.fault}\n`;
|
|
47
51
|
fs.appendFileSync(logPath, logLine);
|
|
48
52
|
rotateOldDays(hooksDir, MAX_AGE_DAYS);
|
|
49
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rejection-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/rejection-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, RepoRootFinder, HOOKS_STATE_DIR } from '@webpieces/rules-config';\n\nimport type { ToolKind, NormalizedToolInput, BlockedResult } from './types';\nimport { logStream } from './log-stream';\n\n// The rejection log SPLITS across the two state dirs on purpose: the `.log` index goes to `logs/`\n// with every other webpieces log, while the dated `hooks/<YYYY-MM-DD>/writeInfo-*.md` DETAIL files\n// are not logs and stay in `hooks/` (see LOGS_STATE_DIR).\nconst LOG_FILE = 'hook-rejection.log';\nconst LOG_FILE_PREV = 'hook-rejection.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded\nconst MAX_AGE_DAYS = 7;\n\nconst RULE_NAME_RE = /^\\[([^\\]]+)\\] \\(/gm;\n\nexport function logRejection(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n result: BlockedResult,\n cwd: string,\n): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // `.webpieces/` lives at the repo root, NOT the AI's cwd — resolve it so a hook fired while\n // the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const now = new Date();\n const timestamp = now.toISOString();\n const epochMs = String(now.getTime());\n const dateStr = timestamp.slice(0, 10);\n\n // LOCAL scope — a rejection is this worktree's event, and a per-worktree log has exactly one\n // writer, so its appends and its daily detail files cannot collide with another agent's.\n const hooksDir = dotWebpieces.localFile(root, HOOKS_STATE_DIR);\n const logsDir = dotWebpieces.logs(root);\n const dayDir = path.join(hooksDir, dateStr);\n fs.mkdirSync(dayDir, { recursive: true });\n fs.mkdirSync(logsDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `writeInfo-${epochMs}.md`;\n // Relative to the STATE DIR, not to the log file's own directory: the index now lives in\n // `logs/` while the detail lives in `hooks/<date>/`, so a bare `<date>/<file>` would no\n // longer resolve from where the reader found the line.\n const detailRelPath = `${HOOKS_STATE_DIR}/${dateStr}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(dayDir, detailFileName), detail);\n\n const logPath = path.join(logsDir, logStream.fileName(LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(LOG_FILE_PREV)));\n\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n rotateOldDays(hooksDir, MAX_AGE_DAYS);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction computeRelativePath(filePath: string, cwd: string): string {\n if (filePath.startsWith(cwd)) {\n const rel = filePath.slice(cwd.length);\n if (rel.startsWith('/')) return rel.slice(1);\n return rel;\n }\n return filePath;\n}\n\n/**\n * The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because\n * two audit streams need the same answer from the same regex: this file's rejection index, and the\n * `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be\n * two answers to one question.\n */\n// webpieces-disable no-function-outside-class -- pure regex scraper beside this module's other module-scope helpers; exported so the invocation log and the rejection index scrape rule names with the SAME code.\nexport function extractRuleNames(report: string): string[] {\n const names: string[] = [];\n let match = RULE_NAME_RE.exec(report);\n while (match !== null) {\n names.push(match[1]);\n match = RULE_NAME_RE.exec(report);\n }\n RULE_NAME_RE.lastIndex = 0;\n return names;\n}\n\nfunction buildDetailContent(\n timestamp: string,\n toolKind: ToolKind,\n relativePath: string,\n ruleNames: string[],\n report: string,\n input: NormalizedToolInput,\n): string {\n const lines: string[] = [];\n lines.push('# Hook Rejection Detail');\n lines.push('');\n lines.push(`- **Timestamp:** ${timestamp}`);\n lines.push(`- **Tool:** ${toolKind}`);\n lines.push(`- **File:** ${relativePath}`);\n lines.push(`- **Rules violated:** ${ruleNames.join(', ')}`);\n lines.push('');\n lines.push('## Report');\n lines.push('');\n lines.push('```');\n lines.push(report.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('## Content Being Written');\n lines.push('');\n\n if (toolKind === 'Write') {\n const content = input.edits.length > 0 ? input.edits[0].newString : '';\n lines.push('```typescript');\n lines.push(content.trimEnd());\n lines.push('```');\n } else {\n for (let i = 0; i < input.edits.length; i += 1) {\n const edit = input.edits[i];\n lines.push(`### Edit ${String(i + 1)} of ${String(input.edits.length)}`);\n lines.push('');\n lines.push('**old_string:**');\n lines.push('```typescript');\n lines.push(edit.oldString.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('**new_string:**');\n lines.push('```typescript');\n lines.push(edit.newString.trimEnd());\n lines.push('```');\n lines.push('');\n }\n }\n\n return lines.join('\\n') + '\\n';\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 err;\n }\n}\n\nfunction rotateOldDays(hooksDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(hooksDir);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return;\n }\n\n for (const entry of entries) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(entry)) continue;\n const dirDate = new Date(entry + 'T00:00:00Z');\n if (isNaN(dirDate.getTime())) continue;\n if (dirDate.getTime() < cutoff) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(path.join(hooksDir, entry), { recursive: true, force: true });\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rejection-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/rejection-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, RepoRootFinder, HOOKS_STATE_DIR } from '@webpieces/rules-config';\n\nimport type { ToolKind, NormalizedToolInput, BlockedResult } from './types';\nimport { logStream } from './log-stream';\n\n// The rejection log SPLITS across the two state dirs on purpose: the `.log` index goes to `logs/`\n// with every other webpieces log, while the dated `hooks/<YYYY-MM-DD>/writeInfo-*.md` DETAIL files\n// are not logs and stay in `hooks/` (see LOGS_STATE_DIR).\nconst LOG_FILE = 'hook-rejection.log';\nconst LOG_FILE_PREV = 'hook-rejection.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded\nconst MAX_AGE_DAYS = 7;\n\nconst RULE_NAME_RE = /^\\[([^\\]]+)\\] \\(/gm;\n\nexport function logRejection(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n result: BlockedResult,\n cwd: string,\n): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // `.webpieces/` lives at the repo root, NOT the AI's cwd — resolve it so a hook fired while\n // the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const now = new Date();\n const timestamp = now.toISOString();\n const epochMs = String(now.getTime());\n const dateStr = timestamp.slice(0, 10);\n\n // LOCAL scope — a rejection is this worktree's event, and a per-worktree log has exactly one\n // writer, so its appends and its daily detail files cannot collide with another agent's.\n const hooksDir = dotWebpieces.localFile(root, HOOKS_STATE_DIR);\n const logsDir = dotWebpieces.logs(root);\n const dayDir = path.join(hooksDir, dateStr);\n fs.mkdirSync(dayDir, { recursive: true });\n fs.mkdirSync(logsDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `writeInfo-${epochMs}.md`;\n // Relative to the STATE DIR, not to the log file's own directory: the index now lives in\n // `logs/` while the detail lives in `hooks/<date>/`, so a bare `<date>/<file>` would no\n // longer resolve from where the reader found the line.\n const detailRelPath = `${HOOKS_STATE_DIR}/${dateStr}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(dayDir, detailFileName), detail);\n\n const logPath = path.join(logsDir, logStream.fileName(LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(LOG_FILE_PREV)));\n\n // APPEND-ONLY, and `fault=` is spelled exactly as the L0 sh log spells it. A fault-S storm\n // previously landed here as a couple of lines attributed to whatever rule the report happened\n // to cite, with nothing anywhere identifying L0 as the cause. (WHO made the call is answered by\n // the filename, which logStream prefixes with session/agent/hook — not by a column here.)\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\tfault=${result.fault}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n rotateOldDays(hooksDir, MAX_AGE_DAYS);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction computeRelativePath(filePath: string, cwd: string): string {\n if (filePath.startsWith(cwd)) {\n const rel = filePath.slice(cwd.length);\n if (rel.startsWith('/')) return rel.slice(1);\n return rel;\n }\n return filePath;\n}\n\n/**\n * The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because\n * two audit streams need the same answer from the same regex: this file's rejection index, and the\n * `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be\n * two answers to one question.\n */\n// webpieces-disable no-function-outside-class -- pure regex scraper beside this module's other module-scope helpers; exported so the invocation log and the rejection index scrape rule names with the SAME code.\nexport function extractRuleNames(report: string): string[] {\n const names: string[] = [];\n let match = RULE_NAME_RE.exec(report);\n while (match !== null) {\n names.push(match[1]);\n match = RULE_NAME_RE.exec(report);\n }\n RULE_NAME_RE.lastIndex = 0;\n return names;\n}\n\nfunction buildDetailContent(\n timestamp: string,\n toolKind: ToolKind,\n relativePath: string,\n ruleNames: string[],\n report: string,\n input: NormalizedToolInput,\n): string {\n const lines: string[] = [];\n lines.push('# Hook Rejection Detail');\n lines.push('');\n lines.push(`- **Timestamp:** ${timestamp}`);\n lines.push(`- **Tool:** ${toolKind}`);\n lines.push(`- **File:** ${relativePath}`);\n lines.push(`- **Rules violated:** ${ruleNames.join(', ')}`);\n lines.push('');\n lines.push('## Report');\n lines.push('');\n lines.push('```');\n lines.push(report.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('## Content Being Written');\n lines.push('');\n\n if (toolKind === 'Write') {\n const content = input.edits.length > 0 ? input.edits[0].newString : '';\n lines.push('```typescript');\n lines.push(content.trimEnd());\n lines.push('```');\n } else {\n for (let i = 0; i < input.edits.length; i += 1) {\n const edit = input.edits[i];\n lines.push(`### Edit ${String(i + 1)} of ${String(input.edits.length)}`);\n lines.push('');\n lines.push('**old_string:**');\n lines.push('```typescript');\n lines.push(edit.oldString.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('**new_string:**');\n lines.push('```typescript');\n lines.push(edit.newString.trimEnd());\n lines.push('```');\n lines.push('');\n }\n }\n\n return lines.join('\\n') + '\\n';\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 err;\n }\n}\n\nfunction rotateOldDays(hooksDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(hooksDir);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return;\n }\n\n for (const entry of entries) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(entry)) continue;\n const dirDate = new Date(entry + 'T00:00:00Z');\n if (isNaN(dirDate.getTime())) continue;\n if (dirDate.getTime() < cutoff) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(path.join(hooksDir, entry), { recursive: true, force: true });\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n }\n}\n"]}
|
|
@@ -29,16 +29,18 @@ class PrMergeGuardRule extends rule_base_1.BashRuleBase {
|
|
|
29
29
|
// which is the moment that actually keeps the local branch list bounded.
|
|
30
30
|
get fixHint() {
|
|
31
31
|
return new fix_hint_1.FixHint('Land the PR with `pnpm wp-land-pr`, never a hand-rolled `gh pr merge`.', [
|
|
32
|
-
'A bare `gh pr merge`
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
32
|
+
'A bare `gh pr merge` leaves the commit message to the repo\'s squash_merge_commit_title/',
|
|
33
|
+
'message settings. On a repo set to PR_TITLE + PR_BODY that now happens to be RIGHT — the',
|
|
34
|
+
'PR description IS the gated commit body — but on any other combination it is wrong, and',
|
|
35
|
+
'commonly means the internal "Squash merge of <branch>" subject. `pnpm wp-land-pr` passes',
|
|
36
|
+
'`--subject`/`--body-file` explicitly, so it is right on EVERY repo regardless of settings:',
|
|
37
37
|
'',
|
|
38
38
|
' pnpm wp-land-pr',
|
|
39
39
|
'',
|
|
40
40
|
'It squash-merges when the PR is mergeable, and enables auto-merge with the same',
|
|
41
|
-
'subject/body when the checks are still running.
|
|
41
|
+
'subject/body when the checks are still running. It also does the two things a raw merge',
|
|
42
|
+
'never does: archive the pre-squash tip as archive/<date>/<branch>, and reap the landed',
|
|
43
|
+
'worktree. Then clean up:',
|
|
42
44
|
'',
|
|
43
45
|
]
|
|
44
46
|
.concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-merge-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-merge-guard.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AAKzC,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,mDAAyD;AAEzD,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,MAAa,gBAAiB,SAAQ,wBAAgC;IAClE,YAAY,MAA0B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAEnE,WAAW,GAAG,yFAAyF,CAAC;IAEjH,iGAAiG;IACzF,aAAa,GAAG,kBAAkB,CAAC;IAE3C,2FAA2F;IAC3F,8FAA8F;IACtF,QAAQ,GAAa,SAAS,CAAC;IAC/B,YAAY,GAAG,gBAAgB,CAAC;IAEvB,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAE/C,6FAA6F;IAC7F,iEAAiE;IACjE,EAAE;IACF,8FAA8F;IAC9F,gGAAgG;IAChG,+FAA+F;IAC/F,yEAAyE;IACzE,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,wEAAwE,EACxE;YACI,
|
|
1
|
+
{"version":3,"file":"pr-merge-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-merge-guard.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AAKzC,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,mDAAyD;AAEzD,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,MAAa,gBAAiB,SAAQ,wBAAgC;IAClE,YAAY,MAA0B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAEnE,WAAW,GAAG,yFAAyF,CAAC;IAEjH,iGAAiG;IACzF,aAAa,GAAG,kBAAkB,CAAC;IAE3C,2FAA2F;IAC3F,8FAA8F;IACtF,QAAQ,GAAa,SAAS,CAAC;IAC/B,YAAY,GAAG,gBAAgB,CAAC;IAEvB,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAE/C,6FAA6F;IAC7F,iEAAiE;IACjE,EAAE;IACF,8FAA8F;IAC9F,gGAAgG;IAChG,+FAA+F;IAC/F,yEAAyE;IACzE,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,wEAAwE,EACxE;YACI,0FAA0F;YAC1F,0FAA0F;YAC1F,yFAAyF;YACzF,0FAA0F;YAC1F,4FAA4F;YAC5F,EAAE;YACF,mBAAmB;YACnB,EAAE;YACF,iFAAiF;YACjF,yFAAyF;YACzF,wFAAwF;YACxF,0BAA0B;YAC1B,EAAE;SACL;aACI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;aACxF,MAAM,CAAC,CAAC,EAAE,EAAE,yEAAyE,CAAC,CAAC;aACvF,IAAI,CAAC,IAAI,CAAC,CAClB,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,EAAE,CAAC;QAExD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxD,IAAI,CAAC,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC7D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,aAAa,CAAC;QAEtC,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;CACJ;AAlFD,4CAkFC","sourcesContent":["import { execSync } from 'child_process';\n\nimport { PrMergeGuardConfig } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { TreeRecovery, TreeKind } from './tree-recovery';\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class PrMergeGuardRule extends BashRuleBase<PrMergeGuardConfig> {\n constructor(config: PrMergeGuardConfig) { super(config, 'pr-merge-guard'); }\n\n readonly description = 'Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.';\n\n // Substituted with the real branch name in check(); the getter reads it. Placeholder until then.\n private currentBranch = '<current-branch>';\n\n // The tree we are standing in, resolved in check() so fixHint renders the ONE cleanup that\n // actually works here. 'unknown' until check() runs (fixHint is also read before/without it).\n private treeKind: TreeKind = 'unknown';\n private worktreePath = '<worktree-dir>';\n\n private readonly recovery = new TreeRecovery();\n\n // Single fix, no distinct options — the whole guidance lives in mainMessage so it renders as\n // one coherent block (never split into fake \"Fix Option 1/2/3\").\n //\n // `pnpm wp-cleanup` rather than `git branch -d <branch>`: a raw `-d` reads as destructive, so\n // agents ask permission and stop, and the branch survives. wp-cleanup is one named command that\n // deletes ONLY provably-dead branches — and it reaps every OTHER dead branch at the same time,\n // which is the moment that actually keeps the local branch list bounded.\n get fixHint(): FixHint {\n return new FixHint(\n 'Land the PR with `pnpm wp-land-pr`, never a hand-rolled `gh pr merge`.',\n [\n 'A bare `gh pr merge` leaves the commit message to the repo\\'s squash_merge_commit_title/',\n 'message settings. On a repo set to PR_TITLE + PR_BODY that now happens to be RIGHT — the',\n 'PR description IS the gated commit body — but on any other combination it is wrong, and',\n 'commonly means the internal \"Squash merge of <branch>\" subject. `pnpm wp-land-pr` passes',\n '`--subject`/`--body-file` explicitly, so it is right on EVERY repo regardless of settings:',\n '',\n ' pnpm wp-land-pr',\n '',\n 'It squash-merges when the PR is mergeable, and enables auto-merge with the same',\n 'subject/body when the checks are still running. It also does the two things a raw merge',\n 'never does: archive the pre-squash tip as archive/<date>/<branch>, and reap the landed',\n 'worktree. Then clean up:',\n '',\n ]\n .concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))\n .concat(['', \"Add this to your memory so you don't forget next time and waste tokens.\"])\n .join('\\n'),\n );\n }\n\n /**\n * Any hand-rolled `gh pr merge` is blocked outright — there is no \"but I cleaned up afterwards\"\n * escape, because the damage is the commit message that merge writes into main, which cleanup\n * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child\n * process this hook never sees, exactly like the other gated commands.\n *\n * ─── Why a CORRECT-LOOKING `gh pr merge --auto --subject --body-file` is still blocked ──────────\n * Full reasoning, and what was rejected: `decisions/0004-pr-artifacts-are-machine-global.md` § 8.\n * It was re-examined when the gated body moved to its machine-global home, and the block stands. A\n * `--body-file` proves a file was passed, never that it holds the GATED bytes: the guard sees a\n * command string, cannot know the PR number, and so cannot check the path against\n * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. Allowing the shape would let any\n * file at all — including a hand-written one, or the PR description — land as the reviewed body,\n * with nothing to distinguish it afterwards.\n *\n * Two things now depend on that. `wp-land-pr --fallback-title-only` is a HUMAN opt-in that stamps\n * \"this is a fallback\" into the commit; a permitted hand-rolled merge would be the same degraded\n * outcome with no stamp and no opt-in. And landing now DECIDES whether the archive/merge-info/reap\n * bookkeeping belongs to this tree, using the provenance filed beside the body — a hand-rolled merge\n * skips that decision silently, which is how a landed worktree becomes a corpse.\n *\n * The tree kind still decides which cleanup steps the hint prints: in a linked worktree\n * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.\n */\n check(ctx: BashContext): readonly Violation[] {\n if (!/gh\\s+pr\\s+merge/.test(ctx.commandCode)) return [];\n\n this.treeKind = this.recovery.kindOf(ctx.workspaceRoot);\n this.currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n this.worktreePath = ctx.workspaceRoot;\n\n return [new V(1, truncate(ctx.command))];\n }\n}\n"]}
|