@claudexor/review 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1097 @@
1
+ import { parseUnifiedDiff, runCapture } from "@claudexor/core";
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";
5
+ import { cp, mkdir, readdir, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
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";
9
+ import { dedupeFindings, extractJsonBlocks, parseFindingsDetailed, } from "./findings.js";
10
+ import { buildRouteProof, classifyDiversity } from "./route.js";
11
+ const DEFAULT_REVIEWER_TIMEOUT_MS = 10 * 60_000;
12
+ const DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY = {
13
+ maxRetries: 2,
14
+ initialDelayMs: 1_000,
15
+ maxDelayMs: 10_000,
16
+ };
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");
33
+ }
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
+ export async function reviewCandidate(input) {
40
+ const findingsByReviewer = input.reviewers.map(() => []);
41
+ const routeProofs = [];
42
+ const reviewerRequests = [];
43
+ const healthyFamilies = new Set();
44
+ const healthyReviewerIndexes = new Set();
45
+ let reviewSpendUsd = 0;
46
+ let reviewSpendEstimated = false;
47
+ const reviewerTimeoutMs = input.reviewerTimeoutMs ?? DEFAULT_REVIEWER_TIMEOUT_MS;
48
+ if (containsSecretLikeToken(input.diff || "(empty diff)\n")) {
49
+ throw new Error("diff evidence contains a secret-like token; refusing to persist raw DIFF.patch");
50
+ }
51
+ writeDiffEvidence(input.evidenceDir, input.diff);
52
+ const preflight = preflightEvidence(input.evidenceDir);
53
+ if (!preflight.ok) {
54
+ const parts = [
55
+ preflight.missing.length > 0 ? `missing: ${preflight.missing.join(", ")}` : "",
56
+ preflight.empty.length > 0 ? `empty: ${preflight.empty.join(", ")}` : "",
57
+ ].filter(Boolean);
58
+ throw new Error(`mandatory evidence preflight failed (${parts.join("; ")})`);
59
+ }
60
+ const artifactsBaseDir = input.artifactsDir ?? join(input.evidenceDir, "reviewer-artifacts");
61
+ ensureDir(artifactsBaseDir);
62
+ const persistentEvidenceDir = join(artifactsBaseDir, "evidence");
63
+ await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir);
64
+ const persistentPatch = writeDiffEvidence(persistentEvidenceDir, input.diff);
65
+ writeJson(join(persistentEvidenceDir, "metadata.json"), {
66
+ source_evidence_dir: input.evidenceDir,
67
+ candidate_root: input.cwd,
68
+ persistent_evidence_dir: persistentEvidenceDir,
69
+ diff_path: persistentPatch.diffPath,
70
+ summary_path: persistentPatch.summaryPath,
71
+ diff_sha256: persistentPatch.diffSha256,
72
+ });
73
+ const artifacts = [];
74
+ const reviewerFamilies = input.reviewers.map((r) => r.providerFamily);
75
+ const preservePaths = extractDiffTouchedPaths(input.diff);
76
+ 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,
86
+ });
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:
126
+ - evidence_dir: ${persistentEvidenceDir}
127
+ - candidate_root: ${reviewerWorkspace.root}
128
+ - source_candidate_root: ${input.cwd}
129
+ - source_candidate_evidence_dir: ${input.evidenceDir}
130
+ - diff_path: ${persistentPatch.diffPath}
131
+ - diff_sha256: ${persistentPatch.diffSha256}
132
+
133
+ Runtime prompt used during review follows. Its candidate-tree paths may be transient after orchestrator cleanup; use the durable replay paths above for audit/replay.
134
+
135
+ ${runtimePrompt}
136
+ `));
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 {
228
+ await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
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).`));
282
+ }
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}`));
293
+ }
294
+ if (Object.keys(parseError).length > 0) {
295
+ writeParseError(artifact, parseError);
296
+ continue;
297
+ }
298
+ if (reviewer.providerFamily !== "unknown")
299
+ healthyFamilies.add(reviewer.providerFamily);
300
+ healthyReviewerIndexes.add(index);
301
+ findingsByReviewer[index]?.push(...parsed.findings);
302
+ }
303
+ const classifiedProofs = classifyDiversity(routeProofs);
304
+ for (const [index, proof] of classifiedProofs.entries()) {
305
+ const artifact = artifacts[index];
306
+ if (artifact) {
307
+ updateReviewerMetadata(artifact, {
308
+ route_proof_status: proof.status,
309
+ route_proof: proof,
310
+ });
311
+ }
312
+ }
313
+ const findings = findingsByReviewer.flatMap((items, index) => {
314
+ const status = classifiedProofs[index]?.status;
315
+ return items.map((f) => {
316
+ if (!status || f.reviewer.route_proof_status === status)
317
+ return f;
318
+ return ReviewFindingSchema.parse({
319
+ ...f,
320
+ reviewer: { ...f.reviewer, route_proof_status: status },
321
+ });
322
+ });
323
+ });
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 observedFamilies = [
331
+ ...new Set(classifiedProofs
332
+ .filter((p, index) => p.status === "verified" && healthyReviewerIndexes.has(index))
333
+ .map((p) => p.requested.provider_family)
334
+ .filter((f) => f !== "unknown")),
335
+ ];
336
+ return {
337
+ findings: dedupeFindings(findings),
338
+ routeProofs: classifiedProofs,
339
+ reviewerRequests,
340
+ crossFamilyHealthy: healthyProviders.length >= 2,
341
+ healthyProviders,
342
+ crossFamilyVerified: observedFamilies.length >= 2,
343
+ distinctProviders: observedFamilies,
344
+ reviewSpendUsd,
345
+ reviewSpendEstimated,
346
+ };
347
+ }
348
+ finally {
349
+ await cleanupTemporaryReviewerWorkspaceBaseDir(reviewerWorkspaceBaseDir, artifactsBaseDir);
350
+ }
351
+ }
352
+ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPolicy, artifact, onReviewerEvent, signal) {
353
+ const controller = new AbortController();
354
+ spec.extra["abortSignal"] = controller.signal;
355
+ const startMs = Date.now();
356
+ const startTime = nowIso();
357
+ updateReviewerMetadata(artifact, {
358
+ status: "started",
359
+ start_time: startTime,
360
+ requested_model: reviewer.requestedModel ?? null,
361
+ requested_effort: reviewer.requestedEffort ?? null,
362
+ provider_family: reviewer.providerFamily,
363
+ harness_id: reviewer.adapter.id,
364
+ prompt_path: artifact.promptPath,
365
+ });
366
+ emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
367
+ type: "reviewer.started",
368
+ at: startTime,
369
+ });
370
+ let runSpec = spec;
371
+ let currentIter = null;
372
+ let timeout = null;
373
+ let settled = false;
374
+ let timedOut = false;
375
+ let cancelledBySignal = false;
376
+ let firstEventTime = null;
377
+ let observedModel;
378
+ 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.
382
+ let costUsd = 0;
383
+ let costEstimated = false;
384
+ let partialText = "";
385
+ const isCancelled = () => cancelledBySignal || signal?.aborted === true || controller.signal.aborted;
386
+ const consumeOnce = async (nativeTry) => {
387
+ const iter = (reviewer.adapter.review ?? reviewer.adapter.run).call(reviewer.adapter, runSpec);
388
+ currentIter = iter;
389
+ let text = "";
390
+ let sawTransient = false;
391
+ let sawError = false;
392
+ let lastError = null;
393
+ let attemptObservedModel;
394
+ let attemptObservedSource = "unavailable";
395
+ for await (const ev of iter) {
396
+ const eventTime = nowIso();
397
+ appendLine(artifact.eventsPath, JSON.stringify(redactValue(ev)));
398
+ if (ev.transient)
399
+ sawTransient = true;
400
+ if (ev.type === "error") {
401
+ sawError = true;
402
+ lastError = redactSecrets(ev.error ?? ev.text ?? "reviewer emitted an error event");
403
+ }
404
+ if (ev.type === "message" && ev.payload?.["auth_switched"] === true) {
405
+ const authSwitch = reviewerAuthSwitchFromEvent(ev);
406
+ updateReviewerMetadata(artifact, { auth_switch: authSwitch });
407
+ emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
408
+ type: "reviewer.auth_switched",
409
+ at: eventTime,
410
+ ...authSwitch,
411
+ });
412
+ }
413
+ if (!firstEventTime) {
414
+ firstEventTime = eventTime;
415
+ updateReviewerMetadata(artifact, { first_event_time: firstEventTime });
416
+ emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
417
+ type: "reviewer.first_event",
418
+ at: firstEventTime,
419
+ });
420
+ }
421
+ if (ev.type === "usage" && ev.usage?.cost_usd) {
422
+ costUsd += ev.usage.cost_usd;
423
+ if (ev.usage.estimated)
424
+ costEstimated = true;
425
+ updateReviewerMetadata(artifact, { cost_usd: costUsd, cost_estimated: costEstimated });
426
+ }
427
+ if (ev.type === "message" && ev.text && ev.payload?.["auth_switched"] !== true) {
428
+ const safeText = redactSecrets(ev.text);
429
+ text += safeText + "\n";
430
+ partialText += safeText + "\n";
431
+ appendLine(artifact.transcriptPath, safeText);
432
+ }
433
+ if (ev.observed_model) {
434
+ observedModel = ev.observed_model;
435
+ const source = ev.payload?.["observed_model_source"];
436
+ observedSource =
437
+ source === "metadata" || source === "model_catalog" || source === "transcript"
438
+ ? source
439
+ : "stream_event";
440
+ attemptObservedModel = observedModel;
441
+ attemptObservedSource = observedSource;
442
+ updateReviewerMetadata(artifact, {
443
+ observed_model: observedModel,
444
+ observed_source: observedSource,
445
+ });
446
+ }
447
+ }
448
+ if (isCancelled()) {
449
+ throw new Error("Reviewer cancelled");
450
+ }
451
+ if (sawTransient &&
452
+ text.trim() === "" &&
453
+ nativeTry < transientRetryPolicy.maxRetries &&
454
+ !timedOut &&
455
+ !isCancelled()) {
456
+ const retryAt = nowIso();
457
+ const delayMs = transientRetryDelayMs(transientRetryPolicy, nativeTry);
458
+ updateReviewerMetadata(artifact, { transient_retry: nativeTry + 1 });
459
+ emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
460
+ type: "reviewer.failed",
461
+ at: retryAt,
462
+ duration_ms: Date.now() - startMs,
463
+ observed_model: attemptObservedModel ?? null,
464
+ observed_source: attemptObservedSource,
465
+ message: `Reviewer transient failure produced no output; retrying (${nativeTry + 1}/${transientRetryPolicy.maxRetries})`,
466
+ });
467
+ const remaining = Math.max(1, timeoutMs - (Date.now() - startMs));
468
+ await sleep(Math.min(delayMs, remaining));
469
+ if (timedOut) {
470
+ throw new Error(`Reviewer timed out after ${timeoutMs}ms`);
471
+ }
472
+ if (isCancelled()) {
473
+ throw new Error("Reviewer cancelled");
474
+ }
475
+ runSpec = HarnessRunSpec.parse({
476
+ ...runSpec,
477
+ session_id: newId("ses"),
478
+ extra: { ...runSpec.extra, abortSignal: controller.signal },
479
+ });
480
+ return consumeOnce(nativeTry + 1);
481
+ }
482
+ if (sawError && !timedOut) {
483
+ throw new Error(`Reviewer emitted error event: ${lastError ?? "unknown error"}`);
484
+ }
485
+ if (!timedOut && !isCancelled()) {
486
+ const completedTime = nowIso();
487
+ const durationMs = Date.now() - startMs;
488
+ updateReviewerMetadata(artifact, {
489
+ status: "completed",
490
+ completion_time: completedTime,
491
+ duration_ms: durationMs,
492
+ observed_model: attemptObservedModel ?? null,
493
+ observed_source: attemptObservedSource,
494
+ raw_normalized_stream_path: artifact.eventsPath,
495
+ transcript_path: artifact.transcriptPath,
496
+ });
497
+ emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
498
+ type: "reviewer.completed",
499
+ at: completedTime,
500
+ duration_ms: durationMs,
501
+ observed_model: attemptObservedModel ?? null,
502
+ observed_source: attemptObservedSource,
503
+ });
504
+ }
505
+ return {
506
+ text,
507
+ observedModel: attemptObservedModel,
508
+ observedSource: attemptObservedSource,
509
+ artifactDir: artifact.dir,
510
+ costUsd,
511
+ costEstimated,
512
+ };
513
+ };
514
+ const consume = consumeOnce(0);
515
+ const removeExternalAbortListeners = [];
516
+ const cancelled = new Promise((_, reject) => {
517
+ if (!signal)
518
+ return;
519
+ const onAbort = () => {
520
+ if (settled)
521
+ return;
522
+ cancelledBySignal = true;
523
+ controller.abort();
524
+ void currentIter?.return?.();
525
+ reject(Object.assign(new Error("Reviewer cancelled"), {
526
+ partialCostUsd: costUsd,
527
+ partialCostEstimated: costEstimated,
528
+ partialObservedModel: observedModel,
529
+ partialObservedSource: observedSource,
530
+ partialText,
531
+ }));
532
+ };
533
+ if (signal.aborted)
534
+ queueMicrotask(onAbort);
535
+ else
536
+ signal.addEventListener("abort", onAbort, { once: true });
537
+ removeExternalAbortListeners.push(() => signal.removeEventListener("abort", onAbort));
538
+ });
539
+ const timed = new Promise((_, reject) => {
540
+ timeout = setTimeout(() => {
541
+ if (settled)
542
+ return;
543
+ timedOut = true;
544
+ const timedOutAt = nowIso();
545
+ const durationMs = Date.now() - startMs;
546
+ controller.abort();
547
+ void currentIter?.return?.();
548
+ updateReviewerMetadata(artifact, {
549
+ status: "timed_out",
550
+ timeout_time: timedOutAt,
551
+ duration_ms: durationMs,
552
+ observed_model: observedModel ?? null,
553
+ observed_source: observedSource,
554
+ raw_normalized_stream_path: artifact.eventsPath,
555
+ transcript_path: artifact.transcriptPath,
556
+ });
557
+ emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
558
+ type: "reviewer.timed_out",
559
+ at: timedOutAt,
560
+ duration_ms: durationMs,
561
+ observed_model: observedModel ?? null,
562
+ observed_source: observedSource,
563
+ message: `Reviewer timed out after ${timeoutMs}ms`,
564
+ });
565
+ reject(Object.assign(new Error(`Reviewer timed out after ${timeoutMs}ms`), {
566
+ partialCostUsd: costUsd,
567
+ partialCostEstimated: costEstimated,
568
+ partialObservedModel: observedModel,
569
+ partialObservedSource: observedSource,
570
+ partialText,
571
+ }));
572
+ }, Math.max(1, timeoutMs));
573
+ });
574
+ try {
575
+ return await Promise.race([consume, timed, cancelled]);
576
+ }
577
+ catch (err) {
578
+ if (!timedOut) {
579
+ const failedAt = nowIso();
580
+ const durationMs = Date.now() - startMs;
581
+ const rawMessage = err instanceof Error ? err.message : String(err);
582
+ const message = cancelledBySignal ? "Reviewer cancelled" : rawMessage;
583
+ updateReviewerMetadata(artifact, {
584
+ status: "failed",
585
+ failure_time: failedAt,
586
+ duration_ms: durationMs,
587
+ error: redactSecrets(message),
588
+ raw_normalized_stream_path: artifact.eventsPath,
589
+ transcript_path: artifact.transcriptPath,
590
+ });
591
+ emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
592
+ type: "reviewer.failed",
593
+ at: failedAt,
594
+ duration_ms: durationMs,
595
+ message,
596
+ });
597
+ }
598
+ if (err && typeof err === "object") {
599
+ Object.assign(err, {
600
+ partialCostUsd: costUsd,
601
+ partialCostEstimated: costEstimated,
602
+ partialObservedModel: observedModel,
603
+ partialObservedSource: observedSource,
604
+ partialText,
605
+ });
606
+ }
607
+ throw err;
608
+ }
609
+ finally {
610
+ settled = true;
611
+ if (timeout)
612
+ clearTimeout(timeout);
613
+ for (const removeExternalAbortListener of removeExternalAbortListeners) {
614
+ removeExternalAbortListener();
615
+ }
616
+ consume.catch(() => {
617
+ /* timeout path: consume may reject after the race already returned */
618
+ });
619
+ }
620
+ }
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
+ function selectReviewerWorkspaceBaseDir(sourceRoot, artifactsBaseDir, sourceEvidenceDir) {
630
+ const durableBase = join(artifactsBaseDir, "workspaces");
631
+ if (!isSameOrInside(sourceRoot, durableBase) && !isSameOrInside(sourceEvidenceDir, durableBase)) {
632
+ return durableBase;
633
+ }
634
+ return join(tmpdir(), `claudexor-review-workspaces-${newId("ws")}`);
635
+ }
636
+ function isTemporaryReviewerWorkspaceBaseDir(baseDir) {
637
+ const resolved = resolve(baseDir);
638
+ const rel = relative(tmpdir(), resolved);
639
+ return (isSameOrInside(tmpdir(), resolved) &&
640
+ rel.split(/[\\/]+/)[0]?.startsWith("claudexor-review-workspaces-") === true);
641
+ }
642
+ async function prepareReviewerWorkspace(input) {
643
+ const sourceRoot = resolve(input.sourceRoot);
644
+ const workspaceBaseDir = resolve(input.workspaceBaseDir);
645
+ const root = join(workspaceBaseDir, input.reviewerDirName);
646
+ if (!existsSync(sourceRoot)) {
647
+ throw new Error(`candidate root does not exist: ${sourceRoot}`);
648
+ }
649
+ if (isSameOrInside(sourceRoot, root)) {
650
+ throw new Error(`reviewer workspace must be outside candidate root: ${root}`);
651
+ }
652
+ try {
653
+ await rm(root, { recursive: true, force: true });
654
+ await mkdir(root, { recursive: true, mode: 0o700 });
655
+ const excludeRoots = input.excludeRoots.map((p) => resolve(p));
656
+ const resolvedSourceRoot = realpathSync(sourceRoot);
657
+ await cp(sourceRoot, root, {
658
+ recursive: true,
659
+ dereference: false,
660
+ filter: (sourcePath) => shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, input.preservePaths),
661
+ });
662
+ const sourceEvidenceDir = resolve(input.sourceEvidenceDir);
663
+ const evidenceDir = join(root, ".claudexor-review-evidence");
664
+ if (existsSync(sourceEvidenceDir)) {
665
+ const resolvedSourceEvidenceDir = realpathSync(sourceEvidenceDir);
666
+ const evidenceExcludeRoots = excludeRoots.filter((root) => !isSameOrInside(root, sourceEvidenceDir));
667
+ 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
+ });
673
+ }
674
+ await mkdir(evidenceDir, { recursive: true, mode: 0o700 });
675
+ await initializeReviewerWorkspaceGit(root);
676
+ return { root, evidenceDir };
677
+ }
678
+ catch (err) {
679
+ await rm(root, { recursive: true, force: true });
680
+ throw err;
681
+ }
682
+ }
683
+ async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir) {
684
+ const source = resolve(sourceEvidenceDir);
685
+ const target = resolve(persistentEvidenceDir);
686
+ await rm(target, { recursive: true, force: true });
687
+ await mkdir(target, { recursive: true, mode: 0o700 });
688
+ if (!existsSync(source)) {
689
+ return;
690
+ }
691
+ const resolvedSource = realpathSync(source);
692
+ for (const entry of await readdir(source, { withFileTypes: true })) {
693
+ const sourcePath = join(source, entry.name);
694
+ if (!shouldCopyEvidencePacketPath(source, resolvedSource, sourcePath, target)) {
695
+ continue;
696
+ }
697
+ await copyReviewEvidenceEntry(source, resolvedSource, sourcePath, join(target, entry.name), target);
698
+ }
699
+ }
700
+ async function copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, targetPath, targetEvidenceDir) {
701
+ const stat = lstatSync(sourcePath);
702
+ if (stat.isDirectory()) {
703
+ await mkdir(targetPath, { recursive: true, mode: 0o700 });
704
+ for (const entry of await readdir(sourcePath, { withFileTypes: true })) {
705
+ const childSource = join(sourcePath, entry.name);
706
+ if (!shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceDir, childSource, targetEvidenceDir)) {
707
+ continue;
708
+ }
709
+ await copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidenceDir, childSource, join(targetPath, entry.name), targetEvidenceDir);
710
+ }
711
+ return;
712
+ }
713
+ if (stat.isFile() && shouldTextSanitizeEvidenceFile(sourcePath)) {
714
+ const raw = readTextSafe(sourcePath);
715
+ if (raw === null)
716
+ throw new Error(`could not read review evidence file: ${sourcePath}`);
717
+ const text = shouldFailClosedEvidenceFile(sourcePath) ? raw : redactSecrets(raw);
718
+ if (containsSecretLikeToken(text)) {
719
+ throw new Error(`review evidence file contains a secret-like token: ${relative(sourceEvidenceDir, sourcePath)}`);
720
+ }
721
+ writeText(targetPath, text);
722
+ return;
723
+ }
724
+ await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
725
+ await cp(sourcePath, targetPath, { recursive: false, dereference: false });
726
+ }
727
+ 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"));
735
+ }
736
+ function shouldFailClosedEvidenceFile(path) {
737
+ return path.toLowerCase().endsWith(".patch");
738
+ }
739
+ function shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, targetEvidenceDir) {
740
+ const resolvedSourcePath = resolve(sourcePath);
741
+ if (isSameOrInside(resolvedSourcePath, targetEvidenceDir))
742
+ return false;
743
+ if (isSameOrInside(targetEvidenceDir, resolvedSourcePath))
744
+ return false;
745
+ return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [
746
+ targetEvidenceDir,
747
+ ]);
748
+ }
749
+ async function cleanupReviewerWorkspace(workspace, artifact) {
750
+ try {
751
+ await rm(workspace.root, { recursive: true, force: true });
752
+ tryUpdateReviewerMetadata(artifact, { reviewer_workspace_cleanup: "removed" });
753
+ }
754
+ catch (err) {
755
+ tryUpdateReviewerMetadata(artifact, {
756
+ reviewer_workspace_cleanup: "failed",
757
+ reviewer_workspace_cleanup_error: redactSecrets(err instanceof Error ? err.message : String(err)),
758
+ });
759
+ }
760
+ }
761
+ async function cleanupTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir, artifactsBaseDir) {
762
+ if (!isTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir))
763
+ return;
764
+ try {
765
+ await rm(workspaceBaseDir, { recursive: true, force: true });
766
+ }
767
+ catch (err) {
768
+ try {
769
+ writeJson(join(artifactsBaseDir, "reviewer-workspace-base-cleanup-error.json"), {
770
+ reviewer_workspace_base_cleanup: "failed",
771
+ workspace_base_dir: workspaceBaseDir,
772
+ error: redactSecrets(err instanceof Error ? err.message : String(err)),
773
+ });
774
+ }
775
+ catch {
776
+ // Do not let cleanup telemetry hide the review result or the original error.
777
+ }
778
+ }
779
+ }
780
+ function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set()) {
781
+ const resolvedSourcePath = resolve(sourcePath);
782
+ if (!isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, resolvedSourcePath, excludeRoots)) {
783
+ return false;
784
+ }
785
+ if (excludeRoots.some((root) => isSameOrInside(root, resolvedSourcePath)))
786
+ return false;
787
+ const rel = relative(sourceRoot, resolvedSourcePath);
788
+ if (!rel)
789
+ return true;
790
+ const parts = rel.split(/[\\/]+/);
791
+ if (parts.some(isReviewerSecretLikePathPart)) {
792
+ return false;
793
+ }
794
+ if (parts[0] === ".claudexor") {
795
+ return isCopyableReviewerClaudexorPath(rel, parts, preservePaths);
796
+ }
797
+ if (parts.some((part) => [
798
+ ".git",
799
+ ".adversarial-review",
800
+ ".turbo",
801
+ "node_modules",
802
+ ].includes(part))) {
803
+ return false;
804
+ }
805
+ if (parts.some((part) => [".next", ".cache", "coverage", "dist"].includes(part)) &&
806
+ !isPreservedReviewerPath(rel, preservePaths)) {
807
+ return false;
808
+ }
809
+ return !rel.endsWith(".tsbuildinfo");
810
+ }
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;
831
+ }
832
+ if (/^id_(rsa|dsa|ecdsa|ed25519)$/.test(lower))
833
+ return true;
834
+ if (lower.endsWith(".pem") || lower.endsWith(".p12") || lower.endsWith(".pfx"))
835
+ return true;
836
+ return false;
837
+ }
838
+ function isSafeEnvTemplateName(lower) {
839
+ return [".env.example", ".env.sample", ".env.template"].includes(lower);
840
+ }
841
+ function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
842
+ if (parts.length === 1) {
843
+ return true;
844
+ }
845
+ if (parts.length === 2 && parts[1] === "config.yaml") {
846
+ return true;
847
+ }
848
+ 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)) {
863
+ return false;
864
+ }
865
+ return isPreservedReviewerPath(rel, preservePaths);
866
+ }
867
+ function isPreservedReviewerPath(rel, preservePaths) {
868
+ const normalized = normalizeReviewerRelativePath(rel);
869
+ if (!normalized)
870
+ return false;
871
+ if (preservePaths.has(normalized))
872
+ return true;
873
+ const prefix = `${normalized}/`;
874
+ for (const preserved of preservePaths) {
875
+ if (preserved.startsWith(prefix))
876
+ return true;
877
+ }
878
+ return false;
879
+ }
880
+ /** Test-only alias for the preserve-set extractor. */
881
+ export function __testExtractDiffTouchedPaths(diff) {
882
+ return extractDiffTouchedPaths(diff);
883
+ }
884
+ 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
+ const paths = new Set();
890
+ for (const file of parseUnifiedDiff(diff).files) {
891
+ if (file.oldPath)
892
+ addReviewerPreservePath(paths, file.oldPath);
893
+ if (file.newPath)
894
+ addReviewerPreservePath(paths, file.newPath);
895
+ }
896
+ return paths;
897
+ }
898
+ function addReviewerPreservePath(paths, value) {
899
+ const normalized = normalizeReviewerRelativePath(value);
900
+ if (normalized)
901
+ paths.add(normalized);
902
+ }
903
+ function normalizeReviewerRelativePath(value) {
904
+ if (!value || value === "/dev/null" || isAbsolute(value))
905
+ return null;
906
+ const normalized = normalize(value).replace(/\\/g, "/");
907
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
908
+ return null;
909
+ }
910
+ return normalized;
911
+ }
912
+ function isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots) {
913
+ let stat;
914
+ try {
915
+ stat = lstatSync(sourcePath);
916
+ }
917
+ catch {
918
+ return false;
919
+ }
920
+ if (!stat.isSymbolicLink())
921
+ return true;
922
+ let linkTarget = "";
923
+ let resolvedTarget = "";
924
+ try {
925
+ linkTarget = readlinkSync(sourcePath);
926
+ resolvedTarget = realpathSync(sourcePath);
927
+ }
928
+ catch {
929
+ return false;
930
+ }
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));
948
+ }
949
+ 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"]);
972
+ await runGitOrThrow("commit", root, [
973
+ "-c",
974
+ "core.hooksPath=/dev/null",
975
+ "commit",
976
+ "--allow-empty",
977
+ "--no-verify",
978
+ "--no-gpg-sign",
979
+ "-m",
980
+ "review baseline",
981
+ ]);
982
+ }
983
+ async function runGitOrThrow(label, cwd, args) {
984
+ const gitEnv = Object.fromEntries(Object.keys(process.env)
985
+ .filter((key) => key.startsWith("GIT_"))
986
+ .map((key) => [key, null]));
987
+ gitEnv.GIT_CONFIG_NOSYSTEM = "1";
988
+ const result = await runCapture("git", args, { cwd, env: gitEnv, timeoutMs: 60_000 });
989
+ if (result.code === 0)
990
+ return;
991
+ const detail = redactSecrets((result.stderr || result.stdout || `exit ${result.code}`).trim());
992
+ throw new Error(`failed to prepare reviewer workspace (${label}): ${detail}`);
993
+ }
994
+ function isSameOrInside(parent, target) {
995
+ const rel = relative(resolve(parent), resolve(target));
996
+ const firstPart = rel.split(/[\\/]+/)[0];
997
+ return rel === "" || (!!rel && firstPart !== ".." && !isAbsolute(rel));
998
+ }
999
+ function createReviewerArtifactContext(baseDir, index, reviewer) {
1000
+ const dir = join(baseDir, `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`);
1001
+ ensureDir(dir);
1002
+ const progressPath = join(baseDir, "reviewer-progress.jsonl");
1003
+ const metadata = {
1004
+ harness_id: reviewer.adapter.id,
1005
+ provider_family: reviewer.providerFamily,
1006
+ requested_model: reviewer.requestedModel ?? null,
1007
+ requested_effort: reviewer.requestedEffort ?? null,
1008
+ artifact_dir: dir,
1009
+ };
1010
+ const ctx = {
1011
+ dir,
1012
+ progressPath,
1013
+ metadataPath: join(dir, "metadata.json"),
1014
+ eventsPath: join(dir, "raw-normalized-stream.jsonl"),
1015
+ transcriptPath: join(dir, "transcript.md"),
1016
+ promptPath: join(dir, "prompt.md"),
1017
+ parsedPath: join(dir, "parsed-json-blocks.json"),
1018
+ parseErrorPath: join(dir, "parse-error.json"),
1019
+ metadata,
1020
+ };
1021
+ writeJson(ctx.metadataPath, metadata);
1022
+ writeText(ctx.eventsPath, "");
1023
+ writeText(ctx.transcriptPath, "");
1024
+ return ctx;
1025
+ }
1026
+ function safeFilePart(value) {
1027
+ const safe = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1028
+ return safe || "reviewer";
1029
+ }
1030
+ function emitReviewerProgress(artifact, reviewer, onReviewerEvent, patch) {
1031
+ const event = {
1032
+ harness_id: reviewer.adapter.id,
1033
+ provider_family: reviewer.providerFamily,
1034
+ requested_model: reviewer.requestedModel ?? null,
1035
+ requested_effort: reviewer.requestedEffort ?? null,
1036
+ artifact_dir: artifact.dir,
1037
+ ...patch,
1038
+ };
1039
+ const redacted = redactValue(event);
1040
+ appendLine(artifact.progressPath, JSON.stringify(redacted));
1041
+ try {
1042
+ onReviewerEvent?.(redacted);
1043
+ }
1044
+ catch {
1045
+ /* progress observers must never affect review state */
1046
+ }
1047
+ }
1048
+ function transientRetryDelayMs(policy, retryIndex) {
1049
+ return Math.min(policy.initialDelayMs * 2 ** retryIndex, policy.maxDelayMs);
1050
+ }
1051
+ function sleep(ms) {
1052
+ if (ms <= 0)
1053
+ return Promise.resolve();
1054
+ return new Promise((resolve) => setTimeout(resolve, ms));
1055
+ }
1056
+ function updateReviewerMetadata(artifact, patch) {
1057
+ artifact.metadata = { ...artifact.metadata, ...redactValue(patch) };
1058
+ writeJson(artifact.metadataPath, artifact.metadata);
1059
+ }
1060
+ function tryUpdateReviewerMetadata(artifact, patch) {
1061
+ try {
1062
+ updateReviewerMetadata(artifact, patch);
1063
+ }
1064
+ catch {
1065
+ // Cleanup telemetry must never hide the review result or original error.
1066
+ }
1067
+ }
1068
+ function writeParseError(artifact, value) {
1069
+ writeJson(artifact.parseErrorPath, redactValue(value));
1070
+ }
1071
+ function redactValue(value) {
1072
+ try {
1073
+ return JSON.parse(redactSecrets(JSON.stringify(value)));
1074
+ }
1075
+ catch {
1076
+ return value;
1077
+ }
1078
+ }
1079
+ function insufficientEvidenceFinding(reviewer, claim) {
1080
+ return ReviewFindingSchema.parse({
1081
+ id: newId("f"),
1082
+ severity: "INSUFFICIENT_EVIDENCE",
1083
+ category: "test_gap",
1084
+ claim,
1085
+ evidence: {},
1086
+ proposed_fix: "Treat this review as inconclusive and rerun with a healthy reviewer.",
1087
+ reviewer: {
1088
+ harness_id: reviewer.harness_id,
1089
+ requested_model: reviewer.requested_model ?? null,
1090
+ requested_effort: reviewer.requested_effort ?? null,
1091
+ observed_model: reviewer.observed_model ?? null,
1092
+ route_proof_status: reviewer.route_proof_status ?? "unverified",
1093
+ },
1094
+ status: "insufficient_evidence",
1095
+ });
1096
+ }
1097
+ //# sourceMappingURL=reviewEngine.js.map