@webpieces/rules-config 0.4.537 → 0.4.539

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.
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReviewProvenanceService = exports.ReviewProvenance = exports.ProvenanceWriteRequest = exports.OfferedContext = exports.ReviewerTranscript = exports.ReviewerPaths = exports.DEFAULT_RETENTION_DAYS = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const os = tslib_1.__importStar(require("os"));
8
+ const inversify_1 = require("inversify");
9
+ const to_error_1 = require("./to-error");
10
+ // The audit record `wp-finish-upsert-pr` writes beside review.json, and where a consumed one is retired to.
11
+ const PROVENANCE_FILE = 'provenance.json';
12
+ const OLD_PROVENANCE_FILE = 'old-provenance.json';
13
+ /**
14
+ * Claude Code deletes transcripts after `cleanupPeriodDays`; 30 is its default and what applies when the
15
+ * setting is absent, which is the common case. Recorded in every provenance file so a reader knows how
16
+ * long the links it is holding remain resolvable rather than discovering it by following a dead path.
17
+ */
18
+ exports.DEFAULT_RETENTION_DAYS = 30;
19
+ const WHAT_THIS_IS = 'AUDIT RECORD — written by wp-finish-upsert-pr, never by an AI. It links each reviewer verdict to the ' +
20
+ 'transcript of the subagent that produced it, and records what that reviewer was OFFERED versus what it ' +
21
+ 'demonstrably READ. Open it to audit the review process itself: whether a verdict was written by an ' +
22
+ 'agent that actually opened the diff and its checklist doc. Do not hand-edit it, and do not treat it as ' +
23
+ 'a review — it says nothing about whether the code is good, only about how it was looked at. The linked ' +
24
+ 'transcripts expire (see transcriptsExpireOn); the counters recorded here do not.';
25
+ /** Where ONE reviewer's inputs and output live on disk. Data-only (per CLAUDE.md). */
26
+ class ReviewerPaths {
27
+ verdictFile; // the review-<id>.json this reviewer was told to write
28
+ instructionsFile; // its generated <subagent>.instructions.md
29
+ docPath; // its checklist's guidance doc ('' when the checklist names none)
30
+ constructor(verdictFile, instructionsFile, docPath) {
31
+ this.verdictFile = verdictFile;
32
+ this.instructionsFile = instructionsFile;
33
+ this.docPath = docPath;
34
+ }
35
+ }
36
+ exports.ReviewerPaths = ReviewerPaths;
37
+ /**
38
+ * ONE reviewer's provenance row: which agent ran, where its transcript is, and the evidence counters read
39
+ * out of that transcript. Data-only, and its FIELD NAMES ARE THE JSON KEYS — the file is written by
40
+ * serializing these objects directly, so renaming a field renames it in every consumer's audit record.
41
+ *
42
+ * The counters are copied here rather than left to be re-derived because the transcript they came from is a
43
+ * wasting asset (~30 days). After it expires this row still answers "did that reviewer read the diff?".
44
+ */
45
+ class ReviewerTranscript {
46
+ id;
47
+ agentType;
48
+ agentId;
49
+ transcript; // absolute path to agent-<id>.jsonl, '' when it could not be resolved
50
+ transcriptExists;
51
+ verdictFile;
52
+ instructionsFile;
53
+ docPath;
54
+ readDiff;
55
+ readDoc;
56
+ toolCallCount;
57
+ offRepoSearches;
58
+ constructor(evidence, paths) {
59
+ this.id = evidence.agentType; // a checklist's id IS its subagent name (see ChecklistDefinition)
60
+ this.agentType = evidence.agentType;
61
+ this.agentId = evidence.agentId;
62
+ this.transcript = evidence.transcriptPath;
63
+ this.transcriptExists = evidence.transcriptPath !== '' && fs.existsSync(evidence.transcriptPath);
64
+ this.verdictFile = paths.verdictFile;
65
+ this.instructionsFile = paths.instructionsFile;
66
+ this.docPath = paths.docPath;
67
+ this.readDiff = evidence.readDiff;
68
+ this.readDoc = evidence.readDoc;
69
+ this.toolCallCount = evidence.toolCallCount;
70
+ this.offRepoSearches = evidence.offRepoSearches;
71
+ }
72
+ }
73
+ exports.ReviewerTranscript = ReviewerTranscript;
74
+ /** What the reviewers were handed, whether or not any of them opened it. Data-only. */
75
+ class OfferedContext {
76
+ diffDir;
77
+ instructionsDir;
78
+ constructor(diffDir, instructionsDir) {
79
+ this.diffDir = diffDir;
80
+ this.instructionsDir = instructionsDir;
81
+ }
82
+ }
83
+ exports.OfferedContext = OfferedContext;
84
+ /**
85
+ * What {@link ReviewProvenanceService.write} is asked to record. Data-only; `offered` and `reviewers` are
86
+ * assigned after construction (the same shape ChecklistCommentRow uses) so this never becomes a 7-param
87
+ * constructor.
88
+ */
89
+ class ProvenanceWriteRequest {
90
+ prDir;
91
+ branch;
92
+ headSha;
93
+ provenanceStatus; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED
94
+ offered = new OfferedContext('', '');
95
+ reviewers = [];
96
+ // eslint-disable-next-line @typescript-eslint/max-params
97
+ constructor(prDir, branch, headSha, provenanceStatus) {
98
+ this.prDir = prDir;
99
+ this.branch = branch;
100
+ this.headSha = headSha;
101
+ this.provenanceStatus = provenanceStatus;
102
+ }
103
+ }
104
+ exports.ProvenanceWriteRequest = ProvenanceWriteRequest;
105
+ /**
106
+ * The written record. Field names are the JSON keys, in this order — `_WHAT_THIS_IS` is declared FIRST so
107
+ * anything that opens the file reads what it is before it reads anything it might act on, exactly as
108
+ * ReviewJsonService.archiveReviewJson stamps its note first.
109
+ */
110
+ class ReviewProvenance {
111
+ // webpieces-disable naming-convention -- the leading underscore marks a note-to-the-reader key, not data
112
+ _WHAT_THIS_IS = WHAT_THIS_IS;
113
+ sessionId;
114
+ mainTranscript;
115
+ branch;
116
+ headSha;
117
+ stampedAt;
118
+ transcriptRetentionDays;
119
+ transcriptsExpireOn;
120
+ provenanceStatus;
121
+ offered;
122
+ reviewers;
123
+ constructor(request) {
124
+ this.sessionId = '';
125
+ this.mainTranscript = '';
126
+ this.branch = request.branch;
127
+ this.headSha = request.headSha;
128
+ this.stampedAt = '';
129
+ this.transcriptRetentionDays = exports.DEFAULT_RETENTION_DAYS;
130
+ this.transcriptsExpireOn = '';
131
+ this.provenanceStatus = request.provenanceStatus;
132
+ this.offered = request.offered;
133
+ this.reviewers = request.reviewers;
134
+ }
135
+ }
136
+ exports.ReviewProvenance = ReviewProvenance;
137
+ /**
138
+ * Records WHICH transcript produced which verdict, so the review process itself can be audited later.
139
+ *
140
+ * Why a service and not a field the AI writes: a reviewer subagent CANNOT know its own transcript path. The
141
+ * environment exposes `CLAUDE_CODE_SESSION_ID` — the PARENT session — and no agent id, so a self-reported
142
+ * link would be invented. Every path here is derived from the harness's own artifacts:
143
+ * ~/.claude/projects/&#42;/<sessionId>.jsonl → the main agent's transcript
144
+ * ~/.claude/projects/&#42;/<sessionId>/subagents/agent-<id>.jsonl → one reviewer's transcript
145
+ * The subagent half is already resolved by {@link SubagentProvenanceService}; this carries it to disk and
146
+ * adds the session-level facts (which session, how long the links live).
147
+ *
148
+ * Deliberately a SEPARATE file from review.json / review-<id>.json: those are AI-authored and stay
149
+ * byte-untouched, so nothing here can be confused for something a reviewer claimed about itself.
150
+ *
151
+ * Best-effort throughout — an unreadable ~/.claude degrades the record to empty links, never fails a PR.
152
+ * Same reasoning as SubagentProvenanceService: this reads undocumented Claude Code internals, and a format
153
+ * change must not wedge a consumer's PR.
154
+ *
155
+ * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
156
+ */
157
+ let ReviewProvenanceService = class ReviewProvenanceService {
158
+ // Where the audit record for a branch lives, beside review.json.
159
+ provenancePath(prDir) {
160
+ return path.join(prDir, PROVENANCE_FILE);
161
+ }
162
+ // Where a consumed record is retired to — the mirror of ReviewJsonService.oldReviewJsonPath, so an
163
+ // archived old-review.json keeps the transcript links belonging to the round that produced it.
164
+ oldProvenancePath(prDir) {
165
+ return path.join(prDir, OLD_PROVENANCE_FILE);
166
+ }
167
+ // The current Claude Code session id, or '' outside a Claude Code session (plain terminal / CI).
168
+ sessionId() {
169
+ return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim();
170
+ }
171
+ /**
172
+ * The main agent's own transcript: `~/.claude/projects/<cwd-slug>/<sessionId>.jsonl`. Located by
173
+ * scanning every project dir for the file named after the session, so the cwd-slug — which is a
174
+ * mangling of the working directory we would otherwise have to reproduce exactly — never has to be
175
+ * derived. '' when there is no session or the file is not there.
176
+ */
177
+ mainTranscript() {
178
+ const session = this.sessionId();
179
+ if (session === '')
180
+ return '';
181
+ const projects = path.join(os.homedir(), '.claude', 'projects');
182
+ for (const proj of this.readDir(projects)) {
183
+ const candidate = path.join(projects, proj, `${session}.jsonl`);
184
+ if (fs.existsSync(candidate))
185
+ return candidate;
186
+ }
187
+ return '';
188
+ }
189
+ /**
190
+ * How many days Claude Code keeps transcripts: `cleanupPeriodDays` from ~/.claude/settings.json (then
191
+ * settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent, which is
192
+ * why the default is documented rather than left implicit.
193
+ */
194
+ retentionDays() {
195
+ const dir = path.join(os.homedir(), '.claude');
196
+ for (const file of ['settings.json', 'settings.local.json']) {
197
+ const settings = this.readJson(path.join(dir, file));
198
+ const days = settings?.['cleanupPeriodDays'];
199
+ if (typeof days === 'number' && Number.isFinite(days) && days > 0)
200
+ return days;
201
+ }
202
+ return exports.DEFAULT_RETENTION_DAYS;
203
+ }
204
+ /**
205
+ * The date the FIRST of these transcripts becomes unreadable: the oldest one's mtime + retentionDays,
206
+ * as an ISO date. The oldest rather than the newest because that is when the audit trail starts losing
207
+ * links, and a reader planning to follow them needs the pessimistic answer. '' when none exist.
208
+ */
209
+ expiresOn(transcripts, retentionDays) {
210
+ let oldest = 0;
211
+ for (const file of transcripts) {
212
+ const mtime = this.mtimeOf(file);
213
+ if (mtime !== 0 && (oldest === 0 || mtime < oldest))
214
+ oldest = mtime;
215
+ }
216
+ if (oldest === 0)
217
+ return '';
218
+ const expiry = new Date(oldest + retentionDays * 24 * 60 * 60 * 1000);
219
+ return expiry.toISOString().slice(0, 10);
220
+ }
221
+ /**
222
+ * Write the record to `<prDir>/provenance.json` and return its path ('' if it could not be written).
223
+ *
224
+ * Written on EVERY finish, including one that refuses for a missing reviewer: a refused round is
225
+ * precisely the one worth auditing, and a record that only ever appears on success cannot answer "what
226
+ * did the reviewers do the time this was rejected?".
227
+ */
228
+ write(request) {
229
+ const provenance = this.build(request);
230
+ const target = this.provenancePath(request.prDir);
231
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: the audit record is never worth failing a PR over
232
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
233
+ try {
234
+ fs.mkdirSync(request.prDir, { recursive: true });
235
+ fs.writeFileSync(target, JSON.stringify(provenance, null, 2) + '\n');
236
+ return target;
237
+ }
238
+ catch (err) {
239
+ const error = (0, to_error_1.toError)(err);
240
+ void error;
241
+ return '';
242
+ }
243
+ }
244
+ // Retire the record for the round that just shipped: copy it to old-provenance.json beside the
245
+ // old-review.json it belongs to. A COPY, not a move — unlike review.json this file is not an input to
246
+ // anything, so leaving it in place cannot mislead a later reviewer, and the next finish overwrites it.
247
+ archive(prDir) {
248
+ const source = this.provenancePath(prDir);
249
+ if (!fs.existsSync(source))
250
+ return '';
251
+ const target = this.oldProvenancePath(prDir);
252
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must not fail the command
253
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
254
+ try {
255
+ fs.copyFileSync(source, target);
256
+ return target;
257
+ }
258
+ catch (err) {
259
+ const error = (0, to_error_1.toError)(err);
260
+ void error;
261
+ return '';
262
+ }
263
+ }
264
+ // Fill in the session-level facts the caller cannot know: which session, which transcripts, how long.
265
+ build(request) {
266
+ const provenance = new ReviewProvenance(request);
267
+ provenance.sessionId = this.sessionId();
268
+ provenance.mainTranscript = this.mainTranscript();
269
+ provenance.stampedAt = new Date().toISOString();
270
+ provenance.transcriptRetentionDays = this.retentionDays();
271
+ const linked = [provenance.mainTranscript, ...request.reviewers.map((r) => r.transcript)].filter((p) => p !== '');
272
+ provenance.transcriptsExpireOn = this.expiresOn(linked, provenance.transcriptRetentionDays);
273
+ return provenance;
274
+ }
275
+ // Epoch millis of a file's mtime, or 0 when it cannot be read.
276
+ mtimeOf(filePath) {
277
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unstattable transcript contributes no expiry
278
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
279
+ try {
280
+ return fs.statSync(filePath).mtime.getTime();
281
+ }
282
+ catch (err) {
283
+ const error = (0, to_error_1.toError)(err);
284
+ void error;
285
+ return 0;
286
+ }
287
+ }
288
+ readDir(dir) {
289
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort)
290
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
291
+ try {
292
+ return fs.readdirSync(dir);
293
+ }
294
+ catch (err) {
295
+ const error = (0, to_error_1.toError)(err);
296
+ void error;
297
+ return [];
298
+ }
299
+ }
300
+ // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller
301
+ readJson(filePath) {
302
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed settings → null (fall through to the default)
303
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
304
+ try {
305
+ // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller
306
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
307
+ }
308
+ catch (err) {
309
+ const error = (0, to_error_1.toError)(err);
310
+ void error;
311
+ return null;
312
+ }
313
+ }
314
+ };
315
+ exports.ReviewProvenanceService = ReviewProvenanceService;
316
+ exports.ReviewProvenanceService = ReviewProvenanceService = tslib_1.__decorate([
317
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
318
+ ], ReviewProvenanceService);
319
+ //# sourceMappingURL=review-provenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-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';\nimport { ReviewerEvidence } from './subagent-provenance';\n\n// The audit record `wp-finish-upsert-pr` writes beside review.json, and where a consumed one is retired to.\nconst PROVENANCE_FILE = 'provenance.json';\nconst OLD_PROVENANCE_FILE = 'old-provenance.json';\n\n/**\n * Claude Code deletes transcripts after `cleanupPeriodDays`; 30 is its default and what applies when the\n * setting is absent, which is the common case. Recorded in every provenance file so a reader knows how\n * long the links it is holding remain resolvable rather than discovering it by following a dead path.\n */\nexport const DEFAULT_RETENTION_DAYS = 30;\n\nconst WHAT_THIS_IS =\n 'AUDIT RECORD — written by wp-finish-upsert-pr, never by an AI. It links each reviewer verdict to the ' +\n 'transcript of the subagent that produced it, and records what that reviewer was OFFERED versus what it ' +\n 'demonstrably READ. Open it to audit the review process itself: whether a verdict was written by an ' +\n 'agent that actually opened the diff and its checklist doc. Do not hand-edit it, and do not treat it as ' +\n 'a review — it says nothing about whether the code is good, only about how it was looked at. The linked ' +\n 'transcripts expire (see transcriptsExpireOn); the counters recorded here do not.';\n\n/** Where ONE reviewer's inputs and output live on disk. Data-only (per CLAUDE.md). */\nexport class ReviewerPaths {\n verdictFile: string; // the review-<id>.json this reviewer was told to write\n instructionsFile: string; // its generated <subagent>.instructions.md\n docPath: string; // its checklist's guidance doc ('' when the checklist names none)\n\n constructor(verdictFile: string, instructionsFile: string, docPath: string) {\n this.verdictFile = verdictFile;\n this.instructionsFile = instructionsFile;\n this.docPath = docPath;\n }\n}\n\n/**\n * ONE reviewer's provenance row: which agent ran, where its transcript is, and the evidence counters read\n * out of that transcript. Data-only, and its FIELD NAMES ARE THE JSON KEYS — the file is written by\n * serializing these objects directly, so renaming a field renames it in every consumer's audit record.\n *\n * The counters are copied here rather than left to be re-derived because the transcript they came from is a\n * wasting asset (~30 days). After it expires this row still answers \"did that reviewer read the diff?\".\n */\nexport class ReviewerTranscript {\n id: string;\n agentType: string;\n agentId: string;\n transcript: string; // absolute path to agent-<id>.jsonl, '' when it could not be resolved\n transcriptExists: boolean;\n verdictFile: string;\n instructionsFile: string;\n docPath: string;\n readDiff: boolean;\n readDoc: boolean;\n toolCallCount: number;\n offRepoSearches: number;\n\n constructor(evidence: ReviewerEvidence, paths: ReviewerPaths) {\n this.id = evidence.agentType; // a checklist's id IS its subagent name (see ChecklistDefinition)\n this.agentType = evidence.agentType;\n this.agentId = evidence.agentId;\n this.transcript = evidence.transcriptPath;\n this.transcriptExists = evidence.transcriptPath !== '' && fs.existsSync(evidence.transcriptPath);\n this.verdictFile = paths.verdictFile;\n this.instructionsFile = paths.instructionsFile;\n this.docPath = paths.docPath;\n this.readDiff = evidence.readDiff;\n this.readDoc = evidence.readDoc;\n this.toolCallCount = evidence.toolCallCount;\n this.offRepoSearches = evidence.offRepoSearches;\n }\n}\n\n/** What the reviewers were handed, whether or not any of them opened it. Data-only. */\nexport class OfferedContext {\n diffDir: string;\n instructionsDir: string;\n\n constructor(diffDir: string, instructionsDir: string) {\n this.diffDir = diffDir;\n this.instructionsDir = instructionsDir;\n }\n}\n\n/**\n * What {@link ReviewProvenanceService.write} is asked to record. Data-only; `offered` and `reviewers` are\n * assigned after construction (the same shape ChecklistCommentRow uses) so this never becomes a 7-param\n * constructor.\n */\nexport class ProvenanceWriteRequest {\n prDir: string;\n branch: string;\n headSha: string;\n provenanceStatus: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n offered: OfferedContext = new OfferedContext('', '');\n reviewers: ReviewerTranscript[] = [];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prDir: string, branch: string, headSha: string, provenanceStatus: string) {\n this.prDir = prDir;\n this.branch = branch;\n this.headSha = headSha;\n this.provenanceStatus = provenanceStatus;\n }\n}\n\n/**\n * The written record. Field names are the JSON keys, in this order — `_WHAT_THIS_IS` is declared FIRST so\n * anything that opens the file reads what it is before it reads anything it might act on, exactly as\n * ReviewJsonService.archiveReviewJson stamps its note first.\n */\nexport class ReviewProvenance {\n // webpieces-disable naming-convention -- the leading underscore marks a note-to-the-reader key, not data\n _WHAT_THIS_IS = WHAT_THIS_IS;\n sessionId: string;\n mainTranscript: string;\n branch: string;\n headSha: string;\n stampedAt: string;\n transcriptRetentionDays: number;\n transcriptsExpireOn: string;\n provenanceStatus: string;\n offered: OfferedContext;\n reviewers: ReviewerTranscript[];\n\n constructor(request: ProvenanceWriteRequest) {\n this.sessionId = '';\n this.mainTranscript = '';\n this.branch = request.branch;\n this.headSha = request.headSha;\n this.stampedAt = '';\n this.transcriptRetentionDays = DEFAULT_RETENTION_DAYS;\n this.transcriptsExpireOn = '';\n this.provenanceStatus = request.provenanceStatus;\n this.offered = request.offered;\n this.reviewers = request.reviewers;\n }\n}\n\n/**\n * Records WHICH transcript produced which verdict, so the review process itself can be audited later.\n *\n * Why a service and not a field the AI writes: a reviewer subagent CANNOT know its own transcript path. The\n * environment exposes `CLAUDE_CODE_SESSION_ID` — the PARENT session — and no agent id, so a self-reported\n * link would be invented. Every path here is derived from the harness's own artifacts:\n * ~/.claude/projects/&#42;/<sessionId>.jsonl → the main agent's transcript\n * ~/.claude/projects/&#42;/<sessionId>/subagents/agent-<id>.jsonl → one reviewer's transcript\n * The subagent half is already resolved by {@link SubagentProvenanceService}; this carries it to disk and\n * adds the session-level facts (which session, how long the links live).\n *\n * Deliberately a SEPARATE file from review.json / review-<id>.json: those are AI-authored and stay\n * byte-untouched, so nothing here can be confused for something a reviewer claimed about itself.\n *\n * Best-effort throughout — an unreadable ~/.claude degrades the record to empty links, never fails a PR.\n * Same reasoning as SubagentProvenanceService: this reads undocumented Claude Code internals, and a format\n * change must not wedge a consumer's PR.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewProvenanceService {\n // Where the audit record for a branch lives, beside review.json.\n provenancePath(prDir: string): string {\n return path.join(prDir, PROVENANCE_FILE);\n }\n\n // Where a consumed record is retired to — the mirror of ReviewJsonService.oldReviewJsonPath, so an\n // archived old-review.json keeps the transcript links belonging to the round that produced it.\n oldProvenancePath(prDir: string): string {\n return path.join(prDir, OLD_PROVENANCE_FILE);\n }\n\n // The current Claude Code session id, or '' outside a Claude Code session (plain terminal / CI).\n sessionId(): string {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim();\n }\n\n /**\n * The main agent's own transcript: `~/.claude/projects/<cwd-slug>/<sessionId>.jsonl`. Located by\n * scanning every project dir for the file named after the session, so the cwd-slug — which is a\n * mangling of the working directory we would otherwise have to reproduce exactly — never has to be\n * derived. '' when there is no session or the file is not there.\n */\n mainTranscript(): string {\n const session = this.sessionId();\n if (session === '') return '';\n const projects = path.join(os.homedir(), '.claude', 'projects');\n for (const proj of this.readDir(projects)) {\n const candidate = path.join(projects, proj, `${session}.jsonl`);\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\n }\n\n /**\n * How many days Claude Code keeps transcripts: `cleanupPeriodDays` from ~/.claude/settings.json (then\n * settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent, which is\n * why the default is documented rather than left implicit.\n */\n retentionDays(): number {\n const dir = path.join(os.homedir(), '.claude');\n for (const file of ['settings.json', 'settings.local.json']) {\n const settings = this.readJson(path.join(dir, file));\n const days = settings?.['cleanupPeriodDays'];\n if (typeof days === 'number' && Number.isFinite(days) && days > 0) return days;\n }\n return DEFAULT_RETENTION_DAYS;\n }\n\n /**\n * The date the FIRST of these transcripts becomes unreadable: the oldest one's mtime + retentionDays,\n * as an ISO date. The oldest rather than the newest because that is when the audit trail starts losing\n * links, and a reader planning to follow them needs the pessimistic answer. '' when none exist.\n */\n expiresOn(transcripts: readonly string[], retentionDays: number): string {\n let oldest = 0;\n for (const file of transcripts) {\n const mtime = this.mtimeOf(file);\n if (mtime !== 0 && (oldest === 0 || mtime < oldest)) oldest = mtime;\n }\n if (oldest === 0) return '';\n const expiry = new Date(oldest + retentionDays * 24 * 60 * 60 * 1000);\n return expiry.toISOString().slice(0, 10);\n }\n\n /**\n * Write the record to `<prDir>/provenance.json` and return its path ('' if it could not be written).\n *\n * Written on EVERY finish, including one that refuses for a missing reviewer: a refused round is\n * precisely the one worth auditing, and a record that only ever appears on success cannot answer \"what\n * did the reviewers do the time this was rejected?\".\n */\n write(request: ProvenanceWriteRequest): string {\n const provenance = this.build(request);\n const target = this.provenancePath(request.prDir);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the audit record is never worth failing a PR over\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(request.prDir, { recursive: true });\n fs.writeFileSync(target, JSON.stringify(provenance, null, 2) + '\\n');\n return target;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // Retire the record for the round that just shipped: copy it to old-provenance.json beside the\n // old-review.json it belongs to. A COPY, not a move — unlike review.json this file is not an input to\n // anything, so leaving it in place cannot mislead a later reviewer, and the next finish overwrites it.\n archive(prDir: string): string {\n const source = this.provenancePath(prDir);\n if (!fs.existsSync(source)) return '';\n const target = this.oldProvenancePath(prDir);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must not fail the command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.copyFileSync(source, target);\n return target;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // Fill in the session-level facts the caller cannot know: which session, which transcripts, how long.\n private build(request: ProvenanceWriteRequest): ReviewProvenance {\n const provenance = new ReviewProvenance(request);\n provenance.sessionId = this.sessionId();\n provenance.mainTranscript = this.mainTranscript();\n provenance.stampedAt = new Date().toISOString();\n provenance.transcriptRetentionDays = this.retentionDays();\n const linked = [provenance.mainTranscript, ...request.reviewers.map(\n (r: ReviewerTranscript): string => r.transcript)].filter((p: string): boolean => p !== '');\n provenance.transcriptsExpireOn = this.expiresOn(linked, provenance.transcriptRetentionDays);\n return provenance;\n }\n\n // Epoch millis of a file's mtime, or 0 when it cannot be read.\n private mtimeOf(filePath: string): number {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unstattable transcript contributes no expiry\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(filePath).mtime.getTime();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 0;\n }\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort)\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 // 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 settings → null (fall through to the default)\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"]}
@@ -6,11 +6,22 @@ export declare class ContextEntry {
6
6
  note: string;
7
7
  constructor(label: string, entryPath: string, note?: string);
8
8
  }
