@claudexor/review 1.0.1 → 2.1.3

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.
@@ -1,54 +1,99 @@
1
1
  import { parseUnifiedDiff, runCapture } from "@claudexor/core";
2
2
  import { preflightEvidence, writeDiffEvidence } from "@claudexor/context";
3
- import { FallbackReason, HarnessRunSpec, ReviewFinding as ReviewFindingSchema, } from "@claudexor/schema";
4
- import { existsSync, lstatSync, readlinkSync, realpathSync } from "node:fs";
3
+ import { HarnessRunSpec, ReviewFinding as ReviewFindingSchema } from "@claudexor/schema";
4
+ import { existsSync, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
5
5
  import { cp, mkdir, readdir, rm } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
8
- import { appendLine, containsSecretLikeToken, ensureDir, newId, nowIso, readTextSafe, redactSecrets, sha256, writeJson, writeText, } from "@claudexor/util";
8
+ import { appendLine, containsSecretLikeToken, ensureDir, newId, nowIso, readTextSafe, redactSecrets, sensitiveResourcePolicy, sha256, writeJson, writeText, } from "@claudexor/util";
9
9
  import { dedupeFindings, extractJsonBlocks, parseFindingsDetailed, } from "./findings.js";
10
+ import { buildReviewPrompt } from "./reviewPrompt.js";
10
11
  import { buildRouteProof, classifyDiversity } from "./route.js";
12
+ import { reviewerAuthMode, reviewerAuthSwitchFromEvent, summarizeReviewerSpend, } from "./reviewRuntimeTypes.js";
11
13
  const DEFAULT_REVIEWER_TIMEOUT_MS = 10 * 60_000;
12
14
  const DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY = {
13
15
  maxRetries: 2,
14
16
  initialDelayMs: 1_000,
15
17
  maxDelayMs: 10_000,
16
18
  };
17
- function reviewPrompt(label, candidateRoot, evidenceDir, patch) {
18
- return [
19
- "You are an adversarial code reviewer.",
20
- `Candidate root: ${candidateRoot}.`,
21
- `First read the evidence packet in ${evidenceDir} (USER_INTENT.md, FORBIDDEN_FINDINGS.md, PLAN_ACCEPTED.md, DECIDED_TRADEOFFS.md, TESTS.txt, DIFF.patch, DIFF_SUMMARY.md). If a mandatory file is missing, return INSUFFICIENT_EVIDENCE.`,
22
- `Review ${label}'s change from the file-backed patch artifact, not from this prompt. Full patch: ${patch.diffPath}. Summary: ${patch.summaryPath}. Patch digest: ${patch.diffSha256}.`,
23
- "All code/file evidence must come from Candidate root or the evidence packet. Do not inspect or cite sibling/base repository paths outside Candidate root; if required evidence is unavailable there, return INSUFFICIENT_EVIDENCE.",
24
- "Treat TESTS.txt as the gate evidence. Do not rerun full build/test gates from the review; run only small targeted commands when needed to verify a concrete finding.",
25
- "In finding evidence, cite candidate files with paths relative to Candidate root. Cite evidence packet files by their evidence filename (for example DIFF.patch or TESTS.txt). Do not cite absolute Candidate root, reviewer workspace, or evidenceDir paths; those are disposable transport paths and will be rejected as evidence.",
26
- "Output ONLY a JSON array of findings.",
27
- `Each finding: {"severity":"BLOCK|FIX_FIRST|WARN|NIT|OUT_OF_SCOPE|INSUFFICIENT_EVIDENCE|NEEDS_HUMAN","category":"correctness|regression|security|performance|maintainability|test_gap|spec_gap|deploy|architecture|ux","claim":"...","evidence":{"files":[{"path":"...","lines":"..."}]},"proposed_fix":"..."}.`,
28
- "Rules: no evidence => do NOT use BLOCK. Do not relitigate FORBIDDEN_FINDINGS or DECIDED_TRADEOFFS.",
29
- "",
30
- "Patch summary (not a replacement for reading DIFF.patch):",
31
- patch.summary,
32
- ].join("\n");
19
+ const BLOCKED_REVIEWER_RUNTIME_ROOTS = new Set("auth cache daemon home homes logs runs secrets state tmp workspaces".split(" "));
20
+ const TEXT_EVIDENCE_SUFFIXES = [".md", ".txt", ".json", ".yaml", ".yml", ".patch"];
21
+ const REVIEW_WAVE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
22
+ function readExistingDiffEvidence(dir, diff) {
23
+ const diffText = diff.endsWith("\n") ? diff : `${diff}\n`;
24
+ const summaryPath = join(dir, "DIFF_SUMMARY.md");
25
+ const summary = readTextSafe(summaryPath);
26
+ if (summary === null)
27
+ throw new Error("sealed review packet is missing DIFF_SUMMARY.md");
28
+ return {
29
+ diffPath: join(dir, "DIFF.patch"),
30
+ summaryPath,
31
+ diffSha256: sha256(diffText),
32
+ summary,
33
+ };
34
+ }
35
+ function reviewerRouteProof(reviewer, modelId, source, peerFamilies) {
36
+ return buildRouteProof({
37
+ harness_id: reviewer.adapter.id,
38
+ provider_family: reviewer.providerFamily,
39
+ model_hint: reviewer.requestedModel ?? null,
40
+ }, {
41
+ provider: reviewer.providerFamily,
42
+ model_id: modelId,
43
+ evidence_source: modelId ? source : "unavailable",
44
+ }, peerFamilies);
45
+ }
46
+ function reviewerInfo(reviewer, routeProofStatus, observedModel = null) {
47
+ return {
48
+ harness_id: reviewer.adapter.id,
49
+ requested_model: reviewer.requestedModel ?? null,
50
+ requested_effort: reviewer.requestedEffort ?? null,
51
+ observed_model: observedModel,
52
+ route_proof_status: routeProofStatus,
53
+ };
33
54
  }
34
- /**
35
- * Cross-family review of one anonymized candidate. Each reviewer runs its review
36
- * intent and emits JSON findings; we attach route proofs and verify the
37
- * reviewers span >= 2 distinct provider families.
38
- */
39
55
  export async function reviewCandidate(input) {
40
56
  const findingsByReviewer = input.reviewers.map(() => []);
41
- const routeProofs = [];
42
- const reviewerRequests = [];
43
- const healthyFamilies = new Set();
57
+ const reviewerFamilies = input.reviewers.map((reviewer) => reviewer.providerFamily);
58
+ const routeProofs = input.reviewers.map((reviewer, index) => reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies.filter((_, otherIndex) => otherIndex !== index)));
59
+ const reviewerRequests = input.reviewers.map((reviewer) => ({
60
+ harness_id: reviewer.adapter.id,
61
+ provider_family: reviewer.providerFamily,
62
+ requested_model: reviewer.requestedModel ?? null,
63
+ requested_effort: reviewer.requestedEffort ?? null,
64
+ }));
44
65
  const healthyReviewerIndexes = new Set();
45
- let reviewSpendUsd = 0;
46
- let reviewSpendEstimated = false;
66
+ const reviewSpendByReviewer = input.reviewers.map(() => 0);
67
+ const reviewSpendEstimatedByReviewer = input.reviewers.map(() => false);
68
+ const reviewCashByReviewer = input.reviewers.map(() => 0);
69
+ const reviewValuationByReviewer = input.reviewers.map(() => 0);
70
+ const reviewUnknownByReviewer = input.reviewers.map(() => 0);
47
71
  const reviewerTimeoutMs = input.reviewerTimeoutMs ?? DEFAULT_REVIEWER_TIMEOUT_MS;
72
+ const reviewWaveId = input.env?.["CLAUDEXOR_REVIEW_WAVE_ID"] ?? process.env["CLAUDEXOR_REVIEW_WAVE_ID"] ?? null;
73
+ if (input.evidenceReadOnly && input.frozenIdentity && !REVIEW_WAVE_ID.test(reviewWaveId ?? "")) {
74
+ throw new Error("sealed release review requires CLAUDEXOR_REVIEW_WAVE_ID UUID");
75
+ }
76
+ const frozenMetadata = input.frozenIdentity
77
+ ? {
78
+ candidate_sha: input.frozenIdentity.candidateSha,
79
+ candidate_tree: input.frozenIdentity.candidateTree,
80
+ packet_manifest_sha256: input.frozenIdentity.packetManifestSha256,
81
+ ...(reviewWaveId ? { review_wave_id: reviewWaveId } : {}),
82
+ }
83
+ : {};
48
84
  if (containsSecretLikeToken(input.diff || "(empty diff)\n")) {
49
85
  throw new Error("diff evidence contains a secret-like token; refusing to persist raw DIFF.patch");
50
86
  }
51
- writeDiffEvidence(input.evidenceDir, input.diff);
87
+ if (input.evidenceReadOnly) {
88
+ const packetDiff = readTextSafe(join(input.evidenceDir, "DIFF.patch"));
89
+ const normalizedDiff = input.diff.endsWith("\n") ? input.diff : `${input.diff}\n`;
90
+ if (packetDiff === null || packetDiff !== normalizedDiff) {
91
+ throw new Error("sealed review packet DIFF.patch does not match the verified review diff");
92
+ }
93
+ }
94
+ else {
95
+ writeDiffEvidence(input.evidenceDir, input.diff);
96
+ }
52
97
  const preflight = preflightEvidence(input.evidenceDir);
53
98
  if (!preflight.ok) {
54
99
  const parts = [
@@ -60,69 +105,72 @@ export async function reviewCandidate(input) {
60
105
  const artifactsBaseDir = input.artifactsDir ?? join(input.evidenceDir, "reviewer-artifacts");
61
106
  ensureDir(artifactsBaseDir);
62
107
  const persistentEvidenceDir = join(artifactsBaseDir, "evidence");
63
- await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir);
64
- const persistentPatch = writeDiffEvidence(persistentEvidenceDir, input.diff);
65
- writeJson(join(persistentEvidenceDir, "metadata.json"), {
108
+ await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir, input.evidenceReadOnly === true);
109
+ const persistentPatch = input.evidenceReadOnly
110
+ ? readExistingDiffEvidence(persistentEvidenceDir, input.diff)
111
+ : writeDiffEvidence(persistentEvidenceDir, input.diff);
112
+ writeJson(input.evidenceReadOnly
113
+ ? join(artifactsBaseDir, "evidence-metadata.json")
114
+ : join(persistentEvidenceDir, "metadata.json"), {
66
115
  source_evidence_dir: input.evidenceDir,
67
116
  candidate_root: input.cwd,
68
117
  persistent_evidence_dir: persistentEvidenceDir,
69
118
  diff_path: persistentPatch.diffPath,
70
119
  summary_path: persistentPatch.summaryPath,
71
120
  diff_sha256: persistentPatch.diffSha256,
121
+ review_subject: input.reviewSubject ?? "code",
122
+ ...frozenMetadata,
72
123
  });
73
- const artifacts = [];
74
- const reviewerFamilies = input.reviewers.map((r) => r.providerFamily);
124
+ const artifacts = input.reviewers.map(() => undefined);
75
125
  const preservePaths = extractDiffTouchedPaths(input.diff);
76
126
  const reviewerWorkspaceBaseDir = selectReviewerWorkspaceBaseDir(input.cwd, artifactsBaseDir, input.evidenceDir);
77
- try {
78
- for (const [index, reviewer] of input.reviewers.entries()) {
79
- if (input.signal?.aborted)
80
- break;
81
- reviewerRequests.push({
82
- harness_id: reviewer.adapter.id,
83
- provider_family: reviewer.providerFamily,
84
- requested_model: reviewer.requestedModel ?? null,
85
- requested_effort: reviewer.requestedEffort ?? null,
127
+ const runReviewer = async (reviewer, index) => {
128
+ if (input.signal?.aborted)
129
+ return;
130
+ const artifact = createReviewerArtifactContext(artifactsBaseDir, index, reviewer);
131
+ artifacts[index] = artifact;
132
+ let reviewerWorkspace = null;
133
+ let spec = null;
134
+ try {
135
+ reviewerWorkspace = await prepareReviewerWorkspace({
136
+ sourceRoot: input.cwd,
137
+ sourceEvidenceDir: persistentEvidenceDir,
138
+ workspaceBaseDir: reviewerWorkspaceBaseDir,
139
+ reviewerDirName: `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`,
140
+ excludeRoots: [artifactsBaseDir],
141
+ preservePaths,
142
+ preserveEvidenceBytes: input.evidenceReadOnly === true,
86
143
  });
87
- const artifact = createReviewerArtifactContext(artifactsBaseDir, index, reviewer);
88
- artifacts.push(artifact);
89
- let reviewerWorkspace = null;
90
- let spec = null;
91
- try {
92
- reviewerWorkspace = await prepareReviewerWorkspace({
93
- sourceRoot: input.cwd,
94
- sourceEvidenceDir: persistentEvidenceDir,
95
- workspaceBaseDir: reviewerWorkspaceBaseDir,
96
- reviewerDirName: `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`,
97
- excludeRoots: [artifactsBaseDir],
98
- preservePaths,
99
- });
100
- const reviewerPatch = writeDiffEvidence(reviewerWorkspace.evidenceDir, input.diff);
101
- updateReviewerMetadata(artifact, {
102
- candidate_evidence_dir: reviewerWorkspace.evidenceDir,
103
- candidate_root: reviewerWorkspace.root,
104
- source_candidate_evidence_dir: input.evidenceDir,
105
- source_candidate_root: input.cwd,
106
- reviewer_workspace_root: reviewerWorkspace.root,
107
- persistent_evidence_dir: persistentEvidenceDir,
108
- persistent_diff_path: persistentPatch.diffPath,
109
- persistent_summary_path: persistentPatch.summaryPath,
110
- diff_sha256: persistentPatch.diffSha256,
111
- });
112
- const runtimePrompt = reviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch);
113
- spec = HarnessRunSpec.parse({
114
- session_id: newId("rev"),
115
- intent: "review",
116
- prompt: runtimePrompt,
117
- cwd: reviewerWorkspace.root,
118
- access: "readonly",
119
- model_hint: reviewer.requestedModel ?? null,
120
- effort_hint: reviewer.requestedEffort ?? null,
121
- auth_preference: reviewer.authPreference ?? "auto",
122
- env_inheritance: input.envInheritance ?? "mirror_native",
123
- ...(input.env ? { env: input.env } : {}),
124
- });
125
- writeText(artifact.promptPath, redactSecrets(`Persistent local replay evidence:
144
+ const reviewerPatch = input.evidenceReadOnly
145
+ ? readExistingDiffEvidence(reviewerWorkspace.evidenceDir, input.diff)
146
+ : writeDiffEvidence(reviewerWorkspace.evidenceDir, input.diff);
147
+ updateReviewerMetadata(artifact, {
148
+ candidate_evidence_dir: reviewerWorkspace.evidenceDir,
149
+ candidate_root: reviewerWorkspace.root,
150
+ source_candidate_evidence_dir: input.evidenceDir,
151
+ source_candidate_root: input.cwd,
152
+ reviewer_workspace_root: reviewerWorkspace.root,
153
+ persistent_evidence_dir: persistentEvidenceDir,
154
+ persistent_diff_path: persistentPatch.diffPath,
155
+ persistent_summary_path: persistentPatch.summaryPath,
156
+ diff_sha256: persistentPatch.diffSha256,
157
+ review_subject: input.reviewSubject ?? "code",
158
+ ...frozenMetadata,
159
+ });
160
+ const runtimePrompt = buildReviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch, input.evidenceReadOnly === true, input.reviewSubject ?? "code");
161
+ spec = HarnessRunSpec.parse({
162
+ session_id: newId("rev"),
163
+ intent: "review",
164
+ prompt: runtimePrompt,
165
+ cwd: reviewerWorkspace.root,
166
+ access: "readonly",
167
+ model_hint: reviewer.requestedModel ?? null,
168
+ effort_hint: reviewer.requestedEffort ?? null,
169
+ auth_preference: reviewer.authPreference ?? "auto",
170
+ env_inheritance: input.envInheritance ?? "mirror_native",
171
+ ...(input.env ? { env: input.env } : {}),
172
+ });
173
+ writeText(artifact.promptPath, redactSecrets(`Persistent local replay evidence:
126
174
  - evidence_dir: ${persistentEvidenceDir}
127
175
  - candidate_root: ${reviewerWorkspace.root}
128
176
  - source_candidate_root: ${input.cwd}
@@ -134,172 +182,130 @@ Runtime prompt used during review follows. Its candidate-tree paths may be trans
134
182
 
135
183
  ${runtimePrompt}
136
184
  `));
137
- }
138
- catch (err) {
139
- const failedAt = nowIso();
140
- const message = redactSecrets(err instanceof Error ? err.message : String(err));
141
- updateReviewerMetadata(artifact, {
142
- status: "failed",
143
- failure_time: failedAt,
144
- error: `reviewer setup failed: ${message}`,
145
- });
146
- writeParseError(artifact, { error: `reviewer setup failed: ${message}` });
147
- emitReviewerProgress(artifact, reviewer, input.onReviewerEvent, {
148
- type: "reviewer.failed",
149
- at: failedAt,
150
- duration_ms: 0,
151
- message: `Reviewer setup failed: ${message}`,
152
- });
153
- if (reviewerWorkspace)
154
- await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
155
- const proof = buildRouteProof({
156
- harness_id: reviewer.adapter.id,
157
- provider_family: reviewer.providerFamily,
158
- model_hint: reviewer.requestedModel ?? null,
159
- }, {
160
- provider: reviewer.providerFamily,
161
- model_id: null,
162
- evidence_source: "unavailable",
163
- }, reviewerFamilies);
164
- routeProofs.push(proof);
165
- findingsByReviewer[index]?.push(insufficientEvidenceFinding({
166
- harness_id: reviewer.adapter.id,
167
- requested_model: reviewer.requestedModel ?? null,
168
- requested_effort: reviewer.requestedEffort ?? null,
169
- observed_model: null,
170
- route_proof_status: proof.status,
171
- }, `Reviewer setup failed: ${message}`));
172
- continue;
173
- }
174
- if (!reviewerWorkspace || !spec)
175
- continue;
176
- let text = "";
177
- // Stream-observed model: ONLY a model the native CLI actually emitted in its
178
- // stream (stream_event/transcript/model_catalog). This is the honest
179
- // `observed_model` for findings — an accepted argv echo is NOT an observation.
180
- let streamObservedModel;
181
- // Route-proof model: stream-observed when present, else the accepted argv arg
182
- // (metadata tier). Drives RouteProof.observed.model_id + status.
183
- let routeModel;
184
- let routeSource = "unavailable";
185
- let reviewerError = null;
186
- try {
187
- const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.transientRetryPolicy ?? DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY, artifact, input.onReviewerEvent, input.signal);
188
- text = out.text;
189
- streamObservedModel = out.observedModel;
190
- routeModel = out.observedModel;
191
- routeSource = out.observedSource;
192
- reviewSpendUsd += out.costUsd;
193
- if (out.costEstimated)
194
- reviewSpendEstimated = true;
195
- // accepted_model_arg semantics: when WE passed an explicit model argument
196
- // and the native CLI completed without rejecting it, the accepted argv is
197
- // metadata-level route evidence (weaker than stream-observed, stronger
198
- // than nothing). Some CLIs (codex exec --json) never echo the model. This
199
- // populates ONLY the route proof — never streamObservedModel, so the
200
- // finding's observed_model stays null (an argv echo is not an observation).
201
- if (!routeModel && reviewer.requestedModel) {
202
- routeModel = reviewer.requestedModel;
203
- routeSource = "metadata";
204
- }
205
- }
206
- catch (err) {
207
- reviewerError = redactSecrets(err instanceof Error ? err.message : String(err));
208
- // Budget truth: a reviewer that streamed paid tokens then timed out/failed
209
- // still spent money. Fold the partial cost into the ledger (the success
210
- // path adds out.costUsd above; these paths are mutually exclusive).
211
- const partial = err;
212
- if (typeof partial?.partialText === "string" && partial.partialText.trim() !== "") {
213
- text = partial.partialText;
214
- }
215
- if (partial && typeof partial.partialCostUsd === "number" && partial.partialCostUsd > 0) {
216
- reviewSpendUsd += partial.partialCostUsd;
217
- if (partial.partialCostEstimated)
218
- reviewSpendEstimated = true;
219
- }
220
- if (partial?.partialObservedModel) {
221
- streamObservedModel = partial.partialObservedModel;
222
- routeModel = partial.partialObservedModel;
223
- routeSource = partial.partialObservedSource ?? "stream_event";
224
- }
225
- writeParseError(artifact, { error: reviewerError });
226
- }
227
- finally {
185
+ }
186
+ catch (err) {
187
+ const failedAt = nowIso();
188
+ const message = redactSecrets(err instanceof Error ? err.message : String(err));
189
+ updateReviewerMetadata(artifact, {
190
+ status: "failed",
191
+ failure_time: failedAt,
192
+ error: `reviewer setup failed: ${message}`,
193
+ });
194
+ writeParseError(artifact, { error: `reviewer setup failed: ${message}` });
195
+ emitReviewerProgress(artifact, reviewer, input.onReviewerEvent, {
196
+ type: "reviewer.failed",
197
+ at: failedAt,
198
+ duration_ms: 0,
199
+ message: `Reviewer setup failed: ${message}`,
200
+ });
201
+ if (reviewerWorkspace)
228
202
  await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
203
+ const proof = reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies);
204
+ routeProofs[index] = proof;
205
+ findingsByReviewer[index]?.push(insufficientEvidenceFinding(reviewerInfo(reviewer, proof.status), `Reviewer setup failed: ${message}`));
206
+ return;
207
+ }
208
+ if (!reviewerWorkspace || !spec)
209
+ return;
210
+ let text = "";
211
+ let streamObservedModel;
212
+ let routeModel;
213
+ let routeSource = "unavailable";
214
+ let reviewerError = null;
215
+ try {
216
+ const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.transientRetryPolicy ?? DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY, artifact, input.onReviewerEvent, input.signal);
217
+ text = out.text;
218
+ streamObservedModel = out.observedModel;
219
+ routeModel = out.observedModel;
220
+ routeSource = out.observedSource;
221
+ reviewSpendByReviewer[index] = out.costUsd;
222
+ reviewSpendEstimatedByReviewer[index] = out.costEstimated;
223
+ reviewCashByReviewer[index] = out.cashUsd;
224
+ reviewValuationByReviewer[index] = out.valuationUsd;
225
+ reviewUnknownByReviewer[index] = out.unknownUsd;
226
+ if (!routeModel && reviewer.requestedModel) {
227
+ routeModel = reviewer.requestedModel;
228
+ routeSource = "metadata";
229
229
  }
230
- const proof = buildRouteProof({
231
- harness_id: reviewer.adapter.id,
232
- provider_family: reviewer.providerFamily,
233
- model_hint: reviewer.requestedModel ?? null,
234
- }, {
235
- provider: reviewer.providerFamily,
236
- model_id: routeModel ?? null,
237
- evidence_source: routeModel ? routeSource : "unavailable",
238
- },
239
- // The other reviewers' families this route is meant to be diverse against
240
- // (mirrors the implementer route proof; reviewer diversity is otherwise
241
- // enforced via classifyDiversity's same_model_fallback status below).
242
- reviewerFamilies.filter((_, i) => i !== index));
243
- routeProofs.push(proof);
244
- const info = {
245
- harness_id: reviewer.adapter.id,
246
- requested_model: reviewer.requestedModel ?? null,
247
- requested_effort: reviewer.requestedEffort ?? null,
248
- // Honest observation only: a finding's observed_model is the STREAM-observed
249
- // model or null. An accepted argv arg lives in the route proof's model_id,
250
- // not here — it must not masquerade as an observed model.
251
- observed_model: streamObservedModel ?? null,
252
- route_proof_status: proof.status,
253
- };
254
- const jsonBlocks = extractJsonBlocks(text);
255
- writeJson(artifact.parsedPath, redactValue(jsonBlocks));
256
- if (reviewerError && (text.trim() === "" || jsonBlocks.length === 0)) {
257
- findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer failed: ${reviewerError}`));
258
- continue;
259
- }
260
- if (text.trim() === "" || jsonBlocks.length === 0) {
261
- writeParseError(artifact, { error: "no_parseable_json", text_sha256: sha256(text) });
262
- findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, "Reviewer produced no parseable JSON findings."));
263
- continue;
264
- }
265
- const parsed = parseFindingsDetailed(text, info);
266
- const parseError = {};
267
- let parsedFindingsRecorded = false;
268
- const recordParsedFindings = () => {
269
- if (parsedFindingsRecorded)
270
- return;
271
- findingsByReviewer[index]?.push(...parsed.findings);
272
- parsedFindingsRecorded = true;
273
- };
274
- if (parsed.malformed > 0) {
275
- Object.assign(parseError, {
276
- error: "malformed_findings",
277
- malformed: parsed.malformed,
278
- text_sha256: sha256(text),
279
- });
280
- recordParsedFindings();
281
- findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer produced ${parsed.malformed} malformed finding item(s).`));
230
+ }
231
+ catch (err) {
232
+ reviewerError = redactSecrets(err instanceof Error ? err.message : String(err));
233
+ const partial = err;
234
+ if (typeof partial?.partialText === "string" && partial.partialText.trim() !== "") {
235
+ text = partial.partialText;
282
236
  }
283
- if (reviewerError) {
284
- Object.assign(parseError, {
285
- error: reviewerError,
286
- recovered_json_blocks: jsonBlocks.length,
287
- text_sha256: sha256(text),
288
- });
289
- recordParsedFindings();
290
- findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, parsed.findings.length === 0
291
- ? `Reviewer failed after parseable JSON with no findings: ${reviewerError}`
292
- : `Reviewer failed after parseable JSON output: ${reviewerError}`));
237
+ if (partial && typeof partial.partialCostUsd === "number" && partial.partialCostUsd > 0) {
238
+ reviewSpendByReviewer[index] = partial.partialCostUsd;
239
+ reviewSpendEstimatedByReviewer[index] = partial.partialCostEstimated === true;
240
+ reviewCashByReviewer[index] = partial.partialCashUsd ?? 0;
241
+ reviewValuationByReviewer[index] = partial.partialValuationUsd ?? 0;
242
+ reviewUnknownByReviewer[index] = partial.partialUnknownUsd ?? 0;
293
243
  }
294
- if (Object.keys(parseError).length > 0) {
295
- writeParseError(artifact, parseError);
296
- continue;
244
+ if (partial?.partialObservedModel) {
245
+ streamObservedModel = partial.partialObservedModel;
246
+ routeModel = partial.partialObservedModel;
247
+ routeSource = partial.partialObservedSource ?? "stream_event";
297
248
  }
298
- if (reviewer.providerFamily !== "unknown")
299
- healthyFamilies.add(reviewer.providerFamily);
300
- healthyReviewerIndexes.add(index);
249
+ writeParseError(artifact, { error: reviewerError });
250
+ }
251
+ finally {
252
+ await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
253
+ }
254
+ const proof = reviewerRouteProof(reviewer, routeModel ?? null, routeSource, reviewerFamilies.filter((_, i) => i !== index));
255
+ routeProofs[index] = proof;
256
+ const info = reviewerInfo(reviewer, proof.status, streamObservedModel ?? null);
257
+ const jsonBlocks = extractJsonBlocks(text);
258
+ writeJson(artifact.parsedPath, redactValue(jsonBlocks));
259
+ if (reviewerError && (text.trim() === "" || jsonBlocks.length === 0)) {
260
+ findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer failed: ${reviewerError}`));
261
+ return;
262
+ }
263
+ if (text.trim() === "" || jsonBlocks.length === 0) {
264
+ writeParseError(artifact, { error: "no_parseable_json", text_sha256: sha256(text) });
265
+ findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, "Reviewer produced no parseable JSON findings."));
266
+ return;
267
+ }
268
+ const parsed = parseFindingsDetailed(text, info);
269
+ const parseError = {};
270
+ let parsedFindingsRecorded = false;
271
+ const recordParsedFindings = () => {
272
+ if (parsedFindingsRecorded)
273
+ return;
301
274
  findingsByReviewer[index]?.push(...parsed.findings);
275
+ parsedFindingsRecorded = true;
276
+ };
277
+ if (parsed.malformed > 0) {
278
+ Object.assign(parseError, {
279
+ error: "malformed_findings",
280
+ malformed: parsed.malformed,
281
+ text_sha256: sha256(text),
282
+ });
283
+ recordParsedFindings();
284
+ findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer produced ${parsed.malformed} malformed finding item(s).`));
285
+ }
286
+ if (reviewerError) {
287
+ Object.assign(parseError, {
288
+ error: reviewerError,
289
+ recovered_json_blocks: jsonBlocks.length,
290
+ text_sha256: sha256(text),
291
+ });
292
+ recordParsedFindings();
293
+ findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, parsed.findings.length === 0
294
+ ? `Reviewer failed after parseable JSON with no findings: ${reviewerError}`
295
+ : `Reviewer failed after parseable JSON output: ${reviewerError}`));
302
296
  }
297
+ if (Object.keys(parseError).length > 0) {
298
+ writeParseError(artifact, parseError);
299
+ return;
300
+ }
301
+ healthyReviewerIndexes.add(index);
302
+ findingsByReviewer[index]?.push(...parsed.findings);
303
+ };
304
+ try {
305
+ const reviewerRuns = await Promise.allSettled(input.reviewers.map((reviewer, index) => runReviewer(reviewer, index)));
306
+ const failedRun = reviewerRuns.find((run) => run.status === "rejected");
307
+ if (failedRun)
308
+ throw failedRun.reason;
303
309
  const classifiedProofs = classifyDiversity(routeProofs);
304
310
  for (const [index, proof] of classifiedProofs.entries()) {
305
311
  const artifact = artifacts[index];
@@ -321,12 +327,12 @@ ${runtimePrompt}
321
327
  });
322
328
  });
323
329
  });
324
- const healthyProviders = [...healthyFamilies];
325
- // Two-tier route proof: crossFamilyVerified — the strong tier that unblocks
326
- // apply requires the model to have been OBSERVED in the reviewer stream
327
- // (status "verified"). An argv/metadata echo ("accepted_model_arg") is a weaker
328
- // tier: it proves we PASSED a model arg, not that the CLI ran it, so it must
329
- // NOT unblock apply on unobserved proof. same_model_fallback never counts.
330
+ const healthyProviders = [
331
+ ...new Set(input.reviewers
332
+ .filter((_, index) => healthyReviewerIndexes.has(index))
333
+ .map((reviewer) => reviewer.providerFamily)
334
+ .filter((family) => family !== "unknown")),
335
+ ];
330
336
  const observedFamilies = [
331
337
  ...new Set(classifiedProofs
332
338
  .filter((p, index) => p.status === "verified" && healthyReviewerIndexes.has(index))
@@ -341,8 +347,7 @@ ${runtimePrompt}
341
347
  healthyProviders,
342
348
  crossFamilyVerified: observedFamilies.length >= 2,
343
349
  distinctProviders: observedFamilies,
344
- reviewSpendUsd,
345
- reviewSpendEstimated,
350
+ ...summarizeReviewerSpend(reviewSpendByReviewer, reviewCashByReviewer, reviewValuationByReviewer, reviewUnknownByReviewer, reviewSpendEstimatedByReviewer),
346
351
  };
347
352
  }
348
353
  finally {
@@ -376,11 +381,13 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
376
381
  let firstEventTime = null;
377
382
  let observedModel;
378
383
  let observedSource = "unavailable";
379
- // Reviewer spend tracked at function scope so a timed-out/failed reviewer still
380
- // contributes its PARTIAL cost to the ledger (budget truth). It is attached to
381
- // the thrown error so the caller can fold it in.
384
+ let observedAuthMode = null;
385
+ let currentAuthMode = null;
382
386
  let costUsd = 0;
383
387
  let costEstimated = false;
388
+ let cashUsd = 0;
389
+ let valuationUsd = 0;
390
+ let unknownUsd = 0;
384
391
  let partialText = "";
385
392
  const isCancelled = () => cancelledBySignal || signal?.aborted === true || controller.signal.aborted;
386
393
  const consumeOnce = async (nativeTry) => {
@@ -403,6 +410,10 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
403
410
  }
404
411
  if (ev.type === "message" && ev.payload?.["auth_switched"] === true) {
405
412
  const authSwitch = reviewerAuthSwitchFromEvent(ev);
413
+ if (authSwitch.to_auth_mode === "subscription")
414
+ currentAuthMode = "local_session";
415
+ if (authSwitch.to_auth_mode === "api_key")
416
+ currentAuthMode = "api_key";
406
417
  updateReviewerMetadata(artifact, { auth_switch: authSwitch });
407
418
  emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
408
419
  type: "reviewer.auth_switched",
@@ -410,6 +421,14 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
410
421
  ...authSwitch,
411
422
  });
412
423
  }
424
+ const disclosedAuthMode = reviewerAuthMode(ev.credential_route);
425
+ if (disclosedAuthMode)
426
+ currentAuthMode = disclosedAuthMode;
427
+ if (!observedAuthMode) {
428
+ observedAuthMode = disclosedAuthMode;
429
+ if (observedAuthMode)
430
+ updateReviewerMetadata(artifact, { auth_mode: observedAuthMode });
431
+ }
413
432
  if (!firstEventTime) {
414
433
  firstEventTime = eventTime;
415
434
  updateReviewerMetadata(artifact, { first_event_time: firstEventTime });
@@ -422,7 +441,19 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
422
441
  costUsd += ev.usage.cost_usd;
423
442
  if (ev.usage.estimated)
424
443
  costEstimated = true;
425
- updateReviewerMetadata(artifact, { cost_usd: costUsd, cost_estimated: costEstimated });
444
+ if (currentAuthMode === "local_session")
445
+ valuationUsd += ev.usage.cost_usd;
446
+ else if (currentAuthMode === "api_key")
447
+ cashUsd += ev.usage.cost_usd;
448
+ else
449
+ unknownUsd += ev.usage.cost_usd;
450
+ updateReviewerMetadata(artifact, {
451
+ cost_usd: costUsd,
452
+ cost_estimated: costEstimated,
453
+ cash_usd: cashUsd,
454
+ valuation_usd: valuationUsd,
455
+ unknown_usd: unknownUsd,
456
+ });
426
457
  }
427
458
  if (ev.type === "message" && ev.text && ev.payload?.["auth_switched"] !== true) {
428
459
  const safeText = redactSecrets(ev.text);
@@ -506,13 +537,15 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
506
537
  text,
507
538
  observedModel: attemptObservedModel,
508
539
  observedSource: attemptObservedSource,
509
- artifactDir: artifact.dir,
510
540
  costUsd,
511
541
  costEstimated,
542
+ cashUsd,
543
+ valuationUsd,
544
+ unknownUsd,
512
545
  };
513
546
  };
514
547
  const consume = consumeOnce(0);
515
- const removeExternalAbortListeners = [];
548
+ let removeExternalAbortListener = () => { };
516
549
  const cancelled = new Promise((_, reject) => {
517
550
  if (!signal)
518
551
  return;
@@ -525,6 +558,9 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
525
558
  reject(Object.assign(new Error("Reviewer cancelled"), {
526
559
  partialCostUsd: costUsd,
527
560
  partialCostEstimated: costEstimated,
561
+ partialCashUsd: cashUsd,
562
+ partialValuationUsd: valuationUsd,
563
+ partialUnknownUsd: unknownUsd,
528
564
  partialObservedModel: observedModel,
529
565
  partialObservedSource: observedSource,
530
566
  partialText,
@@ -534,7 +570,7 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
534
570
  queueMicrotask(onAbort);
535
571
  else
536
572
  signal.addEventListener("abort", onAbort, { once: true });
537
- removeExternalAbortListeners.push(() => signal.removeEventListener("abort", onAbort));
573
+ removeExternalAbortListener = () => signal.removeEventListener("abort", onAbort);
538
574
  });
539
575
  const timed = new Promise((_, reject) => {
540
576
  timeout = setTimeout(() => {
@@ -565,6 +601,9 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
565
601
  reject(Object.assign(new Error(`Reviewer timed out after ${timeoutMs}ms`), {
566
602
  partialCostUsd: costUsd,
567
603
  partialCostEstimated: costEstimated,
604
+ partialCashUsd: cashUsd,
605
+ partialValuationUsd: valuationUsd,
606
+ partialUnknownUsd: unknownUsd,
568
607
  partialObservedModel: observedModel,
569
608
  partialObservedSource: observedSource,
570
609
  partialText,
@@ -599,6 +638,9 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
599
638
  Object.assign(err, {
600
639
  partialCostUsd: costUsd,
601
640
  partialCostEstimated: costEstimated,
641
+ partialCashUsd: cashUsd,
642
+ partialValuationUsd: valuationUsd,
643
+ partialUnknownUsd: unknownUsd,
602
644
  partialObservedModel: observedModel,
603
645
  partialObservedSource: observedSource,
604
646
  partialText,
@@ -610,22 +652,12 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
610
652
  settled = true;
611
653
  if (timeout)
612
654
  clearTimeout(timeout);
613
- for (const removeExternalAbortListener of removeExternalAbortListeners) {
614
- removeExternalAbortListener();
615
- }
655
+ removeExternalAbortListener();
616
656
  consume.catch(() => {
617
657
  /* timeout path: consume may reject after the race already returned */
618
658
  });
619
659
  }
620
660
  }
621
- function reviewerAuthSwitchFromEvent(ev) {
622
- const reason = FallbackReason.safeParse(ev.payload?.["reason"]);
623
- return {
624
- from_auth_mode: typeof ev.payload?.["from_auth_mode"] === "string" ? ev.payload["from_auth_mode"] : "unknown",
625
- to_auth_mode: typeof ev.payload?.["to_auth_mode"] === "string" ? ev.payload["to_auth_mode"] : "unknown",
626
- reason: reason.success ? reason.data : "auth_unavailable",
627
- };
628
- }
629
661
  function selectReviewerWorkspaceBaseDir(sourceRoot, artifactsBaseDir, sourceEvidenceDir) {
630
662
  const durableBase = join(artifactsBaseDir, "workspaces");
631
663
  if (!isSameOrInside(sourceRoot, durableBase) && !isSameOrInside(sourceEvidenceDir, durableBase)) {
@@ -665,11 +697,13 @@ async function prepareReviewerWorkspace(input) {
665
697
  const resolvedSourceEvidenceDir = realpathSync(sourceEvidenceDir);
666
698
  const evidenceExcludeRoots = excludeRoots.filter((root) => !isSameOrInside(root, sourceEvidenceDir));
667
699
  await rm(evidenceDir, { recursive: true, force: true });
668
- await cp(sourceEvidenceDir, evidenceDir, {
669
- recursive: true,
670
- dereference: false,
671
- filter: (sourcePath) => shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, evidenceExcludeRoots),
672
- });
700
+ await cp(sourceEvidenceDir, evidenceDir, input.preserveEvidenceBytes
701
+ ? { recursive: true, dereference: false }
702
+ : {
703
+ recursive: true,
704
+ dereference: false,
705
+ filter: (sourcePath) => shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, evidenceExcludeRoots),
706
+ });
673
707
  }
674
708
  await mkdir(evidenceDir, { recursive: true, mode: 0o700 });
675
709
  await initializeReviewerWorkspaceGit(root);
@@ -680,14 +714,20 @@ async function prepareReviewerWorkspace(input) {
680
714
  throw err;
681
715
  }
682
716
  }
683
- async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir) {
717
+ async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir, preserveBytes = false) {
684
718
  const source = resolve(sourceEvidenceDir);
685
719
  const target = resolve(persistentEvidenceDir);
686
720
  await rm(target, { recursive: true, force: true });
687
- await mkdir(target, { recursive: true, mode: 0o700 });
688
721
  if (!existsSync(source)) {
722
+ await mkdir(target, { recursive: true, mode: 0o700 });
689
723
  return;
690
724
  }
725
+ if (preserveBytes) {
726
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
727
+ await cp(source, target, { recursive: true, dereference: false });
728
+ return;
729
+ }
730
+ await mkdir(target, { recursive: true, mode: 0o700 });
691
731
  const resolvedSource = realpathSync(source);
692
732
  for (const entry of await readdir(source, { withFileTypes: true })) {
693
733
  const sourcePath = join(source, entry.name);
@@ -725,13 +765,7 @@ async function copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidence
725
765
  await cp(sourcePath, targetPath, { recursive: false, dereference: false });
726
766
  }
727
767
  function shouldTextSanitizeEvidenceFile(path) {
728
- const lower = path.toLowerCase();
729
- return (lower.endsWith(".md") ||
730
- lower.endsWith(".txt") ||
731
- lower.endsWith(".json") ||
732
- lower.endsWith(".yaml") ||
733
- lower.endsWith(".yml") ||
734
- lower.endsWith(".patch"));
768
+ return TEXT_EVIDENCE_SUFFIXES.some((extension) => path.toLowerCase().endsWith(extension));
735
769
  }
736
770
  function shouldFailClosedEvidenceFile(path) {
737
771
  return path.toLowerCase().endsWith(".patch");
@@ -742,9 +776,7 @@ function shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceD
742
776
  return false;
743
777
  if (isSameOrInside(targetEvidenceDir, resolvedSourcePath))
744
778
  return false;
745
- return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [
746
- targetEvidenceDir,
747
- ]);
779
+ return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [targetEvidenceDir], new Set(), false);
748
780
  }
749
781
  async function cleanupReviewerWorkspace(workspace, artifact) {
750
782
  try {
@@ -777,7 +809,7 @@ async function cleanupTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir, artifa
777
809
  }
778
810
  }
779
811
  }
780
- function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set()) {
812
+ function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set(), enforceContentPolicy = true) {
781
813
  const resolvedSourcePath = resolve(sourcePath);
782
814
  if (!isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, resolvedSourcePath, excludeRoots)) {
783
815
  return false;
@@ -788,55 +820,35 @@ function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excl
788
820
  if (!rel)
789
821
  return true;
790
822
  const parts = rel.split(/[\\/]+/);
791
- if (parts.some(isReviewerSecretLikePathPart)) {
823
+ if (sensitiveResourcePolicy.classifyPath(rel).sensitive) {
792
824
  return false;
793
825
  }
794
826
  if (parts[0] === ".claudexor") {
795
- return isCopyableReviewerClaudexorPath(rel, parts, preservePaths);
827
+ return (isCopyableReviewerClaudexorPath(rel, parts, preservePaths) &&
828
+ (!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
796
829
  }
797
- if (parts.some((part) => [
798
- ".git",
799
- ".adversarial-review",
800
- ".turbo",
801
- "node_modules",
802
- ].includes(part))) {
830
+ if (parts.some((part) => [".git", ".adversarial-review", ".turbo", "node_modules"].includes(part))) {
803
831
  return false;
804
832
  }
805
833
  if (parts.some((part) => [".next", ".cache", "coverage", "dist"].includes(part)) &&
806
834
  !isPreservedReviewerPath(rel, preservePaths)) {
807
835
  return false;
808
836
  }
809
- return !rel.endsWith(".tsbuildinfo");
837
+ return (!rel.endsWith(".tsbuildinfo") &&
838
+ (!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
810
839
  }
811
- function isReviewerSecretLikePathPart(part) {
812
- const lower = part.toLowerCase();
813
- if (lower.startsWith(".env") && !isSafeEnvTemplateName(lower))
814
- return true;
815
- if ([
816
- ".npmrc",
817
- ".netrc",
818
- ".pypirc",
819
- ".git-credentials",
820
- ".ssh",
821
- ".aws",
822
- ".azure",
823
- ".gcloud",
824
- ".cursor",
825
- ".codex",
826
- ".claude",
827
- ".anthropic",
828
- ".openai",
829
- ].includes(lower)) {
830
- return true;
840
+ function reviewerFileContentAllowed(path) {
841
+ let targetStat;
842
+ try {
843
+ targetStat = statSync(path);
831
844
  }
832
- if (/^id_(rsa|dsa|ecdsa|ed25519)$/.test(lower))
833
- return true;
834
- if (lower.endsWith(".pem") || lower.endsWith(".p12") || lower.endsWith(".pfx"))
845
+ catch {
846
+ return false;
847
+ }
848
+ if (!targetStat.isFile())
835
849
  return true;
836
- return false;
837
- }
838
- function isSafeEnvTemplateName(lower) {
839
- return [".env.example", ".env.sample", ".env.template"].includes(lower);
850
+ const content = readTextSafe(path);
851
+ return content !== null && !sensitiveResourcePolicy.containsSensitiveContent(content);
840
852
  }
841
853
  function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
842
854
  if (parts.length === 1) {
@@ -846,22 +858,8 @@ function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
846
858
  return true;
847
859
  }
848
860
  const runtimeRoot = parts[1]?.toLowerCase();
849
- if (runtimeRoot &&
850
- [
851
- "auth",
852
- "cache",
853
- "daemon",
854
- "home",
855
- "homes",
856
- "logs",
857
- "runs",
858
- "secrets",
859
- "state",
860
- "tmp",
861
- "workspaces",
862
- ].includes(runtimeRoot)) {
861
+ if (runtimeRoot && BLOCKED_REVIEWER_RUNTIME_ROOTS.has(runtimeRoot))
863
862
  return false;
864
- }
865
863
  return isPreservedReviewerPath(rel, preservePaths);
866
864
  }
867
865
  function isPreservedReviewerPath(rel, preservePaths) {
@@ -877,15 +875,10 @@ function isPreservedReviewerPath(rel, preservePaths) {
877
875
  }
878
876
  return false;
879
877
  }
880
- /** Test-only alias for the preserve-set extractor. */
881
878
  export function __testExtractDiffTouchedPaths(diff) {
882
879
  return extractDiffTouchedPaths(diff);
883
880
  }
884
881
  function extractDiffTouchedPaths(diff) {
885
- // One structural parser owns diff headers (INV-050): parseUnifiedDiff is
886
- // quote-aware and decodes git's C-quoted paths (incl. octal escapes for
887
- // non-ASCII), which the old private tokenizer mis-decoded — a mis-decoded
888
- // touched path silently dropped the file from the reviewer preserve set.
889
882
  const paths = new Set();
890
883
  for (const file of parseUnifiedDiff(diff).files) {
891
884
  if (file.oldPath)
@@ -921,57 +914,40 @@ function isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, sourcePath, e
921
914
  return true;
922
915
  let linkTarget = "";
923
916
  let resolvedTarget = "";
917
+ let targetKind = "other";
924
918
  try {
925
919
  linkTarget = readlinkSync(sourcePath);
926
920
  resolvedTarget = realpathSync(sourcePath);
921
+ const targetStat = statSync(sourcePath);
922
+ targetKind = targetStat.isDirectory() ? "directory" : targetStat.isFile() ? "file" : "other";
927
923
  }
928
924
  catch {
929
925
  return false;
930
926
  }
931
- if (isAbsolute(linkTarget))
932
- return false;
933
- if (!isSameOrInside(resolvedSourceRoot, resolvedTarget))
934
- return false;
935
- if (excludeRoots.some((root) => isSameOrInside(root, resolvedTarget)))
936
- return false;
937
- const sourceParentRel = relative(sourceRoot, dirname(sourcePath));
938
- if (sourceParentRel.split(/[\\/]+/)[0] === ".." || isAbsolute(sourceParentRel))
939
- return false;
940
- const relocatedTargetRel = normalize(join(sourceParentRel, linkTarget));
941
- const relocatedFirstPart = relocatedTargetRel.split(/[\\/]+/)[0];
942
- if (relocatedFirstPart === ".." || isAbsolute(relocatedTargetRel))
943
- return false;
944
- const relocatedTargetPath = resolve(sourceRoot, relocatedTargetRel);
945
- if (!isSameOrInside(sourceRoot, relocatedTargetPath))
946
- return false;
947
- return !excludeRoots.some((root) => isSameOrInside(root, relocatedTargetPath));
927
+ return sensitiveResourcePolicy.assessSymlink({
928
+ sourceRoot,
929
+ canonicalSourceRoot: resolvedSourceRoot,
930
+ sourcePath,
931
+ linkTarget,
932
+ resolvedTargetPath: resolvedTarget,
933
+ targetKind,
934
+ allowedTargetKinds: ["file", "directory"],
935
+ excludedRoots: excludeRoots,
936
+ relocationRoot: sourceRoot,
937
+ }).allowed;
948
938
  }
949
939
  async function initializeReviewerWorkspaceGit(root) {
950
- await runGitOrThrow("init", root, [
951
- "-c",
952
- "init.templateDir=",
953
- "-c",
954
- "core.hooksPath=/dev/null",
955
- "init",
956
- ]);
957
- await runGitOrThrow("config user.email", root, [
958
- "-c",
959
- "core.hooksPath=/dev/null",
960
- "config",
961
- "user.email",
962
- "claudexor-review@example.invalid",
963
- ]);
964
- await runGitOrThrow("config user.name", root, [
965
- "-c",
966
- "core.hooksPath=/dev/null",
967
- "config",
968
- "user.name",
969
- "Claudexor Review",
970
- ]);
971
- await runGitOrThrow("add", root, ["-c", "core.hooksPath=/dev/null", "add", "-A", "--force"]);
940
+ const noHooks = ["-c", "core.hooksPath=/dev/null"];
941
+ await runGitOrThrow("init", root, ["-c", "init.templateDir=", ...noHooks, "init"]);
942
+ for (const [key, value] of [
943
+ ["user.email", "claudexor-review@example.invalid"],
944
+ ["user.name", "Claudexor Review"],
945
+ ]) {
946
+ await runGitOrThrow(`config ${key}`, root, [...noHooks, "config", key, value]);
947
+ }
948
+ await runGitOrThrow("add", root, [...noHooks, "add", "-A", "--force"]);
972
949
  await runGitOrThrow("commit", root, [
973
- "-c",
974
- "core.hooksPath=/dev/null",
950
+ ...noHooks,
975
951
  "commit",
976
952
  "--allow-empty",
977
953
  "--no-verify",
@@ -1034,6 +1010,9 @@ function emitReviewerProgress(artifact, reviewer, onReviewerEvent, patch) {
1034
1010
  requested_model: reviewer.requestedModel ?? null,
1035
1011
  requested_effort: reviewer.requestedEffort ?? null,
1036
1012
  artifact_dir: artifact.dir,
1013
+ ...(typeof artifact.metadata["review_wave_id"] === "string"
1014
+ ? { review_wave_id: artifact.metadata["review_wave_id"] }
1015
+ : {}),
1037
1016
  ...patch,
1038
1017
  };
1039
1018
  const redacted = redactValue(event);