@webpieces/rules-config 0.4.498 → 0.4.499
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 +1 -1
- package/src/branch-mutation-log.d.ts +1 -1
- package/src/branch-mutation-log.js.map +1 -1
- package/src/checklist-config.js +1 -1
- package/src/checklist-config.js.map +1 -1
- package/src/checklist-instructions.d.ts +1 -1
- package/src/checklist-instructions.js +28 -13
- package/src/checklist-instructions.js.map +1 -1
- package/src/diff-scope.d.ts +10 -0
- package/src/diff-scope.js +14 -1
- package/src/diff-scope.js.map +1 -1
- package/src/index.d.ts +3 -2
- package/src/index.js +11 -3
- package/src/index.js.map +1 -1
- package/src/pr-gate-config.d.ts +53 -0
- package/src/pr-gate-config.js +66 -4
- package/src/pr-gate-config.js.map +1 -1
- package/src/review-json.d.ts +35 -2
- package/src/review-json.js +73 -14
- package/src/review-json.js.map +1 -1
- package/src/reviewer-instructions.d.ts +86 -0
- package/src/reviewer-instructions.js +236 -0
- package/src/reviewer-instructions.js.map +1 -0
- package/src/subagent-provenance.d.ts +55 -1
- package/src/subagent-provenance.js +143 -5
- package/src/subagent-provenance.js.map +1 -1
- package/templates/webpieces.git-workflow.md +13 -7
- package/templates/webpieces.review-checklists.md +19 -8
|
@@ -4,7 +4,32 @@ export declare const PROVENANCE_SKIPPED = "skipped";
|
|
|
4
4
|
export declare class ProvenanceResult {
|
|
5
5
|
status: string;
|
|
6
6
|
detail: string;
|
|
7
|
-
|
|
7
|
+
agentIds: Record<string, string>;
|
|
8
|
+
constructor(status: string, detail: string, agentIds?: Record<string, string>);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).
|
|
12
|
+
*
|
|
13
|
+
* `verifyDistinct` answers "did a reviewer of this type run?" — an INTEGRITY question, and it blocks. This
|
|
14
|
+
* answers "did it look at the change?" — a QUALITY question, and it only warns. The distinction is
|
|
15
|
+
* deliberate; see {@link SubagentProvenanceService.evidenceFor}.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ReviewerEvidence {
|
|
18
|
+
agentType: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
readDiff: boolean;
|
|
21
|
+
readDoc: boolean;
|
|
22
|
+
toolCallCount: number;
|
|
23
|
+
offRepoSearches: number;
|
|
24
|
+
constructor(agentType: string, agentId: string, readDiff?: boolean, readDoc?: boolean, toolCallCount?: number, offRepoSearches?: number);
|
|
25
|
+
}
|
|
26
|
+
/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */
|
|
27
|
+
export declare class EvidenceRequest {
|
|
28
|
+
branch: string;
|
|
29
|
+
agentIds: Record<string, string>;
|
|
30
|
+
diffDir: string;
|
|
31
|
+
docPaths: Record<string, string>;
|
|
32
|
+
constructor(branch: string, agentIds: Record<string, string>, diffDir?: string, docPaths?: Record<string, string>);
|
|
8
33
|
}
|
|
9
34
|
/**
|
|
10
35
|
* Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
|
|
@@ -28,6 +53,35 @@ export declare class ProvenanceResult {
|
|
|
28
53
|
export declare class SubagentProvenanceService {
|
|
29
54
|
verify(expectedAgentType: string, branch: string): ProvenanceResult;
|
|
30
55
|
verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult;
|
|
56
|
+
/**
|
|
57
|
+
* What each credited reviewer actually READ, from its own transcript.
|
|
58
|
+
*
|
|
59
|
+
* `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff
|
|
60
|
+
* and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and
|
|
61
|
+
* the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because
|
|
62
|
+
* nothing had told it where anything was, which `offRepoSearches` now makes visible.
|
|
63
|
+
*
|
|
64
|
+
* WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least
|
|
65
|
+
* one repo has watched it:
|
|
66
|
+
* 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every
|
|
67
|
+
* consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly
|
|
68
|
+
* this reason, and blocking on a richer read of the same files would be less safe, not more.
|
|
69
|
+
* 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).
|
|
70
|
+
* 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating
|
|
71
|
+
* them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.
|
|
72
|
+
*
|
|
73
|
+
* Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.
|
|
74
|
+
*/
|
|
75
|
+
evidenceFor(request: EvidenceRequest): ReviewerEvidence[];
|
|
76
|
+
private evidenceFromTranscript;
|
|
77
|
+
private transcriptPath;
|
|
78
|
+
/**
|
|
79
|
+
* Every tool_use input in a transcript, JSON-stringified. Stringified rather than walked field-by-field
|
|
80
|
+
* because the shape differs per tool (Read takes file_path, Bash takes command, Grep takes path) and a
|
|
81
|
+
* substring test over the serialized input is both simpler and robust to a tool we have not seen.
|
|
82
|
+
*/
|
|
83
|
+
private toolInputsOf;
|
|
84
|
+
private mentions;
|
|
31
85
|
private inClaudeSession;
|
|
32
86
|
private skipped;
|
|
33
87
|
private findMatchingAgentId;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SubagentProvenanceService = exports.ProvenanceResult = exports.PROVENANCE_SKIPPED = exports.PROVENANCE_MISSING = exports.PROVENANCE_OK = void 0;
|
|
3
|
+
exports.SubagentProvenanceService = exports.EvidenceRequest = exports.ReviewerEvidence = exports.ProvenanceResult = exports.PROVENANCE_SKIPPED = exports.PROVENANCE_MISSING = exports.PROVENANCE_OK = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
@@ -14,12 +14,55 @@ exports.PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain term
|
|
|
14
14
|
class ProvenanceResult {
|
|
15
15
|
status; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED
|
|
16
16
|
detail; // human-readable explanation for the warning/error/dashboard
|
|
17
|
-
|
|
17
|
+
// agentIds credited, keyed by agentType, so an evidence pass does not re-scan to find them again.
|
|
18
|
+
agentIds;
|
|
19
|
+
constructor(status, detail, agentIds = {}) {
|
|
18
20
|
this.status = status;
|
|
19
21
|
this.detail = detail;
|
|
22
|
+
this.agentIds = agentIds;
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
exports.ProvenanceResult = ProvenanceResult;
|
|
26
|
+
/**
|
|
27
|
+
* What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).
|
|
28
|
+
*
|
|
29
|
+
* `verifyDistinct` answers "did a reviewer of this type run?" — an INTEGRITY question, and it blocks. This
|
|
30
|
+
* answers "did it look at the change?" — a QUALITY question, and it only warns. The distinction is
|
|
31
|
+
* deliberate; see {@link SubagentProvenanceService.evidenceFor}.
|
|
32
|
+
*/
|
|
33
|
+
class ReviewerEvidence {
|
|
34
|
+
agentType;
|
|
35
|
+
agentId;
|
|
36
|
+
readDiff; // opened the materialized diff dir (or its own instructions file)
|
|
37
|
+
readDoc; // opened its checklist's guidance doc
|
|
38
|
+
toolCallCount;
|
|
39
|
+
offRepoSearches; // tool calls that reached into node_modules — the archaeology signal
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
41
|
+
constructor(agentType, agentId, readDiff = false, readDoc = false, toolCallCount = 0, offRepoSearches = 0) {
|
|
42
|
+
this.agentType = agentType;
|
|
43
|
+
this.agentId = agentId;
|
|
44
|
+
this.readDiff = readDiff;
|
|
45
|
+
this.readDoc = readDoc;
|
|
46
|
+
this.toolCallCount = toolCallCount;
|
|
47
|
+
this.offRepoSearches = offRepoSearches;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.ReviewerEvidence = ReviewerEvidence;
|
|
51
|
+
/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */
|
|
52
|
+
class EvidenceRequest {
|
|
53
|
+
branch;
|
|
54
|
+
agentIds; // agentType → agentId, straight from ProvenanceResult
|
|
55
|
+
diffDir; // '' when nothing was materialized
|
|
56
|
+
docPaths; // agentType → its checklist doc path ('' when none)
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
58
|
+
constructor(branch, agentIds, diffDir = '', docPaths = {}) {
|
|
59
|
+
this.branch = branch;
|
|
60
|
+
this.agentIds = agentIds;
|
|
61
|
+
this.diffDir = diffDir;
|
|
62
|
+
this.docPaths = docPaths;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.EvidenceRequest = EvidenceRequest;
|
|
23
66
|
/**
|
|
24
67
|
* Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
|
|
25
68
|
* a subagent of a given `agentType` actually ran during the current session on the current branch. Used
|
|
@@ -66,16 +109,111 @@ let SubagentProvenanceService = class SubagentProvenanceService {
|
|
|
66
109
|
const dirs = this.allSubagentsDirs();
|
|
67
110
|
const missing = [];
|
|
68
111
|
const usedAgentIds = new Set();
|
|
112
|
+
const credited = {};
|
|
69
113
|
for (const type of expectedAgentTypes) {
|
|
70
114
|
const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);
|
|
71
115
|
if (agentId === '')
|
|
72
116
|
missing.push(type);
|
|
73
|
-
else
|
|
117
|
+
else {
|
|
74
118
|
usedAgentIds.add(agentId);
|
|
119
|
+
credited[type] = agentId;
|
|
120
|
+
}
|
|
75
121
|
}
|
|
76
122
|
return missing.length === 0
|
|
77
|
-
? new ProvenanceResult(exports.PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran
|
|
78
|
-
: new ProvenanceResult(exports.PROVENANCE_MISSING, `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}
|
|
123
|
+
? new ProvenanceResult(exports.PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`, credited)
|
|
124
|
+
: new ProvenanceResult(exports.PROVENANCE_MISSING, `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}`, credited);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What each credited reviewer actually READ, from its own transcript.
|
|
128
|
+
*
|
|
129
|
+
* `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff
|
|
130
|
+
* and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and
|
|
131
|
+
* the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because
|
|
132
|
+
* nothing had told it where anything was, which `offRepoSearches` now makes visible.
|
|
133
|
+
*
|
|
134
|
+
* WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least
|
|
135
|
+
* one repo has watched it:
|
|
136
|
+
* 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every
|
|
137
|
+
* consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly
|
|
138
|
+
* this reason, and blocking on a richer read of the same files would be less safe, not more.
|
|
139
|
+
* 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).
|
|
140
|
+
* 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating
|
|
141
|
+
* them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.
|
|
142
|
+
*
|
|
143
|
+
* Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.
|
|
144
|
+
*/
|
|
145
|
+
evidenceFor(request) {
|
|
146
|
+
if (!this.inClaudeSession())
|
|
147
|
+
return [];
|
|
148
|
+
const dirs = this.allSubagentsDirs();
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const agentType of Object.keys(request.agentIds)) {
|
|
151
|
+
const agentId = request.agentIds[agentType];
|
|
152
|
+
const jsonl = this.transcriptPath(dirs, agentId);
|
|
153
|
+
if (jsonl === '') {
|
|
154
|
+
out.push(new ReviewerEvidence(agentType, agentId));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
out.push(this.evidenceFromTranscript(agentType, agentId, jsonl, request));
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
162
|
+
evidenceFromTranscript(agentType, agentId, jsonl, request) {
|
|
163
|
+
const inputs = this.toolInputsOf(jsonl);
|
|
164
|
+
const docPath = request.docPaths[agentType] ?? '';
|
|
165
|
+
// The instructions file counts as "read the diff": it lives in the same per-branch dir, it is what
|
|
166
|
+
// the reviewer is told to open first, and it inlines the diff paths. A reviewer that opened it and
|
|
167
|
+
// then its own diff files is the intended path; treating only the diff dir as proof would flag it.
|
|
168
|
+
const readDiff = request.diffDir !== '' && this.mentions(inputs, request.diffDir);
|
|
169
|
+
return new ReviewerEvidence(agentType, agentId, readDiff, docPath !== '' && this.mentions(inputs, docPath), inputs.length, inputs.filter((i) => i.includes('node_modules')).length);
|
|
170
|
+
}
|
|
171
|
+
// The absolute path of agent-<id>.jsonl across all session dirs, or '' if it is not there.
|
|
172
|
+
transcriptPath(dirs, agentId) {
|
|
173
|
+
for (const dir of dirs) {
|
|
174
|
+
const candidate = path.join(dir, `agent-${agentId}.jsonl`);
|
|
175
|
+
if (fs.existsSync(candidate))
|
|
176
|
+
return candidate;
|
|
177
|
+
}
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Every tool_use input in a transcript, JSON-stringified. Stringified rather than walked field-by-field
|
|
182
|
+
* because the shape differs per tool (Read takes file_path, Bash takes command, Grep takes path) and a
|
|
183
|
+
* substring test over the serialized input is both simpler and robust to a tool we have not seen.
|
|
184
|
+
*/
|
|
185
|
+
toolInputsOf(jsonl) {
|
|
186
|
+
const out = [];
|
|
187
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable transcript yields no evidence, never a crash
|
|
188
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
189
|
+
try {
|
|
190
|
+
for (const line of fs.readFileSync(jsonl, 'utf8').split('\n')) {
|
|
191
|
+
if (line.trim() === '')
|
|
192
|
+
continue;
|
|
193
|
+
const rec = this.parseLine(line);
|
|
194
|
+
const message = rec?.['message'];
|
|
195
|
+
if (typeof message !== 'object' || message === null)
|
|
196
|
+
continue;
|
|
197
|
+
// webpieces-disable no-any-unknown -- opaque transcript record, narrowed by the guard above
|
|
198
|
+
const content = message['content'];
|
|
199
|
+
if (!Array.isArray(content))
|
|
200
|
+
continue;
|
|
201
|
+
for (const block of content) {
|
|
202
|
+
// webpieces-disable no-any-unknown -- opaque content block, only `type`/`input` are read
|
|
203
|
+
const b = block;
|
|
204
|
+
if (b['type'] === 'tool_use')
|
|
205
|
+
out.push(JSON.stringify(b['input'] ?? {}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const error = (0, to_error_1.toError)(err);
|
|
211
|
+
void error;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
mentions(inputs, needle) {
|
|
216
|
+
return inputs.some((i) => i.includes(needle));
|
|
79
217
|
}
|
|
80
218
|
// Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)
|
|
81
219
|
inClaudeSession() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/subagent-provenance.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\n\n// Outcome of a provenance check.\nexport const PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch\nexport const PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)\nexport const PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain terminal / CI) → warn + pass\n\nexport class ProvenanceResult {\n status: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n detail: string; // human-readable explanation for the warning/error/dashboard\n\n constructor(status: string, detail: string) {\n this.status = status;\n this.detail = detail;\n }\n}\n\n/**\n * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that\n * a subagent of a given `agentType` actually ran during the current session on the current branch. Used\n * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than\n * the coding agent self-certifying.\n *\n * The harness writes, beside each subagent transcript:\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }\n * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir\n * by the unique sessionId (globbing `projects/*/<sessionId>/subagents`), so the cwd-slug never has to be\n * derived/guessed.\n *\n * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This\n * raises the bar from \"trust the model's word\" to \"deliberate, auditable forgery outside the repo\"; it\n * is not cryptographic. Say so wherever `subagent:` is documented.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class SubagentProvenanceService {\n // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain\n // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.\n verify(expectedAgentType: string, branch: string): ProvenanceResult {\n if (!this.inClaudeSession()) return this.skipped(`the \"${expectedAgentType}\" reviewer subagent`);\n const dirs = this.allSubagentsDirs();\n const agentId = this.findMatchingAgentId(dirs, expectedAgentType, branch);\n return agentId !== ''\n ? new ProvenanceResult(PROVENANCE_OK, `verified \"${expectedAgentType}\" reviewer subagent ran (agent ${agentId}).`)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `No \"${expectedAgentType}\" reviewer subagent ran on this branch — a separate reviewer of that type must review this checklist.`);\n }\n\n // Verify EVERY expected reviewer subagent ran on `branch` as a DISTINCT run — the coding agent may not\n // self-certify, and one reviewer may not stand in for several. SKIPPED (pass) without a session id.\n //\n // Scoped by BRANCH across ALL sessions (not the current session): once a reviewer ran on this branch in\n // any session, a later re-push in a NEW session still finds it, so the review is NOT forced to re-run.\n // That is what keeps \"review once per branch\" true across sessions. A PR opened outside the gated flow\n // still has no review-<id>.json, so wp-finish forces the review regardless of provenance.\n verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult {\n if (expectedAgentTypes.length === 0) return new ProvenanceResult(PROVENANCE_OK, 'no reviewer subagents required');\n if (!this.inClaudeSession()) return this.skipped('reviewer subagents');\n const dirs = this.allSubagentsDirs();\n const missing: string[] = [];\n const usedAgentIds = new Set<string>();\n for (const type of expectedAgentTypes) {\n const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);\n if (agentId === '') missing.push(type);\n else usedAgentIds.add(agentId);\n }\n return missing.length === 0\n ? new ProvenanceResult(PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}`);\n }\n\n // Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)\n private inClaudeSession(): boolean {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() !== '';\n }\n\n private skipped(what: string): ProvenanceResult {\n return new ProvenanceResult(PROVENANCE_SKIPPED,\n `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`);\n }\n\n // The agentId of a matching subagent run for `agentType` on `branch`, searched across ALL sessions'\n // subagent dirs (branch-scoped, so a run from a prior session still counts). '' if none. `exclude`\n // skips agentIds already credited to another checklist so one run can't satisfy two.\n private findMatchingAgentId(dirs: readonly string[], agentType: string, branch: string, exclude: ReadonlySet<string> = new Set()): string {\n for (const dir of dirs) {\n for (const metaFile of this.metaFiles(dir)) {\n const agentId = this.agentIdOf(metaFile);\n if (exclude.has(agentId)) continue;\n const meta = this.readJson(path.join(dir, metaFile));\n if (!meta || meta['agentType'] !== agentType) continue;\n const spawnDepth = meta['spawnDepth'];\n if (typeof spawnDepth !== 'number' || spawnDepth < 1) continue;\n if (this.sidechainOnBranch(dir, agentId, branch)) return agentId;\n }\n }\n return '';\n }\n\n // Every `projects/*/<session>/subagents` dir that exists — matching by the recorded gitBranch (not by\n // session id) is what makes provenance survive across sessions.\n private allSubagentsDirs(): string[] {\n const projects = path.join(os.homedir(), '.claude', 'projects');\n if (!fs.existsSync(projects)) return [];\n const out: string[] = [];\n for (const proj of this.readDir(projects)) {\n const projDir = path.join(projects, proj);\n for (const session of this.readDir(projDir)) {\n const candidate = path.join(projDir, session, 'subagents');\n if (fs.existsSync(candidate)) out.push(candidate);\n }\n }\n return out;\n }\n\n private metaFiles(dir: string): string[] {\n return this.readDir(dir).filter((f: string): boolean => f.endsWith('.meta.json'));\n }\n\n // agent-<id>.meta.json → <id>\n private agentIdOf(metaFile: string): string {\n return metaFile.replace(/^agent-/, '').replace(/\\.meta\\.json$/, '');\n }\n\n // Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main\n // loop) and, when recorded, the same branch. Lenient on branch drift (a leftover …wpN suffix from a\n // mid-flight branch rename) and on a missing jsonl (meta already matched — accept, best-effort).\n private sidechainOnBranch(dir: string, agentId: string, branch: string): boolean {\n const jsonl = path.join(dir, `agent-${agentId}.jsonl`);\n if (!fs.existsSync(jsonl)) return true;\n const first = this.firstNonEmptyLine(jsonl);\n if (first === '') return true;\n const rec = this.parseLine(first);\n if (!rec) return true;\n if (rec['isSidechain'] !== true) return false;\n const gitBranch = rec['gitBranch'];\n if (typeof gitBranch !== 'string' || gitBranch === '') return true;\n return this.stripWp(gitBranch) === this.stripWp(branch);\n }\n\n private stripWp(branch: string): string {\n return branch.replace(/-?wp\\d+$/i, '');\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort provenance)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private firstNonEmptyLine(filePath: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unreadable transcript → '' (best-effort)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(filePath, 'utf8').split('\\n')) {\n if (line.trim() !== '') return line;\n }\n return '';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller\n private readJson(filePath: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed meta.json → null (skipped)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- one opaque JSONL record, keys read by the caller\n private parseLine(line: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed JSONL record → null\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(line) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subagent-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/subagent-provenance.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\n\n// Outcome of a provenance check.\nexport const PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch\nexport const PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)\nexport const PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain terminal / CI) → warn + pass\n\nexport class ProvenanceResult {\n status: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n detail: string; // human-readable explanation for the warning/error/dashboard\n // agentIds credited, keyed by agentType, so an evidence pass does not re-scan to find them again.\n agentIds: Record<string, string>;\n\n constructor(status: string, detail: string, agentIds: Record<string, string> = {}) {\n this.status = status;\n this.detail = detail;\n this.agentIds = agentIds;\n }\n}\n\n/**\n * What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).\n *\n * `verifyDistinct` answers \"did a reviewer of this type run?\" — an INTEGRITY question, and it blocks. This\n * answers \"did it look at the change?\" — a QUALITY question, and it only warns. The distinction is\n * deliberate; see {@link SubagentProvenanceService.evidenceFor}.\n */\nexport class ReviewerEvidence {\n agentType: string;\n agentId: string;\n readDiff: boolean; // opened the materialized diff dir (or its own instructions file)\n readDoc: boolean; // opened its checklist's guidance doc\n toolCallCount: number;\n offRepoSearches: number; // tool calls that reached into node_modules — the archaeology signal\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(agentType: string, agentId: string, readDiff = false, readDoc = false, toolCallCount = 0, offRepoSearches = 0) {\n this.agentType = agentType;\n this.agentId = agentId;\n this.readDiff = readDiff;\n this.readDoc = readDoc;\n this.toolCallCount = toolCallCount;\n this.offRepoSearches = offRepoSearches;\n }\n}\n\n/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */\nexport class EvidenceRequest {\n branch: string;\n agentIds: Record<string, string>; // agentType → agentId, straight from ProvenanceResult\n diffDir: string; // '' when nothing was materialized\n docPaths: Record<string, string>; // agentType → its checklist doc path ('' when none)\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(branch: string, agentIds: Record<string, string>, diffDir = '', docPaths: Record<string, string> = {}) {\n this.branch = branch;\n this.agentIds = agentIds;\n this.diffDir = diffDir;\n this.docPaths = docPaths;\n }\n}\n\n/**\n * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that\n * a subagent of a given `agentType` actually ran during the current session on the current branch. Used\n * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than\n * the coding agent self-certifying.\n *\n * The harness writes, beside each subagent transcript:\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }\n * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir\n * by the unique sessionId (globbing `projects/*/<sessionId>/subagents`), so the cwd-slug never has to be\n * derived/guessed.\n *\n * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This\n * raises the bar from \"trust the model's word\" to \"deliberate, auditable forgery outside the repo\"; it\n * is not cryptographic. Say so wherever `subagent:` is documented.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class SubagentProvenanceService {\n // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain\n // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.\n verify(expectedAgentType: string, branch: string): ProvenanceResult {\n if (!this.inClaudeSession()) return this.skipped(`the \"${expectedAgentType}\" reviewer subagent`);\n const dirs = this.allSubagentsDirs();\n const agentId = this.findMatchingAgentId(dirs, expectedAgentType, branch);\n return agentId !== ''\n ? new ProvenanceResult(PROVENANCE_OK, `verified \"${expectedAgentType}\" reviewer subagent ran (agent ${agentId}).`)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `No \"${expectedAgentType}\" reviewer subagent ran on this branch — a separate reviewer of that type must review this checklist.`);\n }\n\n // Verify EVERY expected reviewer subagent ran on `branch` as a DISTINCT run — the coding agent may not\n // self-certify, and one reviewer may not stand in for several. SKIPPED (pass) without a session id.\n //\n // Scoped by BRANCH across ALL sessions (not the current session): once a reviewer ran on this branch in\n // any session, a later re-push in a NEW session still finds it, so the review is NOT forced to re-run.\n // That is what keeps \"review once per branch\" true across sessions. A PR opened outside the gated flow\n // still has no review-<id>.json, so wp-finish forces the review regardless of provenance.\n verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult {\n if (expectedAgentTypes.length === 0) return new ProvenanceResult(PROVENANCE_OK, 'no reviewer subagents required');\n if (!this.inClaudeSession()) return this.skipped('reviewer subagents');\n const dirs = this.allSubagentsDirs();\n const missing: string[] = [];\n const usedAgentIds = new Set<string>();\n const credited: Record<string, string> = {};\n for (const type of expectedAgentTypes) {\n const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);\n if (agentId === '') missing.push(type);\n else {\n usedAgentIds.add(agentId);\n credited[type] = agentId;\n }\n }\n return missing.length === 0\n ? new ProvenanceResult(PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`, credited)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}`,\n credited);\n }\n\n /**\n * What each credited reviewer actually READ, from its own transcript.\n *\n * `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff\n * and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and\n * the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because\n * nothing had told it where anything was, which `offRepoSearches` now makes visible.\n *\n * WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least\n * one repo has watched it:\n * 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every\n * consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly\n * this reason, and blocking on a richer read of the same files would be less safe, not more.\n * 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).\n * 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating\n * them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.\n *\n * Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.\n */\n evidenceFor(request: EvidenceRequest): ReviewerEvidence[] {\n if (!this.inClaudeSession()) return [];\n const dirs = this.allSubagentsDirs();\n const out: ReviewerEvidence[] = [];\n for (const agentType of Object.keys(request.agentIds)) {\n const agentId = request.agentIds[agentType];\n const jsonl = this.transcriptPath(dirs, agentId);\n if (jsonl === '') {\n out.push(new ReviewerEvidence(agentType, agentId));\n continue;\n }\n out.push(this.evidenceFromTranscript(agentType, agentId, jsonl, request));\n }\n return out;\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private evidenceFromTranscript(agentType: string, agentId: string, jsonl: string, request: EvidenceRequest): ReviewerEvidence {\n const inputs = this.toolInputsOf(jsonl);\n const docPath = request.docPaths[agentType] ?? '';\n // The instructions file counts as \"read the diff\": it lives in the same per-branch dir, it is what\n // the reviewer is told to open first, and it inlines the diff paths. A reviewer that opened it and\n // then its own diff files is the intended path; treating only the diff dir as proof would flag it.\n const readDiff = request.diffDir !== '' && this.mentions(inputs, request.diffDir);\n return new ReviewerEvidence(\n agentType, agentId, readDiff,\n docPath !== '' && this.mentions(inputs, docPath),\n inputs.length,\n inputs.filter((i: string): boolean => i.includes('node_modules')).length,\n );\n }\n\n // The absolute path of agent-<id>.jsonl across all session dirs, or '' if it is not there.\n private transcriptPath(dirs: readonly string[], agentId: string): string {\n for (const dir of dirs) {\n const candidate = path.join(dir, `agent-${agentId}.jsonl`);\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\n }\n\n /**\n * Every tool_use input in a transcript, JSON-stringified. Stringified rather than walked field-by-field\n * because the shape differs per tool (Read takes file_path, Bash takes command, Grep takes path) and a\n * substring test over the serialized input is both simpler and robust to a tool we have not seen.\n */\n private toolInputsOf(jsonl: string): string[] {\n const out: string[] = [];\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable transcript yields no evidence, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(jsonl, 'utf8').split('\\n')) {\n if (line.trim() === '') continue;\n const rec = this.parseLine(line);\n const message = rec?.['message'];\n if (typeof message !== 'object' || message === null) continue;\n // webpieces-disable no-any-unknown -- opaque transcript record, narrowed by the guard above\n const content = (message as Record<string, unknown>)['content'];\n if (!Array.isArray(content)) continue;\n for (const block of content) {\n // webpieces-disable no-any-unknown -- opaque content block, only `type`/`input` are read\n const b = block as Record<string, unknown>;\n if (b['type'] === 'tool_use') out.push(JSON.stringify(b['input'] ?? {}));\n }\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n return out;\n }\n\n private mentions(inputs: readonly string[], needle: string): boolean {\n return inputs.some((i: string): boolean => i.includes(needle));\n }\n\n // Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)\n private inClaudeSession(): boolean {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() !== '';\n }\n\n private skipped(what: string): ProvenanceResult {\n return new ProvenanceResult(PROVENANCE_SKIPPED,\n `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`);\n }\n\n // The agentId of a matching subagent run for `agentType` on `branch`, searched across ALL sessions'\n // subagent dirs (branch-scoped, so a run from a prior session still counts). '' if none. `exclude`\n // skips agentIds already credited to another checklist so one run can't satisfy two.\n private findMatchingAgentId(dirs: readonly string[], agentType: string, branch: string, exclude: ReadonlySet<string> = new Set()): string {\n for (const dir of dirs) {\n for (const metaFile of this.metaFiles(dir)) {\n const agentId = this.agentIdOf(metaFile);\n if (exclude.has(agentId)) continue;\n const meta = this.readJson(path.join(dir, metaFile));\n if (!meta || meta['agentType'] !== agentType) continue;\n const spawnDepth = meta['spawnDepth'];\n if (typeof spawnDepth !== 'number' || spawnDepth < 1) continue;\n if (this.sidechainOnBranch(dir, agentId, branch)) return agentId;\n }\n }\n return '';\n }\n\n // Every `projects/*/<session>/subagents` dir that exists — matching by the recorded gitBranch (not by\n // session id) is what makes provenance survive across sessions.\n private allSubagentsDirs(): string[] {\n const projects = path.join(os.homedir(), '.claude', 'projects');\n if (!fs.existsSync(projects)) return [];\n const out: string[] = [];\n for (const proj of this.readDir(projects)) {\n const projDir = path.join(projects, proj);\n for (const session of this.readDir(projDir)) {\n const candidate = path.join(projDir, session, 'subagents');\n if (fs.existsSync(candidate)) out.push(candidate);\n }\n }\n return out;\n }\n\n private metaFiles(dir: string): string[] {\n return this.readDir(dir).filter((f: string): boolean => f.endsWith('.meta.json'));\n }\n\n // agent-<id>.meta.json → <id>\n private agentIdOf(metaFile: string): string {\n return metaFile.replace(/^agent-/, '').replace(/\\.meta\\.json$/, '');\n }\n\n // Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main\n // loop) and, when recorded, the same branch. Lenient on branch drift (a leftover …wpN suffix from a\n // mid-flight branch rename) and on a missing jsonl (meta already matched — accept, best-effort).\n private sidechainOnBranch(dir: string, agentId: string, branch: string): boolean {\n const jsonl = path.join(dir, `agent-${agentId}.jsonl`);\n if (!fs.existsSync(jsonl)) return true;\n const first = this.firstNonEmptyLine(jsonl);\n if (first === '') return true;\n const rec = this.parseLine(first);\n if (!rec) return true;\n if (rec['isSidechain'] !== true) return false;\n const gitBranch = rec['gitBranch'];\n if (typeof gitBranch !== 'string' || gitBranch === '') return true;\n return this.stripWp(gitBranch) === this.stripWp(branch);\n }\n\n private stripWp(branch: string): string {\n return branch.replace(/-?wp\\d+$/i, '');\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort provenance)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private firstNonEmptyLine(filePath: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unreadable transcript → '' (best-effort)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(filePath, 'utf8').split('\\n')) {\n if (line.trim() !== '') return line;\n }\n return '';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller\n private readJson(filePath: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed meta.json → null (skipped)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- one opaque JSONL record, keys read by the caller\n private parseLine(line: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed JSONL record → null\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(line) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
|
|
@@ -105,13 +105,19 @@ flow are not interchangeable.
|
|
|
105
105
|
2. **`pnpm wp-finish-update`** — finalize a squash-update after you've resolved its conflicts (validates +
|
|
106
106
|
commits + swaps the branch). Only needed on the conflict path; a clean `wp-start-update` finalizes
|
|
107
107
|
itself. It does NOT build or post a PR.
|
|
108
|
-
3. **`pnpm wp-start-upsert-pr`** — the PR flow
|
|
109
|
-
run `wp-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
4. **`pnpm wp-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
3. **`pnpm wp-start-upsert-pr`** — the PR flow, STAGE ①: update from main (same 3-point engine), then
|
|
109
|
+
tells you to run `wp-review-upsert-pr`. It runs NO build gate and does NOT push, and never creates the
|
|
110
|
+
PR itself. On a merge conflict here, resolve it and continue with `pnpm wp-review-upsert-pr` (not
|
|
111
|
+
`wp-finish-update`, and no longer `wp-finish-upsert-pr`).
|
|
112
|
+
4. **`pnpm wp-review-upsert-pr`** — STAGE ②, and the one that verifies before anyone reviews. It validates
|
|
113
|
+
and commits any in-progress 3-point merge, asserts a clean tree, runs the **build gate**, extracts this
|
|
114
|
+
branch's diff to `.webpieces/pr-review/<branch>/diff/`, writes a per-reviewer instructions file, and
|
|
115
|
+
prints exactly what to spawn plus the `review.json` schema. It CAN fail — and when it does, no reviewer
|
|
116
|
+
has been spawned yet, so a broken branch costs no review effort.
|
|
117
|
+
5. **`pnpm wp-finish-upsert-pr`** — STAGE ③: requires stage ②'s receipt, your `review.json` (its `title`
|
|
118
|
+
becomes the PR title) and every reviewer's verdict, then pushes and creates/updates the PR. It re-runs
|
|
119
|
+
the build gate ONLY if HEAD moved since stage ②, so the three stages still cost one build.
|
|
120
|
+
6. **`pnpm wp-cleanup`** — delete the local branches that are provably dead (merged PR, squash-merge
|
|
115
121
|
backup of a merged branch, or no commits of their own). Run it after `gh pr merge`, or any time the
|
|
116
122
|
branch cap blocks you. It takes no arguments and needs no judgement call from you: it recomputes the
|
|
117
123
|
verdicts itself, deletes one branch per command, spares anything a human should rule on, and logs
|
|
@@ -22,23 +22,34 @@ array of `{ subagent, doc?, patterns? }`, and that is the **only** accepted shap
|
|
|
22
22
|
## The one command you run
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
pnpm wp-
|
|
25
|
+
pnpm wp-review-upsert-pr
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
It validates
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
It validates and commits any in-progress 3-point merge, runs the build gate, then **extracts this branch's
|
|
29
|
+
diff to disk** and writes one instructions file per reviewer under
|
|
30
|
+
`.webpieces/pr-review/<branch>/instructions/`. It prints a copy-paste spawn block per reviewer whose prompt
|
|
31
|
+
is a POINTER to that file and nothing else.
|
|
32
|
+
|
|
33
|
+
That indirection is the design. Everything volatile — the diff, the matched files, the verdict schema, the
|
|
34
|
+
resolved context paths — is REGENERATED every run, so it cannot go stale. The registered
|
|
35
|
+
`.claude/agents/<subagent>.md` stays a thin stub that says "read the instructions file your caller names".
|
|
36
|
+
When the verdict format last changed, hand-written agent files kept documenting the removed `success`
|
|
37
|
+
field and a real PR had to carry a correction in its spawn prompt to work around it; nothing restated by
|
|
38
|
+
hand can drift out of date if nothing is restated by hand.
|
|
39
|
+
|
|
40
|
+
Unlike the report-only command it replaces, this one **can fail** — on an unresolved merge or a red build.
|
|
41
|
+
It fails before any reviewer is spawned, which is the point.
|
|
32
42
|
|
|
33
43
|
Checklists already reviewed on this branch are **not** re-listed — verdict files persist, so a second
|
|
34
|
-
|
|
44
|
+
start/review/finish cycle re-instructs nothing.
|
|
35
45
|
|
|
36
46
|
`wp-finish-upsert-pr` recomputes the same set and **refuses to open the PR** while any reviewer still owes
|
|
37
47
|
a passing verdict, naming only the ones still missing.
|
|
38
48
|
|
|
39
49
|
Note that **not every checklist is pattern-matched**: one with no `patterns` runs on EVERY PR, and the
|
|
40
|
-
whole diff is in its scope. `wp-
|
|
41
|
-
a "match"
|
|
50
|
+
whole diff is in its scope. `wp-review-upsert-pr` says `ALWAYS RUNS` for those rather than calling the
|
|
51
|
+
whole diff a "match" — and says so loudly, because a patternless checklist fires on docs-only PRs too, and
|
|
52
|
+
that is usually a missing `patterns` rather than an intent.
|
|
42
53
|
|
|
43
54
|
Where patterns DO apply, matching is deliberately **coarse** — the reviewer subagent makes the fine,
|
|
44
55
|
content-level judgment by reading the actual diff.
|