9
- /** One reviewer's matched file and the extracted diff for it. Data-only. */
9
+ /**
10
+ * One reviewer's matched file, the extracted diff for it, AND the absolute path of the file itself.
11
+ *
12
+ * `sourcePath` exists because of a measured 0/4: four reviewers on one PR read the diff and not one opened a
13
+ * single full source file. The instructions handed them an absolute path per diff and only a parent DIRECTORY
14
+ * per source, so reading the diff was a paste and reading the source was "join this dir to that filename
15
+ * yourself". Identical affordance, or the cheaper one wins every time. Data-only.
16
+ */
10
17
  export declare class BriefedFile {
11
18
  file: string;
12
19
  diffPath: string;
13
- constructor(file: string, diffPath: string);
20
+ sourcePath: string;
21
+ status: string;
22
+ diffBytes: number;
23
+ truncated: boolean;
24
+ constructor(file: string, diffPath: string, sourcePath?: string, status?: string, diffBytes?: number, truncated?: boolean);
14
25
  }
15
26
  /**
16
27
  * Everything ONE reviewer needs, with every path already resolved to an ABSOLUTE one.
@@ -34,8 +45,24 @@ export declare class ReviewerBriefing {
34
45
  checklistId: string;
35
46
  fileDiffCommand: string;
36
47
  dirty: boolean;
48
+ changedFileCount: number;
49
+ truncatedCount: number;
50
+ excludedCount: number;
51
+ allDiffLines: number;
52
+ allDiffBytes: number;
53
+ hashForkPoint: string;
54
+ hashFeatureHead: string;
55
+ hashMainHead: string;
56
+ ownAgentFileInDiff: string;
37
57
  constructor(subagent: string, checklistId: string, repoRoot: string);
38
58
  }
59
+ /**
60
+ * The Read tool truncates at roughly this many lines, silently. Any path this file prints alongside a bigger
61
+ * line count has to say so, or a reviewer reviews a fraction of a change and reports on all of it.
62
+ */
63
+ export declare const READ_TRUNCATION_LINES = 2000;
64
+ /** Comfortably under {@link READ_TRUNCATION_LINES} — the size at which ALL.diff really is one clean Read. */
65
+ export declare const ALL_DIFF_ONE_READ_LINES = 1500;
39
66
  /**
40
67
  * Renders the per-reviewer instructions file that `wp-review-upsert-pr` writes to
41
68
  * `.webpieces/pr-review/<feature>/instructions/<subagent>.instructions.md`.
@@ -63,13 +90,61 @@ export declare class ReviewerInstructionsService {
63
90
  pathFor(repoRoot: string, featureName: string, subagent: string): string;
64
91
  render(briefing: ReviewerBriefing): string;
65
92
  private identity;
93
+ /**
94
+ * Every PR that edits the review gate hits this: on the measured run, 5 of 8 changed files were the
95
+ * reviewer agent files themselves, two reviewers reviewed their OWN definition, and neither flagged it —
96
+ * neither had any guidance to. Detected by the tooling rather than left to the reviewer to notice.
97
+ */
98
+ private selfEditWarning;
66
99
  private checklistSection;
