@claudexor/review 1.0.0 → 2.1.2
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/convergence.d.ts.map +1 -1
- package/dist/convergence.js +4 -2
- package/dist/convergence.js.map +1 -1
- package/dist/findings.d.ts.map +1 -1
- package/dist/findings.js.map +1 -1
- package/dist/gates.d.ts +11 -2
- package/dist/gates.d.ts.map +1 -1
- package/dist/gates.js +126 -5
- package/dist/gates.js.map +1 -1
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +5 -1
- package/dist/readiness.js.map +1 -1
- package/dist/reviewEngine.d.ts +7 -20
- package/dist/reviewEngine.d.ts.map +1 -1
- package/dist/reviewEngine.js +308 -373
- package/dist/reviewEngine.js.map +1 -1
- package/dist/reviewPrompt.d.ts +3 -0
- package/dist/reviewPrompt.d.ts.map +1 -0
- package/dist/reviewPrompt.js +36 -0
- package/dist/reviewPrompt.js.map +1 -0
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +5 -1
- package/dist/route.js.map +1 -1
- package/package.json +6 -5
package/dist/reviewEngine.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { parseUnifiedDiff, runCapture } from "@claudexor/core";
|
|
2
2
|
import { preflightEvidence, writeDiffEvidence } from "@claudexor/context";
|
|
3
3
|
import { FallbackReason, HarnessRunSpec, ReviewFinding as ReviewFindingSchema, } from "@claudexor/schema";
|
|
4
|
-
import { existsSync, lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
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";
|
|
11
12
|
const DEFAULT_REVIEWER_TIMEOUT_MS = 10 * 60_000;
|
|
12
13
|
const DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY = {
|
|
@@ -14,41 +15,81 @@ const DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY = {
|
|
|
14
15
|
initialDelayMs: 1_000,
|
|
15
16
|
maxDelayMs: 10_000,
|
|
16
17
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
const BLOCKED_REVIEWER_RUNTIME_ROOTS = new Set("auth cache daemon home homes logs runs secrets state tmp workspaces".split(" "));
|
|
19
|
+
const TEXT_EVIDENCE_SUFFIXES = [".md", ".txt", ".json", ".yaml", ".yml", ".patch"];
|
|
20
|
+
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;
|
|
21
|
+
function readExistingDiffEvidence(dir, diff) {
|
|
22
|
+
const diffText = diff.endsWith("\n") ? diff : `${diff}\n`;
|
|
23
|
+
const summaryPath = join(dir, "DIFF_SUMMARY.md");
|
|
24
|
+
const summary = readTextSafe(summaryPath);
|
|
25
|
+
if (summary === null)
|
|
26
|
+
throw new Error("sealed review packet is missing DIFF_SUMMARY.md");
|
|
27
|
+
return {
|
|
28
|
+
diffPath: join(dir, "DIFF.patch"),
|
|
29
|
+
summaryPath,
|
|
30
|
+
diffSha256: sha256(diffText),
|
|
31
|
+
summary,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function reviewerRouteProof(reviewer, modelId, source, peerFamilies) {
|
|
35
|
+
return buildRouteProof({
|
|
36
|
+
harness_id: reviewer.adapter.id,
|
|
37
|
+
provider_family: reviewer.providerFamily,
|
|
38
|
+
model_hint: reviewer.requestedModel ?? null,
|
|
39
|
+
}, {
|
|
40
|
+
provider: reviewer.providerFamily,
|
|
41
|
+
model_id: modelId,
|
|
42
|
+
evidence_source: modelId ? source : "unavailable",
|
|
43
|
+
}, peerFamilies);
|
|
44
|
+
}
|
|
45
|
+
function reviewerInfo(reviewer, routeProofStatus, observedModel = null) {
|
|
46
|
+
return {
|
|
47
|
+
harness_id: reviewer.adapter.id,
|
|
48
|
+
requested_model: reviewer.requestedModel ?? null,
|
|
49
|
+
requested_effort: reviewer.requestedEffort ?? null,
|
|
50
|
+
observed_model: observedModel,
|
|
51
|
+
route_proof_status: routeProofStatus,
|
|
52
|
+
};
|
|
33
53
|
}
|
|
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
54
|
export async function reviewCandidate(input) {
|
|
40
55
|
const findingsByReviewer = input.reviewers.map(() => []);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
56
|
+
const reviewerFamilies = input.reviewers.map((reviewer) => reviewer.providerFamily);
|
|
57
|
+
const routeProofs = input.reviewers.map((reviewer, index) => reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies.filter((_, otherIndex) => otherIndex !== index)));
|
|
58
|
+
const reviewerRequests = input.reviewers.map((reviewer) => ({
|
|
59
|
+
harness_id: reviewer.adapter.id,
|
|
60
|
+
provider_family: reviewer.providerFamily,
|
|
61
|
+
requested_model: reviewer.requestedModel ?? null,
|
|
62
|
+
requested_effort: reviewer.requestedEffort ?? null,
|
|
63
|
+
}));
|
|
44
64
|
const healthyReviewerIndexes = new Set();
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
const reviewSpendByReviewer = input.reviewers.map(() => 0);
|
|
66
|
+
const reviewSpendEstimatedByReviewer = input.reviewers.map(() => false);
|
|
47
67
|
const reviewerTimeoutMs = input.reviewerTimeoutMs ?? DEFAULT_REVIEWER_TIMEOUT_MS;
|
|
68
|
+
const reviewWaveId = input.env?.["CLAUDEXOR_REVIEW_WAVE_ID"] ?? process.env["CLAUDEXOR_REVIEW_WAVE_ID"] ?? null;
|
|
69
|
+
if (input.evidenceReadOnly && input.frozenIdentity && !REVIEW_WAVE_ID.test(reviewWaveId ?? "")) {
|
|
70
|
+
throw new Error("sealed release review requires CLAUDEXOR_REVIEW_WAVE_ID UUID");
|
|
71
|
+
}
|
|
72
|
+
const frozenMetadata = input.frozenIdentity
|
|
73
|
+
? {
|
|
74
|
+
candidate_sha: input.frozenIdentity.candidateSha,
|
|
75
|
+
candidate_tree: input.frozenIdentity.candidateTree,
|
|
76
|
+
packet_manifest_sha256: input.frozenIdentity.packetManifestSha256,
|
|
77
|
+
...(reviewWaveId ? { review_wave_id: reviewWaveId } : {}),
|
|
78
|
+
}
|
|
79
|
+
: {};
|
|
48
80
|
if (containsSecretLikeToken(input.diff || "(empty diff)\n")) {
|
|
49
81
|
throw new Error("diff evidence contains a secret-like token; refusing to persist raw DIFF.patch");
|
|
50
82
|
}
|
|
51
|
-
|
|
83
|
+
if (input.evidenceReadOnly) {
|
|
84
|
+
const packetDiff = readTextSafe(join(input.evidenceDir, "DIFF.patch"));
|
|
85
|
+
const normalizedDiff = input.diff.endsWith("\n") ? input.diff : `${input.diff}\n`;
|
|
86
|
+
if (packetDiff === null || packetDiff !== normalizedDiff) {
|
|
87
|
+
throw new Error("sealed review packet DIFF.patch does not match the verified review diff");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
writeDiffEvidence(input.evidenceDir, input.diff);
|
|
92
|
+
}
|
|
52
93
|
const preflight = preflightEvidence(input.evidenceDir);
|
|
53
94
|
if (!preflight.ok) {
|
|
54
95
|
const parts = [
|
|
@@ -60,69 +101,70 @@ export async function reviewCandidate(input) {
|
|
|
60
101
|
const artifactsBaseDir = input.artifactsDir ?? join(input.evidenceDir, "reviewer-artifacts");
|
|
61
102
|
ensureDir(artifactsBaseDir);
|
|
62
103
|
const persistentEvidenceDir = join(artifactsBaseDir, "evidence");
|
|
63
|
-
await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir);
|
|
64
|
-
const persistentPatch =
|
|
65
|
-
|
|
104
|
+
await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir, input.evidenceReadOnly === true);
|
|
105
|
+
const persistentPatch = input.evidenceReadOnly
|
|
106
|
+
? readExistingDiffEvidence(persistentEvidenceDir, input.diff)
|
|
107
|
+
: writeDiffEvidence(persistentEvidenceDir, input.diff);
|
|
108
|
+
writeJson(input.evidenceReadOnly
|
|
109
|
+
? join(artifactsBaseDir, "evidence-metadata.json")
|
|
110
|
+
: join(persistentEvidenceDir, "metadata.json"), {
|
|
66
111
|
source_evidence_dir: input.evidenceDir,
|
|
67
112
|
candidate_root: input.cwd,
|
|
68
113
|
persistent_evidence_dir: persistentEvidenceDir,
|
|
69
114
|
diff_path: persistentPatch.diffPath,
|
|
70
115
|
summary_path: persistentPatch.summaryPath,
|
|
71
116
|
diff_sha256: persistentPatch.diffSha256,
|
|
117
|
+
...frozenMetadata,
|
|
72
118
|
});
|
|
73
|
-
const artifacts =
|
|
74
|
-
const reviewerFamilies = input.reviewers.map((r) => r.providerFamily);
|
|
119
|
+
const artifacts = input.reviewers.map(() => undefined);
|
|
75
120
|
const preservePaths = extractDiffTouchedPaths(input.diff);
|
|
76
121
|
const reviewerWorkspaceBaseDir = selectReviewerWorkspaceBaseDir(input.cwd, artifactsBaseDir, input.evidenceDir);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
122
|
+
const runReviewer = async (reviewer, index) => {
|
|
123
|
+
if (input.signal?.aborted)
|
|
124
|
+
return;
|
|
125
|
+
const artifact = createReviewerArtifactContext(artifactsBaseDir, index, reviewer);
|
|
126
|
+
artifacts[index] = artifact;
|
|
127
|
+
let reviewerWorkspace = null;
|
|
128
|
+
let spec = null;
|
|
129
|
+
try {
|
|
130
|
+
reviewerWorkspace = await prepareReviewerWorkspace({
|
|
131
|
+
sourceRoot: input.cwd,
|
|
132
|
+
sourceEvidenceDir: persistentEvidenceDir,
|
|
133
|
+
workspaceBaseDir: reviewerWorkspaceBaseDir,
|
|
134
|
+
reviewerDirName: `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`,
|
|
135
|
+
excludeRoots: [artifactsBaseDir],
|
|
136
|
+
preservePaths,
|
|
137
|
+
preserveEvidenceBytes: input.evidenceReadOnly === true,
|
|
86
138
|
});
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
reviewerWorkspace
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
139
|
+
const reviewerPatch = input.evidenceReadOnly
|
|
140
|
+
? readExistingDiffEvidence(reviewerWorkspace.evidenceDir, input.diff)
|
|
141
|
+
: writeDiffEvidence(reviewerWorkspace.evidenceDir, input.diff);
|
|
142
|
+
updateReviewerMetadata(artifact, {
|
|
143
|
+
candidate_evidence_dir: reviewerWorkspace.evidenceDir,
|
|
144
|
+
candidate_root: reviewerWorkspace.root,
|
|
145
|
+
source_candidate_evidence_dir: input.evidenceDir,
|
|
146
|
+
source_candidate_root: input.cwd,
|
|
147
|
+
reviewer_workspace_root: reviewerWorkspace.root,
|
|
148
|
+
persistent_evidence_dir: persistentEvidenceDir,
|
|
149
|
+
persistent_diff_path: persistentPatch.diffPath,
|
|
150
|
+
persistent_summary_path: persistentPatch.summaryPath,
|
|
151
|
+
diff_sha256: persistentPatch.diffSha256,
|
|
152
|
+
...frozenMetadata,
|
|
153
|
+
});
|
|
154
|
+
const runtimePrompt = buildReviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch, input.evidenceReadOnly === true);
|
|
155
|
+
spec = HarnessRunSpec.parse({
|
|
156
|
+
session_id: newId("rev"),
|
|
157
|
+
intent: "review",
|
|
158
|
+
prompt: runtimePrompt,
|
|
159
|
+
cwd: reviewerWorkspace.root,
|
|
160
|
+
access: "readonly",
|
|
161
|
+
model_hint: reviewer.requestedModel ?? null,
|
|
162
|
+
effort_hint: reviewer.requestedEffort ?? null,
|
|
163
|
+
auth_preference: reviewer.authPreference ?? "auto",
|
|
164
|
+
env_inheritance: input.envInheritance ?? "mirror_native",
|
|
165
|
+
...(input.env ? { env: input.env } : {}),
|
|
166
|
+
});
|
|
167
|
+
writeText(artifact.promptPath, redactSecrets(`Persistent local replay evidence:
|
|
126
168
|
- evidence_dir: ${persistentEvidenceDir}
|
|
127
169
|
- candidate_root: ${reviewerWorkspace.root}
|
|
128
170
|
- source_candidate_root: ${input.cwd}
|
|
@@ -134,172 +176,124 @@ Runtime prompt used during review follows. Its candidate-tree paths may be trans
|
|
|
134
176
|
|
|
135
177
|
${runtimePrompt}
|
|
136
178
|
`));
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 {
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
const failedAt = nowIso();
|
|
182
|
+
const message = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
183
|
+
updateReviewerMetadata(artifact, {
|
|
184
|
+
status: "failed",
|
|
185
|
+
failure_time: failedAt,
|
|
186
|
+
error: `reviewer setup failed: ${message}`,
|
|
187
|
+
});
|
|
188
|
+
writeParseError(artifact, { error: `reviewer setup failed: ${message}` });
|
|
189
|
+
emitReviewerProgress(artifact, reviewer, input.onReviewerEvent, {
|
|
190
|
+
type: "reviewer.failed",
|
|
191
|
+
at: failedAt,
|
|
192
|
+
duration_ms: 0,
|
|
193
|
+
message: `Reviewer setup failed: ${message}`,
|
|
194
|
+
});
|
|
195
|
+
if (reviewerWorkspace)
|
|
228
196
|
await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
|
|
197
|
+
const proof = reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies);
|
|
198
|
+
routeProofs[index] = proof;
|
|
199
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(reviewerInfo(reviewer, proof.status), `Reviewer setup failed: ${message}`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!reviewerWorkspace || !spec)
|
|
203
|
+
return;
|
|
204
|
+
let text = "";
|
|
205
|
+
let streamObservedModel;
|
|
206
|
+
let routeModel;
|
|
207
|
+
let routeSource = "unavailable";
|
|
208
|
+
let reviewerError = null;
|
|
209
|
+
try {
|
|
210
|
+
const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.transientRetryPolicy ?? DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY, artifact, input.onReviewerEvent, input.signal);
|
|
211
|
+
text = out.text;
|
|
212
|
+
streamObservedModel = out.observedModel;
|
|
213
|
+
routeModel = out.observedModel;
|
|
214
|
+
routeSource = out.observedSource;
|
|
215
|
+
reviewSpendByReviewer[index] = out.costUsd;
|
|
216
|
+
reviewSpendEstimatedByReviewer[index] = out.costEstimated;
|
|
217
|
+
if (!routeModel && reviewer.requestedModel) {
|
|
218
|
+
routeModel = reviewer.requestedModel;
|
|
219
|
+
routeSource = "metadata";
|
|
229
220
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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).`));
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
reviewerError = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
224
|
+
const partial = err;
|
|
225
|
+
if (typeof partial?.partialText === "string" && partial.partialText.trim() !== "") {
|
|
226
|
+
text = partial.partialText;
|
|
282
227
|
}
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
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}`));
|
|
228
|
+
if (partial && typeof partial.partialCostUsd === "number" && partial.partialCostUsd > 0) {
|
|
229
|
+
reviewSpendByReviewer[index] = partial.partialCostUsd;
|
|
230
|
+
reviewSpendEstimatedByReviewer[index] = partial.partialCostEstimated === true;
|
|
293
231
|
}
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
232
|
+
if (partial?.partialObservedModel) {
|
|
233
|
+
streamObservedModel = partial.partialObservedModel;
|
|
234
|
+
routeModel = partial.partialObservedModel;
|
|
235
|
+
routeSource = partial.partialObservedSource ?? "stream_event";
|
|
297
236
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
237
|
+
writeParseError(artifact, { error: reviewerError });
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
|
|
241
|
+
}
|
|
242
|
+
const proof = reviewerRouteProof(reviewer, routeModel ?? null, routeSource, reviewerFamilies.filter((_, i) => i !== index));
|
|
243
|
+
routeProofs[index] = proof;
|
|
244
|
+
const info = reviewerInfo(reviewer, proof.status, streamObservedModel ?? null);
|
|
245
|
+
const jsonBlocks = extractJsonBlocks(text);
|
|
246
|
+
writeJson(artifact.parsedPath, redactValue(jsonBlocks));
|
|
247
|
+
if (reviewerError && (text.trim() === "" || jsonBlocks.length === 0)) {
|
|
248
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer failed: ${reviewerError}`));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (text.trim() === "" || jsonBlocks.length === 0) {
|
|
252
|
+
writeParseError(artifact, { error: "no_parseable_json", text_sha256: sha256(text) });
|
|
253
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, "Reviewer produced no parseable JSON findings."));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const parsed = parseFindingsDetailed(text, info);
|
|
257
|
+
const parseError = {};
|
|
258
|
+
let parsedFindingsRecorded = false;
|
|
259
|
+
const recordParsedFindings = () => {
|
|
260
|
+
if (parsedFindingsRecorded)
|
|
261
|
+
return;
|
|
301
262
|
findingsByReviewer[index]?.push(...parsed.findings);
|
|
263
|
+
parsedFindingsRecorded = true;
|
|
264
|
+
};
|
|
265
|
+
if (parsed.malformed > 0) {
|
|
266
|
+
Object.assign(parseError, {
|
|
267
|
+
error: "malformed_findings",
|
|
268
|
+
malformed: parsed.malformed,
|
|
269
|
+
text_sha256: sha256(text),
|
|
270
|
+
});
|
|
271
|
+
recordParsedFindings();
|
|
272
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer produced ${parsed.malformed} malformed finding item(s).`));
|
|
273
|
+
}
|
|
274
|
+
if (reviewerError) {
|
|
275
|
+
Object.assign(parseError, {
|
|
276
|
+
error: reviewerError,
|
|
277
|
+
recovered_json_blocks: jsonBlocks.length,
|
|
278
|
+
text_sha256: sha256(text),
|
|
279
|
+
});
|
|
280
|
+
recordParsedFindings();
|
|
281
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, parsed.findings.length === 0
|
|
282
|
+
? `Reviewer failed after parseable JSON with no findings: ${reviewerError}`
|
|
283
|
+
: `Reviewer failed after parseable JSON output: ${reviewerError}`));
|
|
302
284
|
}
|
|
285
|
+
if (Object.keys(parseError).length > 0) {
|
|
286
|
+
writeParseError(artifact, parseError);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
healthyReviewerIndexes.add(index);
|
|
290
|
+
findingsByReviewer[index]?.push(...parsed.findings);
|
|
291
|
+
};
|
|
292
|
+
try {
|
|
293
|
+
const reviewerRuns = await Promise.allSettled(input.reviewers.map((reviewer, index) => runReviewer(reviewer, index)));
|
|
294
|
+
const failedRun = reviewerRuns.find((run) => run.status === "rejected");
|
|
295
|
+
if (failedRun)
|
|
296
|
+
throw failedRun.reason;
|
|
303
297
|
const classifiedProofs = classifyDiversity(routeProofs);
|
|
304
298
|
for (const [index, proof] of classifiedProofs.entries()) {
|
|
305
299
|
const artifact = artifacts[index];
|
|
@@ -321,12 +315,12 @@ ${runtimePrompt}
|
|
|
321
315
|
});
|
|
322
316
|
});
|
|
323
317
|
});
|
|
324
|
-
const healthyProviders = [
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
318
|
+
const healthyProviders = [
|
|
319
|
+
...new Set(input.reviewers
|
|
320
|
+
.filter((_, index) => healthyReviewerIndexes.has(index))
|
|
321
|
+
.map((reviewer) => reviewer.providerFamily)
|
|
322
|
+
.filter((family) => family !== "unknown")),
|
|
323
|
+
];
|
|
330
324
|
const observedFamilies = [
|
|
331
325
|
...new Set(classifiedProofs
|
|
332
326
|
.filter((p, index) => p.status === "verified" && healthyReviewerIndexes.has(index))
|
|
@@ -341,8 +335,8 @@ ${runtimePrompt}
|
|
|
341
335
|
healthyProviders,
|
|
342
336
|
crossFamilyVerified: observedFamilies.length >= 2,
|
|
343
337
|
distinctProviders: observedFamilies,
|
|
344
|
-
reviewSpendUsd,
|
|
345
|
-
reviewSpendEstimated,
|
|
338
|
+
reviewSpendUsd: reviewSpendByReviewer.reduce((sum, spend) => sum + spend, 0),
|
|
339
|
+
reviewSpendEstimated: reviewSpendEstimatedByReviewer.some(Boolean),
|
|
346
340
|
};
|
|
347
341
|
}
|
|
348
342
|
finally {
|
|
@@ -376,9 +370,6 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
376
370
|
let firstEventTime = null;
|
|
377
371
|
let observedModel;
|
|
378
372
|
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
373
|
let costUsd = 0;
|
|
383
374
|
let costEstimated = false;
|
|
384
375
|
let partialText = "";
|
|
@@ -506,13 +497,12 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
506
497
|
text,
|
|
507
498
|
observedModel: attemptObservedModel,
|
|
508
499
|
observedSource: attemptObservedSource,
|
|
509
|
-
artifactDir: artifact.dir,
|
|
510
500
|
costUsd,
|
|
511
501
|
costEstimated,
|
|
512
502
|
};
|
|
513
503
|
};
|
|
514
504
|
const consume = consumeOnce(0);
|
|
515
|
-
|
|
505
|
+
let removeExternalAbortListener = () => { };
|
|
516
506
|
const cancelled = new Promise((_, reject) => {
|
|
517
507
|
if (!signal)
|
|
518
508
|
return;
|
|
@@ -534,7 +524,7 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
534
524
|
queueMicrotask(onAbort);
|
|
535
525
|
else
|
|
536
526
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
537
|
-
|
|
527
|
+
removeExternalAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
538
528
|
});
|
|
539
529
|
const timed = new Promise((_, reject) => {
|
|
540
530
|
timeout = setTimeout(() => {
|
|
@@ -610,9 +600,7 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
610
600
|
settled = true;
|
|
611
601
|
if (timeout)
|
|
612
602
|
clearTimeout(timeout);
|
|
613
|
-
|
|
614
|
-
removeExternalAbortListener();
|
|
615
|
-
}
|
|
603
|
+
removeExternalAbortListener();
|
|
616
604
|
consume.catch(() => {
|
|
617
605
|
/* timeout path: consume may reject after the race already returned */
|
|
618
606
|
});
|
|
@@ -665,11 +653,13 @@ async function prepareReviewerWorkspace(input) {
|
|
|
665
653
|
const resolvedSourceEvidenceDir = realpathSync(sourceEvidenceDir);
|
|
666
654
|
const evidenceExcludeRoots = excludeRoots.filter((root) => !isSameOrInside(root, sourceEvidenceDir));
|
|
667
655
|
await rm(evidenceDir, { recursive: true, force: true });
|
|
668
|
-
await cp(sourceEvidenceDir, evidenceDir,
|
|
669
|
-
recursive: true,
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
656
|
+
await cp(sourceEvidenceDir, evidenceDir, input.preserveEvidenceBytes
|
|
657
|
+
? { recursive: true, dereference: false }
|
|
658
|
+
: {
|
|
659
|
+
recursive: true,
|
|
660
|
+
dereference: false,
|
|
661
|
+
filter: (sourcePath) => shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, evidenceExcludeRoots),
|
|
662
|
+
});
|
|
673
663
|
}
|
|
674
664
|
await mkdir(evidenceDir, { recursive: true, mode: 0o700 });
|
|
675
665
|
await initializeReviewerWorkspaceGit(root);
|
|
@@ -680,14 +670,20 @@ async function prepareReviewerWorkspace(input) {
|
|
|
680
670
|
throw err;
|
|
681
671
|
}
|
|
682
672
|
}
|
|
683
|
-
async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir) {
|
|
673
|
+
async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir, preserveBytes = false) {
|
|
684
674
|
const source = resolve(sourceEvidenceDir);
|
|
685
675
|
const target = resolve(persistentEvidenceDir);
|
|
686
676
|
await rm(target, { recursive: true, force: true });
|
|
687
|
-
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
688
677
|
if (!existsSync(source)) {
|
|
678
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
689
679
|
return;
|
|
690
680
|
}
|
|
681
|
+
if (preserveBytes) {
|
|
682
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
683
|
+
await cp(source, target, { recursive: true, dereference: false });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
691
687
|
const resolvedSource = realpathSync(source);
|
|
692
688
|
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
693
689
|
const sourcePath = join(source, entry.name);
|
|
@@ -725,13 +721,7 @@ async function copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidence
|
|
|
725
721
|
await cp(sourcePath, targetPath, { recursive: false, dereference: false });
|
|
726
722
|
}
|
|
727
723
|
function shouldTextSanitizeEvidenceFile(path) {
|
|
728
|
-
|
|
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"));
|
|
724
|
+
return TEXT_EVIDENCE_SUFFIXES.some((extension) => path.toLowerCase().endsWith(extension));
|
|
735
725
|
}
|
|
736
726
|
function shouldFailClosedEvidenceFile(path) {
|
|
737
727
|
return path.toLowerCase().endsWith(".patch");
|
|
@@ -742,9 +732,7 @@ function shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceD
|
|
|
742
732
|
return false;
|
|
743
733
|
if (isSameOrInside(targetEvidenceDir, resolvedSourcePath))
|
|
744
734
|
return false;
|
|
745
|
-
return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [
|
|
746
|
-
targetEvidenceDir,
|
|
747
|
-
]);
|
|
735
|
+
return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [targetEvidenceDir], new Set(), false);
|
|
748
736
|
}
|
|
749
737
|
async function cleanupReviewerWorkspace(workspace, artifact) {
|
|
750
738
|
try {
|
|
@@ -777,7 +765,7 @@ async function cleanupTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir, artifa
|
|
|
777
765
|
}
|
|
778
766
|
}
|
|
779
767
|
}
|
|
780
|
-
function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set()) {
|
|
768
|
+
function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set(), enforceContentPolicy = true) {
|
|
781
769
|
const resolvedSourcePath = resolve(sourcePath);
|
|
782
770
|
if (!isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, resolvedSourcePath, excludeRoots)) {
|
|
783
771
|
return false;
|
|
@@ -788,55 +776,35 @@ function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excl
|
|
|
788
776
|
if (!rel)
|
|
789
777
|
return true;
|
|
790
778
|
const parts = rel.split(/[\\/]+/);
|
|
791
|
-
if (
|
|
779
|
+
if (sensitiveResourcePolicy.classifyPath(rel).sensitive) {
|
|
792
780
|
return false;
|
|
793
781
|
}
|
|
794
782
|
if (parts[0] === ".claudexor") {
|
|
795
|
-
return isCopyableReviewerClaudexorPath(rel, parts, preservePaths)
|
|
783
|
+
return (isCopyableReviewerClaudexorPath(rel, parts, preservePaths) &&
|
|
784
|
+
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
796
785
|
}
|
|
797
|
-
if (parts.some((part) => [
|
|
798
|
-
".git",
|
|
799
|
-
".adversarial-review",
|
|
800
|
-
".turbo",
|
|
801
|
-
"node_modules",
|
|
802
|
-
].includes(part))) {
|
|
786
|
+
if (parts.some((part) => [".git", ".adversarial-review", ".turbo", "node_modules"].includes(part))) {
|
|
803
787
|
return false;
|
|
804
788
|
}
|
|
805
789
|
if (parts.some((part) => [".next", ".cache", "coverage", "dist"].includes(part)) &&
|
|
806
790
|
!isPreservedReviewerPath(rel, preservePaths)) {
|
|
807
791
|
return false;
|
|
808
792
|
}
|
|
809
|
-
return !rel.endsWith(".tsbuildinfo")
|
|
793
|
+
return (!rel.endsWith(".tsbuildinfo") &&
|
|
794
|
+
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
810
795
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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;
|
|
796
|
+
function reviewerFileContentAllowed(path) {
|
|
797
|
+
let targetStat;
|
|
798
|
+
try {
|
|
799
|
+
targetStat = statSync(path);
|
|
831
800
|
}
|
|
832
|
-
|
|
833
|
-
return
|
|
834
|
-
|
|
801
|
+
catch {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
if (!targetStat.isFile())
|
|
835
805
|
return true;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
function isSafeEnvTemplateName(lower) {
|
|
839
|
-
return [".env.example", ".env.sample", ".env.template"].includes(lower);
|
|
806
|
+
const content = readTextSafe(path);
|
|
807
|
+
return content !== null && !sensitiveResourcePolicy.containsSensitiveContent(content);
|
|
840
808
|
}
|
|
841
809
|
function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
|
|
842
810
|
if (parts.length === 1) {
|
|
@@ -846,22 +814,8 @@ function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
|
|
|
846
814
|
return true;
|
|
847
815
|
}
|
|
848
816
|
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)) {
|
|
817
|
+
if (runtimeRoot && BLOCKED_REVIEWER_RUNTIME_ROOTS.has(runtimeRoot))
|
|
863
818
|
return false;
|
|
864
|
-
}
|
|
865
819
|
return isPreservedReviewerPath(rel, preservePaths);
|
|
866
820
|
}
|
|
867
821
|
function isPreservedReviewerPath(rel, preservePaths) {
|
|
@@ -877,15 +831,10 @@ function isPreservedReviewerPath(rel, preservePaths) {
|
|
|
877
831
|
}
|
|
878
832
|
return false;
|
|
879
833
|
}
|
|
880
|
-
/** Test-only alias for the preserve-set extractor. */
|
|
881
834
|
export function __testExtractDiffTouchedPaths(diff) {
|
|
882
835
|
return extractDiffTouchedPaths(diff);
|
|
883
836
|
}
|
|
884
837
|
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
838
|
const paths = new Set();
|
|
890
839
|
for (const file of parseUnifiedDiff(diff).files) {
|
|
891
840
|
if (file.oldPath)
|
|
@@ -921,57 +870,40 @@ function isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, sourcePath, e
|
|
|
921
870
|
return true;
|
|
922
871
|
let linkTarget = "";
|
|
923
872
|
let resolvedTarget = "";
|
|
873
|
+
let targetKind = "other";
|
|
924
874
|
try {
|
|
925
875
|
linkTarget = readlinkSync(sourcePath);
|
|
926
876
|
resolvedTarget = realpathSync(sourcePath);
|
|
877
|
+
const targetStat = statSync(sourcePath);
|
|
878
|
+
targetKind = targetStat.isDirectory() ? "directory" : targetStat.isFile() ? "file" : "other";
|
|
927
879
|
}
|
|
928
880
|
catch {
|
|
929
881
|
return false;
|
|
930
882
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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));
|
|
883
|
+
return sensitiveResourcePolicy.assessSymlink({
|
|
884
|
+
sourceRoot,
|
|
885
|
+
canonicalSourceRoot: resolvedSourceRoot,
|
|
886
|
+
sourcePath,
|
|
887
|
+
linkTarget,
|
|
888
|
+
resolvedTargetPath: resolvedTarget,
|
|
889
|
+
targetKind,
|
|
890
|
+
allowedTargetKinds: ["file", "directory"],
|
|
891
|
+
excludedRoots: excludeRoots,
|
|
892
|
+
relocationRoot: sourceRoot,
|
|
893
|
+
}).allowed;
|
|
948
894
|
}
|
|
949
895
|
async function initializeReviewerWorkspaceGit(root) {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
"-
|
|
954
|
-
"
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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"]);
|
|
896
|
+
const noHooks = ["-c", "core.hooksPath=/dev/null"];
|
|
897
|
+
await runGitOrThrow("init", root, ["-c", "init.templateDir=", ...noHooks, "init"]);
|
|
898
|
+
for (const [key, value] of [
|
|
899
|
+
["user.email", "claudexor-review@example.invalid"],
|
|
900
|
+
["user.name", "Claudexor Review"],
|
|
901
|
+
]) {
|
|
902
|
+
await runGitOrThrow(`config ${key}`, root, [...noHooks, "config", key, value]);
|
|
903
|
+
}
|
|
904
|
+
await runGitOrThrow("add", root, [...noHooks, "add", "-A", "--force"]);
|
|
972
905
|
await runGitOrThrow("commit", root, [
|
|
973
|
-
|
|
974
|
-
"core.hooksPath=/dev/null",
|
|
906
|
+
...noHooks,
|
|
975
907
|
"commit",
|
|
976
908
|
"--allow-empty",
|
|
977
909
|
"--no-verify",
|
|
@@ -1034,6 +966,9 @@ function emitReviewerProgress(artifact, reviewer, onReviewerEvent, patch) {
|
|
|
1034
966
|
requested_model: reviewer.requestedModel ?? null,
|
|
1035
967
|
requested_effort: reviewer.requestedEffort ?? null,
|
|
1036
968
|
artifact_dir: artifact.dir,
|
|
969
|
+
...(typeof artifact.metadata["review_wave_id"] === "string"
|
|
970
|
+
? { review_wave_id: artifact.metadata["review_wave_id"] }
|
|
971
|
+
: {}),
|
|
1037
972
|
...patch,
|
|
1038
973
|
};
|
|
1039
974
|
const redacted = redactValue(event);
|