@webpieces/rules-config 0.4.787 → 0.4.788
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/claude-config-dir.d.ts +62 -0
- package/src/claude-config-dir.js +87 -0
- package/src/claude-config-dir.js.map +1 -0
- package/src/harness-agent-activity.d.ts +5 -3
- package/src/harness-agent-activity.js +15 -14
- package/src/harness-agent-activity.js.map +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +8 -4
- package/src/index.js.map +1 -1
- package/src/review-provenance.d.ts +22 -10
- package/src/review-provenance.js +36 -22
- package/src/review-provenance.js.map +1 -1
- package/src/subagent-provenance.d.ts +49 -10
- package/src/subagent-provenance.js +67 -31
- package/src/subagent-provenance.js.map +1 -1
|
@@ -100,15 +100,16 @@ export declare class ReviewProvenance {
|
|
|
100
100
|
* Why a service and not a field the AI writes: a reviewer subagent CANNOT know its own transcript path. The
|
|
101
101
|
* environment exposes `CLAUDE_CODE_SESSION_ID` — the PARENT session — and no agent id, so a self-reported
|
|
102
102
|
* link would be invented. Every path here is derived from the harness's own artifacts:
|
|
103
|
-
*
|
|
104
|
-
*
|
|
103
|
+
* <config>/projects/*/<sessionId>.jsonl → the main agent's transcript
|
|
104
|
+
* <config>/projects/*/<sessionId>/subagents/agent-<id>.jsonl → one reviewer's transcript
|
|
105
|
+
* where `<config>` is {@link ClaudeConfigDir.root} — `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`.
|
|
105
106
|
* The subagent half is already resolved by {@link SubagentProvenanceService}; this carries it to disk and
|
|
106
107
|
* adds the session-level facts (which session, how long the links live).
|
|
107
108
|
*
|
|
108
109
|
* Deliberately a SEPARATE file from review.json / review-<id>.json: those are AI-authored and stay
|
|
109
110
|
* byte-untouched, so nothing here can be confused for something a reviewer claimed about itself.
|
|
110
111
|
*
|
|
111
|
-
* Best-effort throughout — an unreadable
|
|
112
|
+
* Best-effort throughout — an unreadable config tree degrades the record to empty links, never fails a PR.
|
|
112
113
|
* Same reasoning as SubagentProvenanceService: this reads undocumented Claude Code internals, and a format
|
|
113
114
|
* change must not wedge a consumer's PR.
|
|
114
115
|
*
|
|
@@ -119,16 +120,27 @@ export declare class ReviewProvenanceService {
|
|
|
119
120
|
oldProvenancePath(prDir: string): string;
|
|
120
121
|
sessionId(): string;
|
|
121
122
|
/**
|
|
122
|
-
* The main agent's own transcript:
|
|
123
|
-
* scanning every project dir
|
|
124
|
-
*
|
|
125
|
-
* derived. '' when there is no session or the
|
|
123
|
+
* The main agent's own transcript: `<config>/projects/<cwd-slug>/<sessionId>.jsonl`. Located by
|
|
124
|
+
* scanning every project dir under every root {@link ClaudeConfigDir.projectsRoots} names, for the file named
|
|
125
|
+
* after the session — so the cwd-slug, which is a mangling of the working directory we would
|
|
126
|
+
* otherwise have to reproduce exactly, never has to be derived. '' when there is no session or the
|
|
127
|
+
* file is not there.
|
|
128
|
+
*
|
|
129
|
+
* THIS FIELD IS THE DIAGNOSTIC. It is found by session id alone — no cwd, no branch, no stamping —
|
|
130
|
+
* so an empty one while {@link sessionId} is set can ONLY mean the transcript ROOT is wrong. That is
|
|
131
|
+
* exactly what a hardcoded `~/.claude` produced on a machine with `$CLAUDE_CONFIG_DIR` relocated,
|
|
132
|
+
* and the gate then refused every PR with a message about the reviewers' cwd.
|
|
126
133
|
*/
|
|
127
134
|
mainTranscript(): string;
|
|
128
135
|
/**
|
|
129
|
-
* How many days Claude Code keeps transcripts: `cleanupPeriodDays` from
|
|
130
|
-
* settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent,
|
|
131
|
-
* why the default is documented rather than left implicit.
|
|
136
|
+
* How many days Claude Code keeps transcripts: `cleanupPeriodDays` from `<config>/settings.json`
|
|
137
|
+
* (then settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent,
|
|
138
|
+
* which is why the default is documented rather than left implicit.
|
|
139
|
+
*
|
|
140
|
+
* `<config>` is every root {@link ClaudeConfigDir.roots} names, for the same reason the transcripts are
|
|
141
|
+
* searched in both: a hardcoded `~/.claude` silently read SOMEBODY ELSE'S retention (or none) on a
|
|
142
|
+
* relocated config dir, so the recorded `transcriptsExpireOn` was computed from a default rather
|
|
143
|
+
* than from the setting the owner actually wrote.
|
|
132
144
|
*/
|
|
133
145
|
retentionDays(): number;
|
|
134
146
|
/**
|
package/src/review-provenance.js
CHANGED
|
@@ -4,8 +4,8 @@ exports.ReviewProvenanceService = exports.ReviewProvenance = exports.ProvenanceW
|
|
|
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"));
|
|
7
|
-
const os = tslib_1.__importStar(require("os"));
|
|
8
7
|
const inversify_1 = require("inversify");
|
|
8
|
+
const claude_config_dir_1 = require("./claude-config-dir");
|
|
9
9
|
const to_error_1 = require("./to-error");
|
|
10
10
|
// The audit record `wp-finish-upsert-pr` writes beside review.json, and where a consumed one is retired to.
|
|
11
11
|
const PROVENANCE_FILE = 'provenance.json';
|
|
@@ -166,15 +166,16 @@ exports.ReviewProvenance = ReviewProvenance;
|
|
|
166
166
|
* Why a service and not a field the AI writes: a reviewer subagent CANNOT know its own transcript path. The
|
|
167
167
|
* environment exposes `CLAUDE_CODE_SESSION_ID` — the PARENT session — and no agent id, so a self-reported
|
|
168
168
|
* link would be invented. Every path here is derived from the harness's own artifacts:
|
|
169
|
-
*
|
|
170
|
-
*
|
|
169
|
+
* <config>/projects/*/<sessionId>.jsonl → the main agent's transcript
|
|
170
|
+
* <config>/projects/*/<sessionId>/subagents/agent-<id>.jsonl → one reviewer's transcript
|
|
171
|
+
* where `<config>` is {@link ClaudeConfigDir.root} — `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`.
|
|
171
172
|
* The subagent half is already resolved by {@link SubagentProvenanceService}; this carries it to disk and
|
|
172
173
|
* adds the session-level facts (which session, how long the links live).
|
|
173
174
|
*
|
|
174
175
|
* Deliberately a SEPARATE file from review.json / review-<id>.json: those are AI-authored and stay
|
|
175
176
|
* byte-untouched, so nothing here can be confused for something a reviewer claimed about itself.
|
|
176
177
|
*
|
|
177
|
-
* Best-effort throughout — an unreadable
|
|
178
|
+
* Best-effort throughout — an unreadable config tree degrades the record to empty links, never fails a PR.
|
|
178
179
|
* Same reasoning as SubagentProvenanceService: this reads undocumented Claude Code internals, and a format
|
|
179
180
|
* change must not wedge a consumer's PR.
|
|
180
181
|
*
|
|
@@ -195,35 +196,48 @@ let ReviewProvenanceService = class ReviewProvenanceService {
|
|
|
195
196
|
return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim();
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
|
-
* The main agent's own transcript:
|
|
199
|
-
* scanning every project dir
|
|
200
|
-
*
|
|
201
|
-
* derived. '' when there is no session or the
|
|
199
|
+
* The main agent's own transcript: `<config>/projects/<cwd-slug>/<sessionId>.jsonl`. Located by
|
|
200
|
+
* scanning every project dir under every root {@link ClaudeConfigDir.projectsRoots} names, for the file named
|
|
201
|
+
* after the session — so the cwd-slug, which is a mangling of the working directory we would
|
|
202
|
+
* otherwise have to reproduce exactly, never has to be derived. '' when there is no session or the
|
|
203
|
+
* file is not there.
|
|
204
|
+
*
|
|
205
|
+
* THIS FIELD IS THE DIAGNOSTIC. It is found by session id alone — no cwd, no branch, no stamping —
|
|
206
|
+
* so an empty one while {@link sessionId} is set can ONLY mean the transcript ROOT is wrong. That is
|
|
207
|
+
* exactly what a hardcoded `~/.claude` produced on a machine with `$CLAUDE_CONFIG_DIR` relocated,
|
|
208
|
+
* and the gate then refused every PR with a message about the reviewers' cwd.
|
|
202
209
|
*/
|
|
203
210
|
mainTranscript() {
|
|
204
211
|
const session = this.sessionId();
|
|
205
212
|
if (session === '')
|
|
206
213
|
return '';
|
|
207
|
-
const projects
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
for (const projects of claude_config_dir_1.claudeConfigDir.projectsRoots()) {
|
|
215
|
+
for (const proj of this.readDir(projects)) {
|
|
216
|
+
const candidate = path.join(projects, proj, `${session}.jsonl`);
|
|
217
|
+
if (fs.existsSync(candidate))
|
|
218
|
+
return candidate;
|
|
219
|
+
}
|
|
212
220
|
}
|
|
213
221
|
return '';
|
|
214
222
|
}
|
|
215
223
|
/**
|
|
216
|
-
* How many days Claude Code keeps transcripts: `cleanupPeriodDays` from
|
|
217
|
-
* settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent,
|
|
218
|
-
* why the default is documented rather than left implicit.
|
|
224
|
+
* How many days Claude Code keeps transcripts: `cleanupPeriodDays` from `<config>/settings.json`
|
|
225
|
+
* (then settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent,
|
|
226
|
+
* which is why the default is documented rather than left implicit.
|
|
227
|
+
*
|
|
228
|
+
* `<config>` is every root {@link ClaudeConfigDir.roots} names, for the same reason the transcripts are
|
|
229
|
+
* searched in both: a hardcoded `~/.claude` silently read SOMEBODY ELSE'S retention (or none) on a
|
|
230
|
+
* relocated config dir, so the recorded `transcriptsExpireOn` was computed from a default rather
|
|
231
|
+
* than from the setting the owner actually wrote.
|
|
219
232
|
*/
|
|
220
233
|
retentionDays() {
|
|
221
|
-
const dir
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
for (const dir of claude_config_dir_1.claudeConfigDir.roots()) {
|
|
235
|
+
for (const file of ['settings.json', 'settings.local.json']) {
|
|
236
|
+
const settings = this.readJson(path.join(dir, file));
|
|
237
|
+
const days = settings?.['cleanupPeriodDays'];
|
|
238
|
+
if (typeof days === 'number' && Number.isFinite(days) && days > 0)
|
|
239
|
+
return days;
|
|
240
|
+
}
|
|
227
241
|
}
|
|
228
242
|
return exports.DEFAULT_RETENTION_DAYS;
|
|
229
243
|
}
|
|
@@ -1 +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 checklist's generated <id>.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 // Named its own verdict file in a tool input — the row's answer to \"who wrote this verdict?\", and the\n // strongest of the two channels that can credit a reviewer whose harness-stamped cwd points elsewhere.\n wroteVerdict: boolean;\n toolCallCount: number;\n offRepoSearches: number;\n /**\n * The model(s) that ACTUALLY served this reviewer, observed in its transcript — not the `model:` its\n * agent file requested, which can silently disagree, and not anything the reviewer claimed.\n *\n * Copied here for the reason the whole class exists: the transcript is a wasting asset (~30 days),\n * and after it expires this row is the only surviving answer to \"which model reviewed this?\".\n */\n models: string[];\n\n constructor(evidence: ReviewerEvidence, paths: ReviewerPaths) {\n this.id = evidence.checklistId; // the checklist; agentType is the (shared) reviewer agent type\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.wroteVerdict = evidence.wroteVerdict;\n this.toolCallCount = evidence.toolCallCount;\n this.offRepoSearches = evidence.offRepoSearches;\n this.models = evidence.models;\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 /**\n * The dash-sanitized FEATURE SLUG, not a git branch — `AiBranchName.getFeatureName()`, which is\n * `baseBranchName(branch).replace(/\\//g, '-')`. It names the on-disk `pr-review/<slug>/` dir.\n *\n * It was called `branch`, and that mislabel cost a real investigation: a bug report against the\n * provenance gate reasoned from `provenance.json`'s dash-form `branch` that `sidechainOnBranch` must\n * be comparing a dash-form name against a slash-form git branch, and went looking for a missing\n * normalization. There is none — both sides of that comparison are slash-form\n * (`git branch --show-current`) and this field never reaches it. A field called `branch` that never\n * holds a branch is what sent the reader down that path; the name is now what it holds.\n */\n featureSlug: 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, featureSlug: string, headSha: string, provenanceStatus: string) {\n this.prDir = prDir;\n this.featureSlug = featureSlug;\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 // The JSON key too: a reader of provenance.json sees `featureSlug`, matching the `pr-review/<slug>/`\n // dir it sits in. See ProvenanceWriteRequest.featureSlug for why the old `branch` key was a defect.\n featureSlug: 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.featureSlug = request.featureSlug;\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/*/<sessionId>.jsonl → the main agent's transcript\n * ~/.claude/projects/*/<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"]}
|
|
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 { injectable, bindingScopeValues } from 'inversify';\nimport { claudeConfigDir } from './claude-config-dir';\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 checklist's generated <id>.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 // Named its own verdict file in a tool input — the row's answer to \"who wrote this verdict?\", and the\n // strongest of the two channels that can credit a reviewer whose harness-stamped cwd points elsewhere.\n wroteVerdict: boolean;\n toolCallCount: number;\n offRepoSearches: number;\n /**\n * The model(s) that ACTUALLY served this reviewer, observed in its transcript — not the `model:` its\n * agent file requested, which can silently disagree, and not anything the reviewer claimed.\n *\n * Copied here for the reason the whole class exists: the transcript is a wasting asset (~30 days),\n * and after it expires this row is the only surviving answer to \"which model reviewed this?\".\n */\n models: string[];\n\n constructor(evidence: ReviewerEvidence, paths: ReviewerPaths) {\n this.id = evidence.checklistId; // the checklist; agentType is the (shared) reviewer agent type\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.wroteVerdict = evidence.wroteVerdict;\n this.toolCallCount = evidence.toolCallCount;\n this.offRepoSearches = evidence.offRepoSearches;\n this.models = evidence.models;\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 /**\n * The dash-sanitized FEATURE SLUG, not a git branch — `AiBranchName.getFeatureName()`, which is\n * `baseBranchName(branch).replace(/\\//g, '-')`. It names the on-disk `pr-review/<slug>/` dir.\n *\n * It was called `branch`, and that mislabel cost a real investigation: a bug report against the\n * provenance gate reasoned from `provenance.json`'s dash-form `branch` that `sidechainOnBranch` must\n * be comparing a dash-form name against a slash-form git branch, and went looking for a missing\n * normalization. There is none — both sides of that comparison are slash-form\n * (`git branch --show-current`) and this field never reaches it. A field called `branch` that never\n * holds a branch is what sent the reader down that path; the name is now what it holds.\n */\n featureSlug: 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, featureSlug: string, headSha: string, provenanceStatus: string) {\n this.prDir = prDir;\n this.featureSlug = featureSlug;\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 // The JSON key too: a reader of provenance.json sees `featureSlug`, matching the `pr-review/<slug>/`\n // dir it sits in. See ProvenanceWriteRequest.featureSlug for why the old `branch` key was a defect.\n featureSlug: 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.featureSlug = request.featureSlug;\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 * <config>/projects/*/<sessionId>.jsonl → the main agent's transcript\n * <config>/projects/*/<sessionId>/subagents/agent-<id>.jsonl → one reviewer's transcript\n * where `<config>` is {@link ClaudeConfigDir.root} — `$CLAUDE_CONFIG_DIR` when set, else `~/.claude`.\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 config tree 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: `<config>/projects/<cwd-slug>/<sessionId>.jsonl`. Located by\n * scanning every project dir under every root {@link ClaudeConfigDir.projectsRoots} names, for the file named\n * after the session — so the cwd-slug, which is a mangling of the working directory we would\n * otherwise have to reproduce exactly, never has to be derived. '' when there is no session or the\n * file is not there.\n *\n * THIS FIELD IS THE DIAGNOSTIC. It is found by session id alone — no cwd, no branch, no stamping —\n * so an empty one while {@link sessionId} is set can ONLY mean the transcript ROOT is wrong. That is\n * exactly what a hardcoded `~/.claude` produced on a machine with `$CLAUDE_CONFIG_DIR` relocated,\n * and the gate then refused every PR with a message about the reviewers' cwd.\n */\n mainTranscript(): string {\n const session = this.sessionId();\n if (session === '') return '';\n for (const projects of claudeConfigDir.projectsRoots()) {\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 }\n return '';\n }\n\n /**\n * How many days Claude Code keeps transcripts: `cleanupPeriodDays` from `<config>/settings.json`\n * (then settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent,\n * which is why the default is documented rather than left implicit.\n *\n * `<config>` is every root {@link ClaudeConfigDir.roots} names, for the same reason the transcripts are\n * searched in both: a hardcoded `~/.claude` silently read SOMEBODY ELSE'S retention (or none) on a\n * relocated config dir, so the recorded `transcriptsExpireOn` was computed from a default rather\n * than from the setting the owner actually wrote.\n */\n retentionDays(): number {\n for (const dir of claudeConfigDir.roots()) {\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 }\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"]}
|
|
@@ -113,11 +113,16 @@ export declare class ReviewerContext {
|
|
|
113
113
|
constructor(branch: string, diffDir?: string, docPaths?: Record<string, string>, verdictPaths?: Record<string, string>);
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
|
-
* One checklist that must be credited to a reviewer run,
|
|
116
|
+
* One checklist that must be credited to a reviewer run, plus the agent type the BRIEFING named. Data-only.
|
|
117
117
|
*
|
|
118
118
|
* The two used to be one string — a checklist's id WAS its agent type — and provenance keyed everything by
|
|
119
119
|
* it. Now every checklist is reviewed by the same repo-wide agent type, so the id and the type are
|
|
120
|
-
* separate facts
|
|
120
|
+
* separate facts.
|
|
121
|
+
*
|
|
122
|
+
* `agentType` is NOT a provenance input: nothing here matches a run on it (issue #964, see
|
|
123
|
+
* {@link SubagentProvenanceService.isReviewerRun}). It is carried because it is what the BRIEFING told
|
|
124
|
+
* the agent to spawn and what {@link ReviewerEvidence} reports, which are both worth recording even
|
|
125
|
+
* though neither is worth gating on.
|
|
121
126
|
*/
|
|
122
127
|
export declare class ExpectedReviewer {
|
|
123
128
|
checklistId: string;
|
|
@@ -126,16 +131,18 @@ export declare class ExpectedReviewer {
|
|
|
126
131
|
}
|
|
127
132
|
/**
|
|
128
133
|
* Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
|
|
129
|
-
* a
|
|
130
|
-
*
|
|
131
|
-
*
|
|
134
|
+
* a real SUBAGENT actually ran on the current branch and did this checklist's work. Used to enforce that
|
|
135
|
+
* every checklist was reviewed by an INDEPENDENT reviewer, rather than by the coding agent
|
|
136
|
+
* self-certifying.
|
|
132
137
|
*
|
|
133
138
|
* The harness writes, beside each subagent transcript:
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* `
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
+
* <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
|
|
140
|
+
* <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
|
|
141
|
+
* `spawnDepth`/`isSidechain` are written by Claude Code, not by the model, and they are what carries the
|
|
142
|
+
* anti-self-certification property. `agentType` is written there too and is deliberately NOT checked —
|
|
143
|
+
* see {@link SubagentProvenanceService.isReviewerRun}. `<config>` is `$CLAUDE_CONFIG_DIR` when set, else
|
|
144
|
+
* `~/.claude`; see {@link ClaudeConfigDir}, and never re-derive it here. Dirs are found by walking
|
|
145
|
+
* every session, so the cwd-slug never has to be derived/guessed.
|
|
139
146
|
*
|
|
140
147
|
* IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This
|
|
141
148
|
* raises the bar from "trust the model's word" to "deliberate, auditable forgery outside the repo"; it
|
|
@@ -201,8 +208,40 @@ export declare class SubagentProvenanceService {
|
|
|
201
208
|
private inClaudeSession;
|
|
202
209
|
private skipped;
|
|
203
210
|
private findMatchingAgentId;
|
|
211
|
+
/**
|
|
212
|
+
* A harness-written meta for a REAL SUBAGENT — `spawnDepth >= 1`, and nothing about its NAME.
|
|
213
|
+
*
|
|
214
|
+
* The agent TYPE used to be compared against the configured reviewer agent, and that check is
|
|
215
|
+
* gone (issue #964). It proved nothing: `agentType` is a label on a prompt, and it does not show
|
|
216
|
+
* the agent read the checklist, opened the diff, or looked at the right branch. The gates that DO
|
|
217
|
+
* show those are untouched — `spawnDepth >= 1` plus `isSidechain === true` (see
|
|
218
|
+
* {@link sidechainOnBranch}) are harness-written, so the main loop still cannot self-certify;
|
|
219
|
+
* {@link creditedByWhatItTouched} proves the branch and the actual work; `requireDiffEvidence`
|
|
220
|
+
* proves the diff was opened.
|
|
221
|
+
*
|
|
222
|
+
* It was also the ONLY gate with an unfixable failure mode. Claude Code snapshots agent
|
|
223
|
+
* definitions per session and the refresh does not reach an already-running subagent, so a repo
|
|
224
|
+
* that renames its reviewer agent mid-session leaves that subagent's registry offering only the
|
|
225
|
+
* OLD names: the spawn is rejected, the agent falls back to `general-purpose`, does the real
|
|
226
|
+
* review, writes every verdict — and is refused as unattributable, with no cwd, branch or respawn
|
|
227
|
+
* able to change it. Measured: six checklists, six verdict files on disk, PR blocked.
|
|
228
|
+
*
|
|
229
|
+
* Accepted trade-off: without the name check the implementer's own subagent could in principle be
|
|
230
|
+
* credited, since it touches the diff dir while running the gate. That is deliberate. The rule is
|
|
231
|
+
* "if the verdict file is there, a subagent wrote it", and this gate's job is to catch "no
|
|
232
|
+
* subagent ran at all" — not to adjudicate which subagent it was.
|
|
233
|
+
*/
|
|
204
234
|
private isReviewerRun;
|
|
205
235
|
private namedVerdict;
|
|
236
|
+
/**
|
|
237
|
+
* Every `<config>/projects/*/<session>/subagents` dir that exists — matching by the recorded
|
|
238
|
+
* gitBranch (not by session id) is what makes provenance survive across sessions.
|
|
239
|
+
*
|
|
240
|
+
* `<config>` comes from {@link ClaudeConfigDir.projectsRoots}, which is BOTH the configured
|
|
241
|
+
* `$CLAUDE_CONFIG_DIR` tree and `~/.claude`. This used to be a hardcoded `~/.claude`, and on a
|
|
242
|
+
* machine with a relocated config dir that found nothing at all — so every PR was refused with a
|
|
243
|
+
* message about the reviewers' cwd, which is not what was wrong and which no agent could act on.
|
|
244
|
+
*/
|
|
206
245
|
private allSubagentsDirs;
|
|
207
246
|
private metaFiles;
|
|
208
247
|
private agentIdOf;
|
|
@@ -5,8 +5,8 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const fs = tslib_1.__importStar(require("fs"));
|
|
7
7
|
const path = tslib_1.__importStar(require("path"));
|
|
8
|
-
const os = tslib_1.__importStar(require("os"));
|
|
9
8
|
const inversify_1 = require("inversify");
|
|
9
|
+
const claude_config_dir_1 = require("./claude-config-dir");
|
|
10
10
|
const to_error_1 = require("./to-error");
|
|
11
11
|
const state_dir_1 = require("./state-dir");
|
|
12
12
|
// Outcome of a provenance check.
|
|
@@ -162,11 +162,16 @@ class ReviewerContext {
|
|
|
162
162
|
}
|
|
163
163
|
exports.ReviewerContext = ReviewerContext;
|
|
164
164
|
/**
|
|
165
|
-
* One checklist that must be credited to a reviewer run,
|
|
165
|
+
* One checklist that must be credited to a reviewer run, plus the agent type the BRIEFING named. Data-only.
|
|
166
166
|
*
|
|
167
167
|
* The two used to be one string — a checklist's id WAS its agent type — and provenance keyed everything by
|
|
168
168
|
* it. Now every checklist is reviewed by the same repo-wide agent type, so the id and the type are
|
|
169
|
-
* separate facts
|
|
169
|
+
* separate facts.
|
|
170
|
+
*
|
|
171
|
+
* `agentType` is NOT a provenance input: nothing here matches a run on it (issue #964, see
|
|
172
|
+
* {@link SubagentProvenanceService.isReviewerRun}). It is carried because it is what the BRIEFING told
|
|
173
|
+
* the agent to spawn and what {@link ReviewerEvidence} reports, which are both worth recording even
|
|
174
|
+
* though neither is worth gating on.
|
|
170
175
|
*/
|
|
171
176
|
class ExpectedReviewer {
|
|
172
177
|
checklistId;
|
|
@@ -179,16 +184,18 @@ class ExpectedReviewer {
|
|
|
179
184
|
exports.ExpectedReviewer = ExpectedReviewer;
|
|
180
185
|
/**
|
|
181
186
|
* Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
|
|
182
|
-
* a
|
|
183
|
-
*
|
|
184
|
-
*
|
|
187
|
+
* a real SUBAGENT actually ran on the current branch and did this checklist's work. Used to enforce that
|
|
188
|
+
* every checklist was reviewed by an INDEPENDENT reviewer, rather than by the coding agent
|
|
189
|
+
* self-certifying.
|
|
185
190
|
*
|
|
186
191
|
* The harness writes, beside each subagent transcript:
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* `
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
+
* <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
|
|
193
|
+
* <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
|
|
194
|
+
* `spawnDepth`/`isSidechain` are written by Claude Code, not by the model, and they are what carries the
|
|
195
|
+
* anti-self-certification property. `agentType` is written there too and is deliberately NOT checked —
|
|
196
|
+
* see {@link SubagentProvenanceService.isReviewerRun}. `<config>` is `$CLAUDE_CONFIG_DIR` when set, else
|
|
197
|
+
* `~/.claude`; see {@link ClaudeConfigDir}, and never re-derive it here. Dirs are found by walking
|
|
198
|
+
* every session, so the cwd-slug never has to be derived/guessed.
|
|
192
199
|
*
|
|
193
200
|
* IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This
|
|
194
201
|
* raises the bar from "trust the model's word" to "deliberate, auditable forgery outside the repo"; it
|
|
@@ -388,19 +395,20 @@ let SubagentProvenanceService = class SubagentProvenanceService {
|
|
|
388
395
|
skipped(what) {
|
|
389
396
|
return new ProvenanceResult(exports.PROVENANCE_SKIPPED, `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`, {}, []);
|
|
390
397
|
}
|
|
391
|
-
// The agentId of a
|
|
392
|
-
//
|
|
393
|
-
//
|
|
398
|
+
// The agentId of a subagent run on `branch`, searched across ALL sessions' subagent dirs
|
|
399
|
+
// (branch-scoped, so a run from a prior session still counts). '' if none. `exclude` skips agentIds
|
|
400
|
+
// already credited to another checklist so one run can't satisfy two.
|
|
394
401
|
//
|
|
395
|
-
//
|
|
396
|
-
// checklist's verdict file wins; the first
|
|
402
|
+
// The run is NOT matched on its agent type — see isReviewerRun for why that check was removed — so
|
|
403
|
+
// several runs match and the one that NAMED this checklist's verdict file wins; the first
|
|
404
|
+
// branch-matching run is the fallback, as before.
|
|
397
405
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
398
406
|
findMatchingAgentId(dirs, want, context, exclude) {
|
|
399
407
|
let fallback = '';
|
|
400
408
|
for (const dir of dirs) {
|
|
401
409
|
for (const metaFile of this.metaFiles(dir)) {
|
|
402
410
|
const agentId = this.agentIdOf(metaFile);
|
|
403
|
-
if (exclude.has(agentId) || !this.isReviewerRun(dir, metaFile
|
|
411
|
+
if (exclude.has(agentId) || !this.isReviewerRun(dir, metaFile))
|
|
404
412
|
continue;
|
|
405
413
|
if (!this.sidechainOnBranch(dir, agentId, want.checklistId, context))
|
|
406
414
|
continue;
|
|
@@ -412,10 +420,32 @@ let SubagentProvenanceService = class SubagentProvenanceService {
|
|
|
412
420
|
}
|
|
413
421
|
return fallback;
|
|
414
422
|
}
|
|
415
|
-
|
|
416
|
-
|
|
423
|
+
/**
|
|
424
|
+
* A harness-written meta for a REAL SUBAGENT — `spawnDepth >= 1`, and nothing about its NAME.
|
|
425
|
+
*
|
|
426
|
+
* The agent TYPE used to be compared against the configured reviewer agent, and that check is
|
|
427
|
+
* gone (issue #964). It proved nothing: `agentType` is a label on a prompt, and it does not show
|
|
428
|
+
* the agent read the checklist, opened the diff, or looked at the right branch. The gates that DO
|
|
429
|
+
* show those are untouched — `spawnDepth >= 1` plus `isSidechain === true` (see
|
|
430
|
+
* {@link sidechainOnBranch}) are harness-written, so the main loop still cannot self-certify;
|
|
431
|
+
* {@link creditedByWhatItTouched} proves the branch and the actual work; `requireDiffEvidence`
|
|
432
|
+
* proves the diff was opened.
|
|
433
|
+
*
|
|
434
|
+
* It was also the ONLY gate with an unfixable failure mode. Claude Code snapshots agent
|
|
435
|
+
* definitions per session and the refresh does not reach an already-running subagent, so a repo
|
|
436
|
+
* that renames its reviewer agent mid-session leaves that subagent's registry offering only the
|
|
437
|
+
* OLD names: the spawn is rejected, the agent falls back to `general-purpose`, does the real
|
|
438
|
+
* review, writes every verdict — and is refused as unattributable, with no cwd, branch or respawn
|
|
439
|
+
* able to change it. Measured: six checklists, six verdict files on disk, PR blocked.
|
|
440
|
+
*
|
|
441
|
+
* Accepted trade-off: without the name check the implementer's own subagent could in principle be
|
|
442
|
+
* credited, since it touches the diff dir while running the gate. That is deliberate. The rule is
|
|
443
|
+
* "if the verdict file is there, a subagent wrote it", and this gate's job is to catch "no
|
|
444
|
+
* subagent ran at all" — not to adjudicate which subagent it was.
|
|
445
|
+
*/
|
|
446
|
+
isReviewerRun(dir, metaFile) {
|
|
417
447
|
const meta = this.readJson(path.join(dir, metaFile));
|
|
418
|
-
if (!meta
|
|
448
|
+
if (!meta)
|
|
419
449
|
return false;
|
|
420
450
|
const spawnDepth = meta['spawnDepth'];
|
|
421
451
|
return typeof spawnDepth === 'number' && spawnDepth >= 1;
|
|
@@ -429,19 +459,25 @@ let SubagentProvenanceService = class SubagentProvenanceService {
|
|
|
429
459
|
return false;
|
|
430
460
|
return this.mentions(this.scanTranscript(jsonl).inputs, verdictPath);
|
|
431
461
|
}
|
|
432
|
-
|
|
433
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Every `<config>/projects/*/<session>/subagents` dir that exists — matching by the recorded
|
|
464
|
+
* gitBranch (not by session id) is what makes provenance survive across sessions.
|
|
465
|
+
*
|
|
466
|
+
* `<config>` comes from {@link ClaudeConfigDir.projectsRoots}, which is BOTH the configured
|
|
467
|
+
* `$CLAUDE_CONFIG_DIR` tree and `~/.claude`. This used to be a hardcoded `~/.claude`, and on a
|
|
468
|
+
* machine with a relocated config dir that found nothing at all — so every PR was refused with a
|
|
469
|
+
* message about the reviewers' cwd, which is not what was wrong and which no agent could act on.
|
|
470
|
+
*/
|
|
434
471
|
allSubagentsDirs() {
|
|
435
|
-
const projects = path.join(os.homedir(), '.claude', 'projects');
|
|
436
|
-
if (!fs.existsSync(projects))
|
|
437
|
-
return [];
|
|
438
472
|
const out = [];
|
|
439
|
-
for (const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
473
|
+
for (const projects of claude_config_dir_1.claudeConfigDir.projectsRoots()) {
|
|
474
|
+
for (const proj of this.readDir(projects)) {
|
|
475
|
+
const projDir = path.join(projects, proj);
|
|
476
|
+
for (const session of this.readDir(projDir)) {
|
|
477
|
+
const candidate = path.join(projDir, session, 'subagents');
|
|
478
|
+
if (fs.existsSync(candidate))
|
|
479
|
+
out.push(candidate);
|
|
480
|
+
}
|
|
445
481
|
}
|
|
446
482
|
}
|
|
447
483
|
return out;
|