67
100
  /**
68
- * The change itself. The materialized diff leads because it is ONE Read instead of a shell-out per file,
69
- * and because a hand-assembled range can come back empty (it did — see DiffBasis).
101
+ * The change itself. The MANIFEST leads, and the per-file table with it; `ALL.diff` is demoted to one
102
+ * option among several, recommended only where it is actually the right read.
103
+ *
104
+ * That ordering is the fix for a measured 4/4 failure. `ALL.diff` used to be bolded, first, and framed as
105
+ * "everything", with the manifest one unbolded line below it called a "path map" — which reads like a
106
+ * lookup aid, not something to open. All four reviewers on that PR read `ALL.diff`, none opened the
107
+ * manifest, and none could therefore have established that the combined view was complete. Reviewers did
108
+ * exactly what the emphasis told them to; so the emphasis moved.
70
109
  */
71
110
  private diffSection;
111
+ /**
112
+ * The manifest, named as the authority AND with its own headline facts inlined. Inlining them is the
113
+ * point: a reviewer that never opens the manifest still learns whether anything was truncated or
114
+ * excluded, which is the one thing it could not otherwise have known it was missing.
115
+ */
116
+ private manifestLines;
117
+ /**
118
+ * What the diff is taken AGAINST. Two bare shas in a `git diff` command said nothing about this, so a
119
+ * reviewer could not tell a fork-point diff from a diff against main's current tip — and therefore could
120
+ * not tell that anything merged to main since the fork is simply absent from what it is judging.
121
+ * Rendering C beside A makes "main has moved" self-evident exactly when it has.
122
+ */
123
+ private basisLines;
124
+ /**
125
+ * Source and diff get IDENTICAL affordance — an absolute path each, in adjacent columns. The previous
126
+ * table gave an absolute path for the diff and nothing for the source, and the source went unread 4/4.
127
+ */
72
128
  private myFilesTable;
