@claudexor/review 3.1.1 → 3.2.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.
- package/dist/findings.d.ts +11 -0
- package/dist/findings.d.ts.map +1 -1
- package/dist/findings.js +48 -5
- package/dist/findings.js.map +1 -1
- package/dist/readiness.d.ts.map +1 -1
- package/dist/revalidate.js.map +1 -1
- package/dist/reviewEngine.d.ts +2 -4
- package/dist/reviewEngine.d.ts.map +1 -1
- package/dist/reviewEngine.js +202 -372
- package/dist/reviewEngine.js.map +1 -1
- package/dist/reviewPrompt.d.ts +122 -1
- package/dist/reviewPrompt.d.ts.map +1 -1
- package/dist/reviewPrompt.js +102 -17
- package/dist/reviewPrompt.js.map +1 -1
- package/dist/reviewRuntimeTypes.d.ts +9 -2
- package/dist/reviewRuntimeTypes.d.ts.map +1 -1
- package/dist/reviewRuntimeTypes.js +9 -1
- package/dist/reviewRuntimeTypes.js.map +1 -1
- package/dist/reviewerCostKnowledge.d.ts +26 -0
- package/dist/reviewerCostKnowledge.d.ts.map +1 -0
- package/dist/reviewerCostKnowledge.js +73 -0
- package/dist/reviewerCostKnowledge.js.map +1 -0
- package/dist/reviewerSpendAccumulator.d.ts +26 -0
- package/dist/reviewerSpendAccumulator.d.ts.map +1 -0
- package/dist/reviewerSpendAccumulator.js +48 -0
- package/dist/reviewerSpendAccumulator.js.map +1 -0
- package/dist/reviewerWorkspace.d.ts +24 -0
- package/dist/reviewerWorkspace.d.ts.map +1 -0
- package/dist/reviewerWorkspace.js +360 -0
- package/dist/reviewerWorkspace.js.map +1 -0
- package/dist/route.js.map +1 -1
- package/dist/sealedReviewEnvelope.d.ts +20 -0
- package/dist/sealedReviewEnvelope.d.ts.map +1 -0
- package/dist/sealedReviewEnvelope.js +176 -0
- package/dist/sealedReviewEnvelope.js.map +1 -0
- package/package.json +6 -6
package/dist/reviewEngine.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import { parseUnifiedDiff, runCapture } from "@claudexor/core";
|
|
2
1
|
import { preflightEvidence, writeDiffEvidence } from "@claudexor/context";
|
|
3
2
|
import { HarnessRunSpec, ReviewFinding as ReviewFindingSchema } from "@claudexor/schema";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { appendLine, containsSecretLikeToken, ensureDir, newId, nowIso, readTextSafe, redactSecrets,
|
|
9
|
-
import { dedupeFindings, extractJsonBlocks, parseFindingsDetailed, } from "./findings.js";
|
|
10
|
-
import { buildReviewPrompt } from "./reviewPrompt.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
5
|
+
import { rm } from "node:fs/promises";
|
|
6
|
+
import { join, relative, resolve } from "node:path";
|
|
7
|
+
import { appendLine, containsSecretLikeToken, engineBuildIdentity, ensureDir, newId, nowIso, readTextSafe, redactSecrets, sha256, writeJson, writeText, } from "@claudexor/util";
|
|
8
|
+
import { dedupeFindings, extractJsonBlocks, parseFindingsDetailed, parseSealedReviewEnvelopeDetailed, sealedReviewTranscriptChunk, } from "./findings.js";
|
|
9
|
+
import { buildReviewPrompt, SEALED_REVIEW_OUTPUT_SCHEMA } from "./reviewPrompt.js";
|
|
10
|
+
import { sealedReviewTranscriptFromEvents } from "./sealedReviewEnvelope.js";
|
|
11
|
+
import { buildReviewerCandidateInventory, cleanupTemporaryReviewerWorkspaceBaseDir, copyReviewEvidencePacket, extractDiffPostimagePaths, isSameOrInside, prepareReviewerWorkspace, selectReviewerWorkspaceBaseDir, } from "./reviewerWorkspace.js";
|
|
12
|
+
export { extractDiffPostimagePaths as __testExtractDiffPostimagePaths } from "./reviewerWorkspace.js";
|
|
11
13
|
import { buildRouteProof, classifyDiversity } from "./route.js";
|
|
12
|
-
import { reviewerAuthMode, reviewerAuthSwitchFromEvent
|
|
14
|
+
import { reviewerAuthMode, reviewerAuthSwitchFromEvent } from "./reviewRuntimeTypes.js";
|
|
15
|
+
import { ReviewerCostKnowledge } from "./reviewerCostKnowledge.js";
|
|
16
|
+
import { ReviewerSpendAccumulator } from "./reviewerSpendAccumulator.js";
|
|
13
17
|
const DEFAULT_REVIEWER_TIMEOUT_MS = 10 * 60_000;
|
|
14
18
|
const DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY = {
|
|
15
19
|
maxRetries: 2,
|
|
16
20
|
initialDelayMs: 1_000,
|
|
17
21
|
maxDelayMs: 10_000,
|
|
18
22
|
};
|
|
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
23
|
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
24
|
function readExistingDiffEvidence(dir, diff) {
|
|
23
25
|
const diffText = diff.endsWith("\n") ? diff : `${diff}\n`;
|
|
@@ -53,6 +55,26 @@ function reviewerInfo(reviewer, routeProofStatus, observedModel = null) {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
export async function reviewCandidate(input) {
|
|
58
|
+
const sourceRoot = resolve(input.cwd);
|
|
59
|
+
const sourceExists = existsSync(sourceRoot);
|
|
60
|
+
const canonicalSourceRoot = sourceExists ? realpathSync(sourceRoot) : sourceRoot;
|
|
61
|
+
const sourceEvidencePath = resolve(input.evidenceDir);
|
|
62
|
+
const sourceEvidenceRoot = existsSync(input.evidenceDir)
|
|
63
|
+
? realpathSync(input.evidenceDir)
|
|
64
|
+
: sourceEvidencePath;
|
|
65
|
+
if (sourceExists && isSameOrInside(sourceEvidenceRoot, canonicalSourceRoot)) {
|
|
66
|
+
throw new Error("review evidence directory must not contain the candidate root");
|
|
67
|
+
}
|
|
68
|
+
const evidenceInsideLexicalSource = sourceExists && isSameOrInside(sourceRoot, sourceEvidencePath);
|
|
69
|
+
const evidenceInsideCanonicalSource = sourceExists && isSameOrInside(canonicalSourceRoot, sourceEvidenceRoot);
|
|
70
|
+
const evidencePathInSourceNamespace = evidenceInsideCanonicalSource
|
|
71
|
+
? resolve(sourceRoot, relative(canonicalSourceRoot, sourceEvidenceRoot))
|
|
72
|
+
: null;
|
|
73
|
+
const candidateEvidenceExcludeRoots = evidenceInsideLexicalSource || evidenceInsideCanonicalSource
|
|
74
|
+
? [
|
|
75
|
+
...new Set([sourceEvidencePath, sourceEvidenceRoot, evidencePathInSourceNamespace].filter((path) => path !== null)),
|
|
76
|
+
]
|
|
77
|
+
: [];
|
|
56
78
|
const findingsByReviewer = input.reviewers.map(() => []);
|
|
57
79
|
const reviewerFamilies = input.reviewers.map((reviewer) => reviewer.providerFamily);
|
|
58
80
|
const routeProofs = input.reviewers.map((reviewer, index) => reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies.filter((_, otherIndex) => otherIndex !== index)));
|
|
@@ -63,23 +85,33 @@ export async function reviewCandidate(input) {
|
|
|
63
85
|
requested_effort: reviewer.requestedEffort ?? null,
|
|
64
86
|
}));
|
|
65
87
|
const healthyReviewerIndexes = new Set();
|
|
66
|
-
const
|
|
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);
|
|
88
|
+
const reviewerSpend = new ReviewerSpendAccumulator(input.reviewers.length);
|
|
71
89
|
const reviewerTimeoutMs = input.reviewerTimeoutMs ?? DEFAULT_REVIEWER_TIMEOUT_MS;
|
|
72
90
|
const reviewWaveId = input.env?.["CLAUDEXOR_REVIEW_WAVE_ID"] ?? process.env["CLAUDEXOR_REVIEW_WAVE_ID"] ?? null;
|
|
73
91
|
if (input.evidenceReadOnly && input.frozenIdentity && !REVIEW_WAVE_ID.test(reviewWaveId ?? "")) {
|
|
74
92
|
throw new Error("sealed release review requires CLAUDEXOR_REVIEW_WAVE_ID UUID");
|
|
75
93
|
}
|
|
76
94
|
const frozenMetadata = input.frozenIdentity
|
|
77
|
-
? {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
? (() => {
|
|
96
|
+
const runtime = engineBuildIdentity();
|
|
97
|
+
const runtimeEntry = realpathSync(runtime.entry);
|
|
98
|
+
const runtimeEntryStat = lstatSync(runtimeEntry);
|
|
99
|
+
if (!runtimeEntryStat.isFile() || runtimeEntryStat.isSymbolicLink()) {
|
|
100
|
+
throw new Error("sealed release review runtime entry is not a regular file");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
candidate_sha: input.frozenIdentity.candidateSha,
|
|
104
|
+
candidate_tree: input.frozenIdentity.candidateTree,
|
|
105
|
+
packet_manifest_sha256: input.frozenIdentity.packetManifestSha256,
|
|
106
|
+
review_runtime_version: runtime.version,
|
|
107
|
+
review_runtime_build_sha: runtime.sha,
|
|
108
|
+
review_runtime_entry: runtimeEntry,
|
|
109
|
+
review_runtime_entry_sha256: createHash("sha256")
|
|
110
|
+
.update(readFileSync(runtimeEntry))
|
|
111
|
+
.digest("hex"),
|
|
112
|
+
...(reviewWaveId ? { review_wave_id: reviewWaveId } : {}),
|
|
113
|
+
};
|
|
114
|
+
})()
|
|
83
115
|
: {};
|
|
84
116
|
if (containsSecretLikeToken(input.diff || "(empty diff)\n")) {
|
|
85
117
|
throw new Error("diff evidence contains a secret-like token; refusing to persist raw DIFF.patch");
|
|
@@ -102,7 +134,15 @@ export async function reviewCandidate(input) {
|
|
|
102
134
|
].filter(Boolean);
|
|
103
135
|
throw new Error(`mandatory evidence preflight failed (${parts.join("; ")})`);
|
|
104
136
|
}
|
|
137
|
+
const postimagePaths = extractDiffPostimagePaths(input.diff);
|
|
138
|
+
const candidateInventory = await buildReviewerCandidateInventory(input.cwd, postimagePaths, input.evidenceReadOnly === true);
|
|
105
139
|
const artifactsBaseDir = input.artifactsDir ?? join(input.evidenceDir, "reviewer-artifacts");
|
|
140
|
+
if (input.evidenceReadOnly &&
|
|
141
|
+
input.frozenIdentity &&
|
|
142
|
+
existsSync(artifactsBaseDir) &&
|
|
143
|
+
readdirSync(artifactsBaseDir).length > 0) {
|
|
144
|
+
throw new Error("sealed release review requires a fresh empty artifacts directory");
|
|
145
|
+
}
|
|
106
146
|
ensureDir(artifactsBaseDir);
|
|
107
147
|
const persistentEvidenceDir = join(artifactsBaseDir, "evidence");
|
|
108
148
|
await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir, input.evidenceReadOnly === true);
|
|
@@ -118,11 +158,11 @@ export async function reviewCandidate(input) {
|
|
|
118
158
|
diff_path: persistentPatch.diffPath,
|
|
119
159
|
summary_path: persistentPatch.summaryPath,
|
|
120
160
|
diff_sha256: persistentPatch.diffSha256,
|
|
121
|
-
|
|
161
|
+
candidate_inventory_mode: candidateInventory.mode,
|
|
162
|
+
candidate_inventory_reason: candidateInventory.reason,
|
|
122
163
|
...frozenMetadata,
|
|
123
164
|
});
|
|
124
165
|
const artifacts = input.reviewers.map(() => undefined);
|
|
125
|
-
const preservePaths = extractDiffTouchedPaths(input.diff);
|
|
126
166
|
const reviewerWorkspaceBaseDir = selectReviewerWorkspaceBaseDir(input.cwd, artifactsBaseDir, input.evidenceDir);
|
|
127
167
|
const runReviewer = async (reviewer, index) => {
|
|
128
168
|
if (input.signal?.aborted)
|
|
@@ -137,8 +177,9 @@ export async function reviewCandidate(input) {
|
|
|
137
177
|
sourceEvidenceDir: persistentEvidenceDir,
|
|
138
178
|
workspaceBaseDir: reviewerWorkspaceBaseDir,
|
|
139
179
|
reviewerDirName: `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`,
|
|
140
|
-
excludeRoots: [artifactsBaseDir],
|
|
141
|
-
|
|
180
|
+
excludeRoots: [artifactsBaseDir, ...candidateEvidenceExcludeRoots],
|
|
181
|
+
postimagePaths,
|
|
182
|
+
candidateCopyPaths: candidateInventory.copyPaths,
|
|
142
183
|
preserveEvidenceBytes: input.evidenceReadOnly === true,
|
|
143
184
|
});
|
|
144
185
|
const reviewerPatch = input.evidenceReadOnly
|
|
@@ -154,34 +195,42 @@ export async function reviewCandidate(input) {
|
|
|
154
195
|
persistent_diff_path: persistentPatch.diffPath,
|
|
155
196
|
persistent_summary_path: persistentPatch.summaryPath,
|
|
156
197
|
diff_sha256: persistentPatch.diffSha256,
|
|
157
|
-
|
|
198
|
+
candidate_inventory_mode: candidateInventory.mode,
|
|
199
|
+
candidate_inventory_reason: candidateInventory.reason,
|
|
158
200
|
...frozenMetadata,
|
|
159
201
|
});
|
|
160
|
-
const runtimePrompt = buildReviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch,
|
|
202
|
+
const runtimePrompt = buildReviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch, {
|
|
203
|
+
sealed: input.evidenceReadOnly === true,
|
|
204
|
+
candidateInventoryMode: candidateInventory.mode,
|
|
205
|
+
});
|
|
161
206
|
spec = HarnessRunSpec.parse({
|
|
162
207
|
session_id: newId("rev"),
|
|
163
208
|
intent: "review",
|
|
164
209
|
prompt: runtimePrompt,
|
|
165
210
|
cwd: reviewerWorkspace.root,
|
|
166
211
|
access: "readonly",
|
|
212
|
+
...(input.evidenceReadOnly && input.frozenIdentity
|
|
213
|
+
? {
|
|
214
|
+
external_context_policy: "live",
|
|
215
|
+
tool_permission_policy: { web: "live", allow: [], deny: [] },
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
167
218
|
model_hint: reviewer.requestedModel ?? null,
|
|
168
219
|
effort_hint: reviewer.requestedEffort ?? null,
|
|
169
220
|
auth_preference: reviewer.authPreference ?? "auto",
|
|
170
221
|
env_inheritance: input.envInheritance ?? "mirror_native",
|
|
222
|
+
...(input.evidenceReadOnly && input.frozenIdentity
|
|
223
|
+
? { output_schema: SEALED_REVIEW_OUTPUT_SCHEMA }
|
|
224
|
+
: {}),
|
|
171
225
|
...(input.env ? { env: input.env } : {}),
|
|
172
226
|
});
|
|
173
|
-
writeText(artifact.promptPath,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
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.
|
|
182
|
-
|
|
183
|
-
${runtimePrompt}
|
|
184
|
-
`));
|
|
227
|
+
writeText(artifact.promptPath, spec.prompt);
|
|
228
|
+
updateReviewerMetadata(artifact, {
|
|
229
|
+
session_id: spec.session_id,
|
|
230
|
+
external_context_policy: spec.external_context_policy,
|
|
231
|
+
tool_web_policy: spec.tool_permission_policy.web,
|
|
232
|
+
submitted_prompt_sha256: createHash("sha256").update(spec.prompt).digest("hex"),
|
|
233
|
+
});
|
|
185
234
|
}
|
|
186
235
|
catch (err) {
|
|
187
236
|
const failedAt = nowIso();
|
|
@@ -212,17 +261,17 @@ ${runtimePrompt}
|
|
|
212
261
|
let routeModel;
|
|
213
262
|
let routeSource = "unavailable";
|
|
214
263
|
let reviewerError = null;
|
|
264
|
+
let sealedProjectionError = null;
|
|
215
265
|
try {
|
|
216
|
-
const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.
|
|
266
|
+
const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.evidenceReadOnly && input.frozenIdentity
|
|
267
|
+
? { maxRetries: 0, initialDelayMs: 0, maxDelayMs: 0 }
|
|
268
|
+
: (input.transientRetryPolicy ?? DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY), artifact, input.onReviewerEvent, input.signal, input.evidenceReadOnly === true);
|
|
217
269
|
text = out.text;
|
|
270
|
+
sealedProjectionError = out.sealedProjectionError ?? null;
|
|
218
271
|
streamObservedModel = out.observedModel;
|
|
219
272
|
routeModel = out.observedModel;
|
|
220
273
|
routeSource = out.observedSource;
|
|
221
|
-
|
|
222
|
-
reviewSpendEstimatedByReviewer[index] = out.costEstimated;
|
|
223
|
-
reviewCashByReviewer[index] = out.cashUsd;
|
|
224
|
-
reviewValuationByReviewer[index] = out.valuationUsd;
|
|
225
|
-
reviewUnknownByReviewer[index] = out.unknownUsd;
|
|
274
|
+
reviewerSpend.record(index, out);
|
|
226
275
|
if (!routeModel && reviewer.requestedModel) {
|
|
227
276
|
routeModel = reviewer.requestedModel;
|
|
228
277
|
routeSource = "metadata";
|
|
@@ -234,13 +283,10 @@ ${runtimePrompt}
|
|
|
234
283
|
if (typeof partial?.partialText === "string" && partial.partialText.trim() !== "") {
|
|
235
284
|
text = partial.partialText;
|
|
236
285
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
reviewSpendEstimatedByReviewer[index] = partial.partialCostEstimated === true;
|
|
240
|
-
reviewCashByReviewer[index] = partial.partialCashUsd ?? 0;
|
|
241
|
-
reviewValuationByReviewer[index] = partial.partialValuationUsd ?? 0;
|
|
242
|
-
reviewUnknownByReviewer[index] = partial.partialUnknownUsd ?? 0;
|
|
286
|
+
if (typeof partial?.partialSealedProjectionError === "string") {
|
|
287
|
+
sealedProjectionError = partial.partialSealedProjectionError;
|
|
243
288
|
}
|
|
289
|
+
reviewerSpend.recordPartial(index, partial);
|
|
244
290
|
if (partial?.partialObservedModel) {
|
|
245
291
|
streamObservedModel = partial.partialObservedModel;
|
|
246
292
|
routeModel = partial.partialObservedModel;
|
|
@@ -254,8 +300,24 @@ ${runtimePrompt}
|
|
|
254
300
|
const proof = reviewerRouteProof(reviewer, routeModel ?? null, routeSource, reviewerFamilies.filter((_, i) => i !== index));
|
|
255
301
|
routeProofs[index] = proof;
|
|
256
302
|
const info = reviewerInfo(reviewer, proof.status, streamObservedModel ?? null);
|
|
257
|
-
const
|
|
303
|
+
const sealedParse = input.evidenceReadOnly
|
|
304
|
+
? parseSealedReviewEnvelopeDetailed(text, info)
|
|
305
|
+
: null;
|
|
306
|
+
const jsonBlocks = sealedParse?.blocks ?? extractJsonBlocks(text);
|
|
258
307
|
writeJson(artifact.parsedPath, redactValue(jsonBlocks));
|
|
308
|
+
if (sealedParse && (sealedProjectionError || sealedParse.error)) {
|
|
309
|
+
const detail = sealedProjectionError ?? sealedParse.error ?? "invalid sealed review output";
|
|
310
|
+
writeParseError(artifact, {
|
|
311
|
+
error: "invalid_sealed_review_envelope",
|
|
312
|
+
detail,
|
|
313
|
+
malformed: sealedParse.malformed,
|
|
314
|
+
text_sha256: sha256(text),
|
|
315
|
+
...(reviewerError ? { reviewer_error: reviewerError } : {}),
|
|
316
|
+
});
|
|
317
|
+
findingsByReviewer[index]?.push(...sealedParse.findings);
|
|
318
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Invalid sealed review envelope: ${detail}.`));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
259
321
|
if (reviewerError && (text.trim() === "" || jsonBlocks.length === 0)) {
|
|
260
322
|
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer failed: ${reviewerError}`));
|
|
261
323
|
return;
|
|
@@ -265,7 +327,7 @@ ${runtimePrompt}
|
|
|
265
327
|
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, "Reviewer produced no parseable JSON findings."));
|
|
266
328
|
return;
|
|
267
329
|
}
|
|
268
|
-
const parsed = parseFindingsDetailed(text, info);
|
|
330
|
+
const parsed = sealedParse ?? parseFindingsDetailed(text, info);
|
|
269
331
|
const parseError = {};
|
|
270
332
|
let parsedFindingsRecorded = false;
|
|
271
333
|
const recordParsedFindings = () => {
|
|
@@ -274,7 +336,7 @@ ${runtimePrompt}
|
|
|
274
336
|
findingsByReviewer[index]?.push(...parsed.findings);
|
|
275
337
|
parsedFindingsRecorded = true;
|
|
276
338
|
};
|
|
277
|
-
if (parsed.malformed > 0) {
|
|
339
|
+
if (parsed.malformed > 0 && !sealedParse?.error) {
|
|
278
340
|
Object.assign(parseError, {
|
|
279
341
|
error: "malformed_findings",
|
|
280
342
|
malformed: parsed.malformed,
|
|
@@ -347,14 +409,14 @@ ${runtimePrompt}
|
|
|
347
409
|
healthyProviders,
|
|
348
410
|
crossFamilyVerified: observedFamilies.length >= 2,
|
|
349
411
|
distinctProviders: observedFamilies,
|
|
350
|
-
...
|
|
412
|
+
...reviewerSpend.summary(),
|
|
351
413
|
};
|
|
352
414
|
}
|
|
353
415
|
finally {
|
|
354
416
|
await cleanupTemporaryReviewerWorkspaceBaseDir(reviewerWorkspaceBaseDir, artifactsBaseDir);
|
|
355
417
|
}
|
|
356
418
|
}
|
|
357
|
-
async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPolicy, artifact, onReviewerEvent, signal) {
|
|
419
|
+
async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPolicy, artifact, onReviewerEvent, signal, sealed = false) {
|
|
358
420
|
const controller = new AbortController();
|
|
359
421
|
spec.extra["abortSignal"] = controller.signal;
|
|
360
422
|
const startMs = Date.now();
|
|
@@ -383,14 +445,19 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
383
445
|
let observedSource = "unavailable";
|
|
384
446
|
let observedAuthMode = null;
|
|
385
447
|
let currentAuthMode = null;
|
|
448
|
+
const observedAuthModes = new Set();
|
|
449
|
+
const ignoredSettings = new Set();
|
|
386
450
|
let costUsd = 0;
|
|
387
451
|
let costEstimated = false;
|
|
388
452
|
let cashUsd = 0;
|
|
389
453
|
let valuationUsd = 0;
|
|
390
454
|
let unknownUsd = 0;
|
|
455
|
+
const costKnowledge = new ReviewerCostKnowledge();
|
|
391
456
|
let partialText = "";
|
|
392
457
|
const isCancelled = () => cancelledBySignal || signal?.aborted === true || controller.signal.aborted;
|
|
393
458
|
const consumeOnce = async (nativeTry) => {
|
|
459
|
+
currentAuthMode = null;
|
|
460
|
+
costKnowledge.startAttempt();
|
|
394
461
|
const iter = (reviewer.adapter.review ?? reviewer.adapter.run).call(reviewer.adapter, runSpec);
|
|
395
462
|
currentIter = iter;
|
|
396
463
|
let text = "";
|
|
@@ -399,9 +466,20 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
399
466
|
let lastError = null;
|
|
400
467
|
let attemptObservedModel;
|
|
401
468
|
let attemptObservedSource = "unavailable";
|
|
469
|
+
const sealedMessageEvents = [];
|
|
402
470
|
for await (const ev of iter) {
|
|
403
471
|
const eventTime = nowIso();
|
|
404
|
-
|
|
472
|
+
const persistedEvent = redactValue(ev);
|
|
473
|
+
appendLine(artifact.eventsPath, JSON.stringify(persistedEvent));
|
|
474
|
+
const ignored = ev.payload?.["ignored_settings"];
|
|
475
|
+
if (Array.isArray(ignored)) {
|
|
476
|
+
for (const item of ignored)
|
|
477
|
+
if (typeof item === "string")
|
|
478
|
+
ignoredSettings.add(item);
|
|
479
|
+
if (ignoredSettings.size > 0) {
|
|
480
|
+
updateReviewerMetadata(artifact, { ignored_settings: [...ignoredSettings] });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
405
483
|
if (ev.transient)
|
|
406
484
|
sawTransient = true;
|
|
407
485
|
if (ev.type === "error") {
|
|
@@ -422,8 +500,12 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
422
500
|
});
|
|
423
501
|
}
|
|
424
502
|
const disclosedAuthMode = reviewerAuthMode(ev.credential_route);
|
|
425
|
-
if (disclosedAuthMode)
|
|
503
|
+
if (disclosedAuthMode) {
|
|
426
504
|
currentAuthMode = disclosedAuthMode;
|
|
505
|
+
observedAuthModes.add(disclosedAuthMode);
|
|
506
|
+
updateReviewerMetadata(artifact, { auth_modes: [...observedAuthModes] });
|
|
507
|
+
}
|
|
508
|
+
costKnowledge.observeEvent(currentAuthMode);
|
|
427
509
|
if (!observedAuthMode) {
|
|
428
510
|
observedAuthMode = disclosedAuthMode;
|
|
429
511
|
if (observedAuthMode)
|
|
@@ -437,16 +519,23 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
437
519
|
at: firstEventTime,
|
|
438
520
|
});
|
|
439
521
|
}
|
|
440
|
-
if (ev.type === "usage" &&
|
|
522
|
+
if (ev.type === "usage" &&
|
|
523
|
+
typeof ev.usage?.cost_usd === "number" &&
|
|
524
|
+
Number.isFinite(ev.usage.cost_usd) &&
|
|
525
|
+
ev.usage.cost_usd >= 0) {
|
|
441
526
|
costUsd += ev.usage.cost_usd;
|
|
442
527
|
if (ev.usage.estimated)
|
|
443
528
|
costEstimated = true;
|
|
444
|
-
|
|
529
|
+
costKnowledge.observeUsage(currentAuthMode, ev.usage.estimated === true);
|
|
530
|
+
if (currentAuthMode === "local_session") {
|
|
445
531
|
valuationUsd += ev.usage.cost_usd;
|
|
446
|
-
|
|
532
|
+
}
|
|
533
|
+
else if (currentAuthMode === "api_key") {
|
|
447
534
|
cashUsd += ev.usage.cost_usd;
|
|
448
|
-
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
449
537
|
unknownUsd += ev.usage.cost_usd;
|
|
538
|
+
}
|
|
450
539
|
updateReviewerMetadata(artifact, {
|
|
451
540
|
cost_usd: costUsd,
|
|
452
541
|
cost_estimated: costEstimated,
|
|
@@ -455,11 +544,24 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
455
544
|
unknown_usd: unknownUsd,
|
|
456
545
|
});
|
|
457
546
|
}
|
|
547
|
+
if (sealed && ev.type === "message" && ev.final === true) {
|
|
548
|
+
sealedMessageEvents.push(persistedEvent);
|
|
549
|
+
}
|
|
458
550
|
if (ev.type === "message" && ev.text && ev.payload?.["auth_switched"] !== true) {
|
|
459
|
-
const safeText =
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
551
|
+
const safeText = sealedReviewTranscriptChunk(persistedEvent);
|
|
552
|
+
if (safeText !== null) {
|
|
553
|
+
if (sealed) {
|
|
554
|
+
// Keep the long-running transcript visibly active for monitors. A
|
|
555
|
+
// clean completion replaces these progress bytes with the exact
|
|
556
|
+
// typed-final projection used by the sealed parser and sealer.
|
|
557
|
+
appendLine(artifact.transcriptPath, safeText);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
text += safeText;
|
|
561
|
+
partialText += safeText;
|
|
562
|
+
appendLine(artifact.transcriptPath, safeText);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
463
565
|
}
|
|
464
566
|
if (ev.observed_model) {
|
|
465
567
|
observedModel = ev.observed_model;
|
|
@@ -479,6 +581,17 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
479
581
|
if (isCancelled()) {
|
|
480
582
|
throw new Error("Reviewer cancelled");
|
|
481
583
|
}
|
|
584
|
+
let sealedProjectionError;
|
|
585
|
+
if (sealed) {
|
|
586
|
+
try {
|
|
587
|
+
text = sealedReviewTranscriptFromEvents(sealedMessageEvents);
|
|
588
|
+
partialText = text;
|
|
589
|
+
writeText(artifact.transcriptPath, text);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
sealedProjectionError = error instanceof Error ? error.message : String(error);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
482
595
|
if (sawTransient &&
|
|
483
596
|
text.trim() === "" &&
|
|
484
597
|
nativeTry < transientRetryPolicy.maxRetries &&
|
|
@@ -508,10 +621,13 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
508
621
|
session_id: newId("ses"),
|
|
509
622
|
extra: { ...runSpec.extra, abortSignal: controller.signal },
|
|
510
623
|
});
|
|
624
|
+
costKnowledge.finishAttempt();
|
|
511
625
|
return consumeOnce(nativeTry + 1);
|
|
512
626
|
}
|
|
513
627
|
if (sawError && !timedOut) {
|
|
514
|
-
throw new Error(`Reviewer emitted error event: ${lastError ?? "unknown error"}`)
|
|
628
|
+
throw Object.assign(new Error(`Reviewer emitted error event: ${lastError ?? "unknown error"}`), {
|
|
629
|
+
...(sealedProjectionError ? { partialSealedProjectionError: sealedProjectionError } : {}),
|
|
630
|
+
});
|
|
515
631
|
}
|
|
516
632
|
if (!timedOut && !isCancelled()) {
|
|
517
633
|
const completedTime = nowIso();
|
|
@@ -533,8 +649,11 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
533
649
|
observed_source: attemptObservedSource,
|
|
534
650
|
});
|
|
535
651
|
}
|
|
652
|
+
const knowledge = costKnowledge.snapshot();
|
|
653
|
+
costKnowledge.finishAttempt();
|
|
536
654
|
return {
|
|
537
655
|
text,
|
|
656
|
+
...(sealedProjectionError ? { sealedProjectionError } : {}),
|
|
538
657
|
observedModel: attemptObservedModel,
|
|
539
658
|
observedSource: attemptObservedSource,
|
|
540
659
|
costUsd,
|
|
@@ -542,6 +661,7 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
542
661
|
cashUsd,
|
|
543
662
|
valuationUsd,
|
|
544
663
|
unknownUsd,
|
|
664
|
+
...knowledge,
|
|
545
665
|
};
|
|
546
666
|
};
|
|
547
667
|
const consume = consumeOnce(0);
|
|
@@ -555,12 +675,15 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
555
675
|
cancelledBySignal = true;
|
|
556
676
|
controller.abort();
|
|
557
677
|
void currentIter?.return?.();
|
|
678
|
+
const knowledge = costKnowledge.snapshot();
|
|
558
679
|
reject(Object.assign(new Error("Reviewer cancelled"), {
|
|
559
680
|
partialCostUsd: costUsd,
|
|
560
681
|
partialCostEstimated: costEstimated,
|
|
561
682
|
partialCashUsd: cashUsd,
|
|
562
683
|
partialValuationUsd: valuationUsd,
|
|
563
684
|
partialUnknownUsd: unknownUsd,
|
|
685
|
+
partialCashKnowledge: knowledge.cashKnowledge,
|
|
686
|
+
partialValuationKnowledge: knowledge.valuationKnowledge,
|
|
564
687
|
partialObservedModel: observedModel,
|
|
565
688
|
partialObservedSource: observedSource,
|
|
566
689
|
partialText,
|
|
@@ -598,12 +721,15 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
598
721
|
observed_source: observedSource,
|
|
599
722
|
message: `Reviewer timed out after ${timeoutMs}ms`,
|
|
600
723
|
});
|
|
724
|
+
const knowledge = costKnowledge.snapshot();
|
|
601
725
|
reject(Object.assign(new Error(`Reviewer timed out after ${timeoutMs}ms`), {
|
|
602
726
|
partialCostUsd: costUsd,
|
|
603
727
|
partialCostEstimated: costEstimated,
|
|
604
728
|
partialCashUsd: cashUsd,
|
|
605
729
|
partialValuationUsd: valuationUsd,
|
|
606
730
|
partialUnknownUsd: unknownUsd,
|
|
731
|
+
partialCashKnowledge: knowledge.cashKnowledge,
|
|
732
|
+
partialValuationKnowledge: knowledge.valuationKnowledge,
|
|
607
733
|
partialObservedModel: observedModel,
|
|
608
734
|
partialObservedSource: observedSource,
|
|
609
735
|
partialText,
|
|
@@ -635,12 +761,15 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
635
761
|
});
|
|
636
762
|
}
|
|
637
763
|
if (err && typeof err === "object") {
|
|
764
|
+
const knowledge = costKnowledge.snapshot();
|
|
638
765
|
Object.assign(err, {
|
|
639
766
|
partialCostUsd: costUsd,
|
|
640
767
|
partialCostEstimated: costEstimated,
|
|
641
768
|
partialCashUsd: cashUsd,
|
|
642
769
|
partialValuationUsd: valuationUsd,
|
|
643
770
|
partialUnknownUsd: unknownUsd,
|
|
771
|
+
partialCashKnowledge: knowledge.cashKnowledge,
|
|
772
|
+
partialValuationKnowledge: knowledge.valuationKnowledge,
|
|
644
773
|
partialObservedModel: observedModel,
|
|
645
774
|
partialObservedSource: observedSource,
|
|
646
775
|
partialText,
|
|
@@ -658,126 +787,6 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
658
787
|
});
|
|
659
788
|
}
|
|
660
789
|
}
|
|
661
|
-
function selectReviewerWorkspaceBaseDir(sourceRoot, artifactsBaseDir, sourceEvidenceDir) {
|
|
662
|
-
const durableBase = join(artifactsBaseDir, "workspaces");
|
|
663
|
-
if (!isSameOrInside(sourceRoot, durableBase) && !isSameOrInside(sourceEvidenceDir, durableBase)) {
|
|
664
|
-
return durableBase;
|
|
665
|
-
}
|
|
666
|
-
return join(tmpdir(), `claudexor-review-workspaces-${newId("ws")}`);
|
|
667
|
-
}
|
|
668
|
-
function isTemporaryReviewerWorkspaceBaseDir(baseDir) {
|
|
669
|
-
const resolved = resolve(baseDir);
|
|
670
|
-
const rel = relative(tmpdir(), resolved);
|
|
671
|
-
return (isSameOrInside(tmpdir(), resolved) &&
|
|
672
|
-
rel.split(/[\\/]+/)[0]?.startsWith("claudexor-review-workspaces-") === true);
|
|
673
|
-
}
|
|
674
|
-
async function prepareReviewerWorkspace(input) {
|
|
675
|
-
const sourceRoot = resolve(input.sourceRoot);
|
|
676
|
-
const workspaceBaseDir = resolve(input.workspaceBaseDir);
|
|
677
|
-
const root = join(workspaceBaseDir, input.reviewerDirName);
|
|
678
|
-
if (!existsSync(sourceRoot)) {
|
|
679
|
-
throw new Error(`candidate root does not exist: ${sourceRoot}`);
|
|
680
|
-
}
|
|
681
|
-
if (isSameOrInside(sourceRoot, root)) {
|
|
682
|
-
throw new Error(`reviewer workspace must be outside candidate root: ${root}`);
|
|
683
|
-
}
|
|
684
|
-
try {
|
|
685
|
-
await rm(root, { recursive: true, force: true });
|
|
686
|
-
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
687
|
-
const excludeRoots = input.excludeRoots.map((p) => resolve(p));
|
|
688
|
-
const resolvedSourceRoot = realpathSync(sourceRoot);
|
|
689
|
-
await cp(sourceRoot, root, {
|
|
690
|
-
recursive: true,
|
|
691
|
-
dereference: false,
|
|
692
|
-
filter: (sourcePath) => shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, input.preservePaths),
|
|
693
|
-
});
|
|
694
|
-
const sourceEvidenceDir = resolve(input.sourceEvidenceDir);
|
|
695
|
-
const evidenceDir = join(root, ".claudexor-review-evidence");
|
|
696
|
-
if (existsSync(sourceEvidenceDir)) {
|
|
697
|
-
const resolvedSourceEvidenceDir = realpathSync(sourceEvidenceDir);
|
|
698
|
-
const evidenceExcludeRoots = excludeRoots.filter((root) => !isSameOrInside(root, sourceEvidenceDir));
|
|
699
|
-
await rm(evidenceDir, { recursive: true, force: true });
|
|
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
|
-
});
|
|
707
|
-
}
|
|
708
|
-
await mkdir(evidenceDir, { recursive: true, mode: 0o700 });
|
|
709
|
-
await initializeReviewerWorkspaceGit(root);
|
|
710
|
-
return { root, evidenceDir };
|
|
711
|
-
}
|
|
712
|
-
catch (err) {
|
|
713
|
-
await rm(root, { recursive: true, force: true });
|
|
714
|
-
throw err;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir, preserveBytes = false) {
|
|
718
|
-
const source = resolve(sourceEvidenceDir);
|
|
719
|
-
const target = resolve(persistentEvidenceDir);
|
|
720
|
-
await rm(target, { recursive: true, force: true });
|
|
721
|
-
if (!existsSync(source)) {
|
|
722
|
-
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
723
|
-
return;
|
|
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 });
|
|
731
|
-
const resolvedSource = realpathSync(source);
|
|
732
|
-
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
733
|
-
const sourcePath = join(source, entry.name);
|
|
734
|
-
if (!shouldCopyEvidencePacketPath(source, resolvedSource, sourcePath, target)) {
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
await copyReviewEvidenceEntry(source, resolvedSource, sourcePath, join(target, entry.name), target);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
async function copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, targetPath, targetEvidenceDir) {
|
|
741
|
-
const stat = lstatSync(sourcePath);
|
|
742
|
-
if (stat.isDirectory()) {
|
|
743
|
-
await mkdir(targetPath, { recursive: true, mode: 0o700 });
|
|
744
|
-
for (const entry of await readdir(sourcePath, { withFileTypes: true })) {
|
|
745
|
-
const childSource = join(sourcePath, entry.name);
|
|
746
|
-
if (!shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceDir, childSource, targetEvidenceDir)) {
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
await copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidenceDir, childSource, join(targetPath, entry.name), targetEvidenceDir);
|
|
750
|
-
}
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
if (stat.isFile() && shouldTextSanitizeEvidenceFile(sourcePath)) {
|
|
754
|
-
const raw = readTextSafe(sourcePath);
|
|
755
|
-
if (raw === null)
|
|
756
|
-
throw new Error(`could not read review evidence file: ${sourcePath}`);
|
|
757
|
-
const text = shouldFailClosedEvidenceFile(sourcePath) ? raw : redactSecrets(raw);
|
|
758
|
-
if (containsSecretLikeToken(text)) {
|
|
759
|
-
throw new Error(`review evidence file contains a secret-like token: ${relative(sourceEvidenceDir, sourcePath)}`);
|
|
760
|
-
}
|
|
761
|
-
writeText(targetPath, text);
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
765
|
-
await cp(sourcePath, targetPath, { recursive: false, dereference: false });
|
|
766
|
-
}
|
|
767
|
-
function shouldTextSanitizeEvidenceFile(path) {
|
|
768
|
-
return TEXT_EVIDENCE_SUFFIXES.some((extension) => path.toLowerCase().endsWith(extension));
|
|
769
|
-
}
|
|
770
|
-
function shouldFailClosedEvidenceFile(path) {
|
|
771
|
-
return path.toLowerCase().endsWith(".patch");
|
|
772
|
-
}
|
|
773
|
-
function shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, targetEvidenceDir) {
|
|
774
|
-
const resolvedSourcePath = resolve(sourcePath);
|
|
775
|
-
if (isSameOrInside(resolvedSourcePath, targetEvidenceDir))
|
|
776
|
-
return false;
|
|
777
|
-
if (isSameOrInside(targetEvidenceDir, resolvedSourcePath))
|
|
778
|
-
return false;
|
|
779
|
-
return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [targetEvidenceDir], new Set(), false);
|
|
780
|
-
}
|
|
781
790
|
async function cleanupReviewerWorkspace(workspace, artifact) {
|
|
782
791
|
try {
|
|
783
792
|
await rm(workspace.root, { recursive: true, force: true });
|
|
@@ -790,188 +799,6 @@ async function cleanupReviewerWorkspace(workspace, artifact) {
|
|
|
790
799
|
});
|
|
791
800
|
}
|
|
792
801
|
}
|
|
793
|
-
async function cleanupTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir, artifactsBaseDir) {
|
|
794
|
-
if (!isTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir))
|
|
795
|
-
return;
|
|
796
|
-
try {
|
|
797
|
-
await rm(workspaceBaseDir, { recursive: true, force: true });
|
|
798
|
-
}
|
|
799
|
-
catch (err) {
|
|
800
|
-
try {
|
|
801
|
-
writeJson(join(artifactsBaseDir, "reviewer-workspace-base-cleanup-error.json"), {
|
|
802
|
-
reviewer_workspace_base_cleanup: "failed",
|
|
803
|
-
workspace_base_dir: workspaceBaseDir,
|
|
804
|
-
error: redactSecrets(err instanceof Error ? err.message : String(err)),
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
catch {
|
|
808
|
-
// Do not let cleanup telemetry hide the review result or the original error.
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set(), enforceContentPolicy = true) {
|
|
813
|
-
const resolvedSourcePath = resolve(sourcePath);
|
|
814
|
-
if (!isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, resolvedSourcePath, excludeRoots)) {
|
|
815
|
-
return false;
|
|
816
|
-
}
|
|
817
|
-
if (excludeRoots.some((root) => isSameOrInside(root, resolvedSourcePath)))
|
|
818
|
-
return false;
|
|
819
|
-
const rel = relative(sourceRoot, resolvedSourcePath);
|
|
820
|
-
if (!rel)
|
|
821
|
-
return true;
|
|
822
|
-
const parts = rel.split(/[\\/]+/);
|
|
823
|
-
if (sensitiveResourcePolicy.classifyPath(rel).sensitive) {
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
if (parts[0] === ".claudexor") {
|
|
827
|
-
return (isCopyableReviewerClaudexorPath(rel, parts, preservePaths) &&
|
|
828
|
-
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
829
|
-
}
|
|
830
|
-
if (parts.some((part) => [".git", ".adversarial-review", ".turbo", "node_modules"].includes(part))) {
|
|
831
|
-
return false;
|
|
832
|
-
}
|
|
833
|
-
if (parts.some((part) => [".next", ".cache", "coverage", "dist"].includes(part)) &&
|
|
834
|
-
!isPreservedReviewerPath(rel, preservePaths)) {
|
|
835
|
-
return false;
|
|
836
|
-
}
|
|
837
|
-
return (!rel.endsWith(".tsbuildinfo") &&
|
|
838
|
-
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
839
|
-
}
|
|
840
|
-
function reviewerFileContentAllowed(path) {
|
|
841
|
-
let targetStat;
|
|
842
|
-
try {
|
|
843
|
-
targetStat = statSync(path);
|
|
844
|
-
}
|
|
845
|
-
catch {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
if (!targetStat.isFile())
|
|
849
|
-
return true;
|
|
850
|
-
const content = readTextSafe(path);
|
|
851
|
-
return content !== null && !sensitiveResourcePolicy.containsSensitiveContent(content);
|
|
852
|
-
}
|
|
853
|
-
function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
|
|
854
|
-
if (parts.length === 1) {
|
|
855
|
-
return true;
|
|
856
|
-
}
|
|
857
|
-
if (parts.length === 2 && parts[1] === "config.yaml") {
|
|
858
|
-
return true;
|
|
859
|
-
}
|
|
860
|
-
const runtimeRoot = parts[1]?.toLowerCase();
|
|
861
|
-
if (runtimeRoot && BLOCKED_REVIEWER_RUNTIME_ROOTS.has(runtimeRoot))
|
|
862
|
-
return false;
|
|
863
|
-
return isPreservedReviewerPath(rel, preservePaths);
|
|
864
|
-
}
|
|
865
|
-
function isPreservedReviewerPath(rel, preservePaths) {
|
|
866
|
-
const normalized = normalizeReviewerRelativePath(rel);
|
|
867
|
-
if (!normalized)
|
|
868
|
-
return false;
|
|
869
|
-
if (preservePaths.has(normalized))
|
|
870
|
-
return true;
|
|
871
|
-
const prefix = `${normalized}/`;
|
|
872
|
-
for (const preserved of preservePaths) {
|
|
873
|
-
if (preserved.startsWith(prefix))
|
|
874
|
-
return true;
|
|
875
|
-
}
|
|
876
|
-
return false;
|
|
877
|
-
}
|
|
878
|
-
export function __testExtractDiffTouchedPaths(diff) {
|
|
879
|
-
return extractDiffTouchedPaths(diff);
|
|
880
|
-
}
|
|
881
|
-
function extractDiffTouchedPaths(diff) {
|
|
882
|
-
const paths = new Set();
|
|
883
|
-
for (const file of parseUnifiedDiff(diff).files) {
|
|
884
|
-
if (file.oldPath)
|
|
885
|
-
addReviewerPreservePath(paths, file.oldPath);
|
|
886
|
-
if (file.newPath)
|
|
887
|
-
addReviewerPreservePath(paths, file.newPath);
|
|
888
|
-
}
|
|
889
|
-
return paths;
|
|
890
|
-
}
|
|
891
|
-
function addReviewerPreservePath(paths, value) {
|
|
892
|
-
const normalized = normalizeReviewerRelativePath(value);
|
|
893
|
-
if (normalized)
|
|
894
|
-
paths.add(normalized);
|
|
895
|
-
}
|
|
896
|
-
function normalizeReviewerRelativePath(value) {
|
|
897
|
-
if (!value || value === "/dev/null" || isAbsolute(value))
|
|
898
|
-
return null;
|
|
899
|
-
const normalized = normalize(value).replace(/\\/g, "/");
|
|
900
|
-
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
901
|
-
return null;
|
|
902
|
-
}
|
|
903
|
-
return normalized;
|
|
904
|
-
}
|
|
905
|
-
function isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots) {
|
|
906
|
-
let stat;
|
|
907
|
-
try {
|
|
908
|
-
stat = lstatSync(sourcePath);
|
|
909
|
-
}
|
|
910
|
-
catch {
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
913
|
-
if (!stat.isSymbolicLink())
|
|
914
|
-
return true;
|
|
915
|
-
let linkTarget = "";
|
|
916
|
-
let resolvedTarget = "";
|
|
917
|
-
let targetKind = "other";
|
|
918
|
-
try {
|
|
919
|
-
linkTarget = readlinkSync(sourcePath);
|
|
920
|
-
resolvedTarget = realpathSync(sourcePath);
|
|
921
|
-
const targetStat = statSync(sourcePath);
|
|
922
|
-
targetKind = targetStat.isDirectory() ? "directory" : targetStat.isFile() ? "file" : "other";
|
|
923
|
-
}
|
|
924
|
-
catch {
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
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;
|
|
938
|
-
}
|
|
939
|
-
async function initializeReviewerWorkspaceGit(root) {
|
|
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"]);
|
|
949
|
-
await runGitOrThrow("commit", root, [
|
|
950
|
-
...noHooks,
|
|
951
|
-
"commit",
|
|
952
|
-
"--allow-empty",
|
|
953
|
-
"--no-verify",
|
|
954
|
-
"--no-gpg-sign",
|
|
955
|
-
"-m",
|
|
956
|
-
"review baseline",
|
|
957
|
-
]);
|
|
958
|
-
}
|
|
959
|
-
async function runGitOrThrow(label, cwd, args) {
|
|
960
|
-
const gitEnv = Object.fromEntries(Object.keys(process.env)
|
|
961
|
-
.filter((key) => key.startsWith("GIT_"))
|
|
962
|
-
.map((key) => [key, null]));
|
|
963
|
-
gitEnv.GIT_CONFIG_NOSYSTEM = "1";
|
|
964
|
-
const result = await runCapture("git", args, { cwd, env: gitEnv, timeoutMs: 60_000 });
|
|
965
|
-
if (result.code === 0)
|
|
966
|
-
return;
|
|
967
|
-
const detail = redactSecrets((result.stderr || result.stdout || `exit ${result.code}`).trim());
|
|
968
|
-
throw new Error(`failed to prepare reviewer workspace (${label}): ${detail}`);
|
|
969
|
-
}
|
|
970
|
-
function isSameOrInside(parent, target) {
|
|
971
|
-
const rel = relative(resolve(parent), resolve(target));
|
|
972
|
-
const firstPart = rel.split(/[\\/]+/)[0];
|
|
973
|
-
return rel === "" || (!!rel && firstPart !== ".." && !isAbsolute(rel));
|
|
974
|
-
}
|
|
975
802
|
function createReviewerArtifactContext(baseDir, index, reviewer) {
|
|
976
803
|
const dir = join(baseDir, `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`);
|
|
977
804
|
ensureDir(dir);
|
|
@@ -1000,8 +827,11 @@ function createReviewerArtifactContext(baseDir, index, reviewer) {
|
|
|
1000
827
|
return ctx;
|
|
1001
828
|
}
|
|
1002
829
|
function safeFilePart(value) {
|
|
1003
|
-
const safe = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(
|
|
1004
|
-
|
|
830
|
+
const safe = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+/, "");
|
|
831
|
+
let end = safe.length;
|
|
832
|
+
while (end > 0 && safe[end - 1] === "-")
|
|
833
|
+
end -= 1;
|
|
834
|
+
return safe.slice(0, end) || "reviewer";
|
|
1005
835
|
}
|
|
1006
836
|
function emitReviewerProgress(artifact, reviewer, onReviewerEvent, patch) {
|
|
1007
837
|
const event = {
|