129
+ /**
130
+ * One row. A truncated or excluded diff is flagged HERE, in the row, not left as a field inside a file the
131
+ * reviewer has to think to open — and an oversized diff states the line count that makes a single Read
132
+ * come back silently incomplete.
133
+ */
134
+ private fileRow;
135
+ /**
136
+ * `ALL.diff`, recommended ONLY where it is the right read. It is kept — for a patternless reviewer on a
137
+ * small diff it is genuinely one Read instead of N, which is why all four used it — but the blanket
138
+ * "everything on this branch" line was wrong in two different ways at once: it invited a pattern-scoped
139
+ * reviewer to spend its budget on files it was explicitly not asked about, and it pointed a reviewer on a
140
+ * large PR at a file that cannot survive one Read.
141
+ */
142
+ private allDiffLines;
143
+ /**
144
+ * The rule that used to say "when you need to", and lost 4/4 to the more emphatic budget section 40 lines
145
+ * later. "When you need to" delegates the judgment to a reviewer that, by construction, does not yet know
146
+ * what it is missing. It has no opt-out now.
147
+ */
73
148
  private sourceSection;
74
149
  /**
75
150
  * The section that deletes the `node_modules` greps. Entries come from config, because the tooling
@@ -82,5 +157,13 @@ export declare class ReviewerInstructionsService {
82
157
  * reviewer's own checklist id so there is nothing to substitute and nothing to get wrong.
83
158
  */
84
159
  private verdictSection;
160
+ /**
161
+ * The anti-hunting rule, with the changed files carved out explicitly.
162
+ *
163
+ * It used to read as "stay inside what you were given" and, being the LAST thing in the file, it beat
164
+ * the source-reading instruction 4 times out of 4. It never distinguished HUNTING — greps into
165
+ * `node_modules`, re-deriving the dependency graph — from OPENING A FILE IT WAS EXPLICITLY HANDED. The
166
+ * distinction is now stated, because where two instructions conflict the later and more emphatic one wins.
167
+ */
85
168
  private budgetSection;
86
169
  }