ultracode-for-codex 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -193
- package/ULTRACODE_INSTALL.md +20 -11
- package/dist/cli.js +18 -14
- package/dist/runtime/workflow-journal.d.ts +1 -0
- package/dist/runtime/workflow-journal.js +5 -2
- package/dist/runtime/workflow-runtime.js +941 -17
- package/docs/provenance-audit.md +3 -3
- package/package.json +1 -1
- package/skills/ultracode-for-codex-cli/SKILL.md +8 -2
|
@@ -6,7 +6,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:pat
|
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { createContext, runInContext } from 'node:vm';
|
|
8
8
|
import { UltracodeRequestError, estimateTokens } from './types.js';
|
|
9
|
-
import { WORKFLOW_JOURNAL_GENESIS_AGENT_CALL_KEY, WORKFLOW_JOURNAL_WRITE_FAILED_REASON, WorkflowJournalError, WorkflowJournalWriter, computeWorkflowAgentCallKey, isWorkflowJournalError, normalizeJournalJsonValue, readWorkflowJournal, workflowJournalPath, } from './workflow-journal.js';
|
|
9
|
+
import { WORKFLOW_JOURNAL_GENESIS_AGENT_CALL_KEY, WORKFLOW_JOURNAL_WRITE_FAILED_REASON, WorkflowJournalError, WorkflowJournalWriter, computeWorkflowAgentCallKey, isWorkflowJournalError, normalizeJournalJsonValue, readWorkflowJournal, stableJson, workflowJournalPath, } from './workflow-journal.js';
|
|
10
10
|
const MAX_SCRIPT_BYTES = 64 * 1024;
|
|
11
11
|
const MAX_AGENT_CALLS = 1000;
|
|
12
12
|
const MAX_PARALLELISM = 16;
|
|
@@ -14,6 +14,7 @@ const DEFAULT_AGENT_STALL_RETRY_LIMIT = 5;
|
|
|
14
14
|
const DEFAULT_WORKSPACE_CONTEXT_MAX_FILES = 24;
|
|
15
15
|
const DEFAULT_WORKSPACE_CONTEXT_MAX_FILE_BYTES = 12_000;
|
|
16
16
|
const DEFAULT_WORKSPACE_CONTEXT_MAX_BYTES = 80_000;
|
|
17
|
+
const DEFAULT_WORKSPACE_CONTEXT_MAX_DIFF_BYTES = 60_000;
|
|
17
18
|
const execFileAsync = promisify(execFile);
|
|
18
19
|
const PROJECT_WORKFLOW_DIRS = ['.codex/workflows'];
|
|
19
20
|
const WORKFLOW_PERMISSION_REQUIRED_SOURCES = new Set(['script_path', 'project', 'user', 'plugin']);
|
|
@@ -123,15 +124,7 @@ const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
|
123
124
|
},
|
|
124
125
|
{
|
|
125
126
|
name: 'code-review',
|
|
126
|
-
script:
|
|
127
|
-
name: 'code-review',
|
|
128
|
-
description: 'Run an LLM-planned phase-wise parallel code review workflow',
|
|
129
|
-
defaultPrompt: 'Review the current repository for correctness risks.',
|
|
130
|
-
plannerKind: 'code review',
|
|
131
|
-
plannerGuidance: 'Plan an effective code review. Default to phase_parallel with multiple focused reviewers per phase. Commonly useful lenses include runtime correctness, security/capability boundaries, API/CLI contracts, persistence/retry/cancel behavior, and test coverage. Prefer fan-out-and-synthesize plus adversarial verification unless the diff is tiny or one indivisible failure mode.',
|
|
132
|
-
agentGuidance: 'Return material findings only. Prioritize root cause, severity, exact file/line evidence, reproduction or impact, and residual risk.',
|
|
133
|
-
finalGuidance: 'Return findings ordered by severity with exact file/line references. Deduplicate overlaps, preserve dissent or uncertainty, and say clearly if there are no material findings.',
|
|
134
|
-
}),
|
|
127
|
+
script: codeReviewBuiltinWorkflowScript(),
|
|
135
128
|
},
|
|
136
129
|
{
|
|
137
130
|
name: 'batch',
|
|
@@ -157,6 +150,740 @@ return await parallel(prompts.map((prompt, index) => () => agent(
|
|
|
157
150
|
)));`,
|
|
158
151
|
},
|
|
159
152
|
];
|
|
153
|
+
function codeReviewBuiltinWorkflowScript() {
|
|
154
|
+
return `export const meta = {
|
|
155
|
+
name: "code-review",
|
|
156
|
+
description: "Run a dynamic evidence-bound code review workflow"
|
|
157
|
+
};
|
|
158
|
+
const workflowInput = args && typeof args === "object" ? args : {};
|
|
159
|
+
const prompt = typeof workflowInput.prompt === "string" && workflowInput.prompt.trim()
|
|
160
|
+
? workflowInput.prompt
|
|
161
|
+
: "Review the current repository for correctness risks.";
|
|
162
|
+
const level = workflowInput.level === "high" ? "high" : "xhigh";
|
|
163
|
+
const caps = level === "high"
|
|
164
|
+
? { maxFinders: 8, maxCandidatesPerLens: 6, sweep: false, reportCap: 10 }
|
|
165
|
+
: { maxFinders: 10, maxCandidatesPerLens: 8, sweep: true, reportCap: 15 };
|
|
166
|
+
const seedLenses = [
|
|
167
|
+
{ id: "diff-correctness", title: "Diff correctness", kind: "correctness", focus: "Inspect touched hunks and enclosing behavior for runtime bugs." },
|
|
168
|
+
{ id: "removed-behavior", title: "Removed behavior", kind: "correctness", focus: "Check deleted or replaced guards, validation, errors, and tests." },
|
|
169
|
+
{ id: "cross-file-contract", title: "Cross-file contract", kind: "contract", focus: "Trace callers, callees, preconditions, and return shapes." },
|
|
170
|
+
{ id: "language-platform", title: "Language/platform pitfalls", kind: "correctness", focus: "Look for language, framework, and environment-sensitive footguns." },
|
|
171
|
+
{ id: "wrapper-delegation", title: "Wrapper/delegation correctness", kind: "contract", focus: "Check adapters, proxies, caches, decorators, and delegation paths." },
|
|
172
|
+
{ id: "security-boundary", title: "Security/capability boundary", kind: "security", focus: "Check authority, permissions, credential handling, and local state exposure." },
|
|
173
|
+
{ id: "persistence-retry-cancel", title: "Persistence/retry/cancel", kind: "persistence", focus: "Check journals, resume/cache, retries, cancellation, and terminal states." },
|
|
174
|
+
{ id: "cli-user-contract", title: "CLI/user contract", kind: "contract", focus: "Check commands, settings, progress, package contents, and documented behavior." },
|
|
175
|
+
{ id: "tests-package-coverage", title: "Tests/package coverage", kind: "coverage", focus: "Check whether tests and packaged artifacts cover changed behavior." },
|
|
176
|
+
{ id: "maintainability", title: "Maintainability/conventions", kind: "maintainability", focus: "Check duplication, altitude, and repository instruction alignment." }
|
|
177
|
+
];
|
|
178
|
+
const scopeSchema = {
|
|
179
|
+
type: "object",
|
|
180
|
+
additionalProperties: false,
|
|
181
|
+
required: ["files", "summary", "lensDecisions", "lenses"],
|
|
182
|
+
properties: {
|
|
183
|
+
files: { type: "array", items: { type: "string", minLength: 1, maxLength: 240 } },
|
|
184
|
+
summary: { type: "string", minLength: 1 },
|
|
185
|
+
instructions: { type: "string" },
|
|
186
|
+
lensDecisions: {
|
|
187
|
+
type: "array",
|
|
188
|
+
items: {
|
|
189
|
+
type: "object",
|
|
190
|
+
additionalProperties: false,
|
|
191
|
+
required: ["seedId", "action", "reasonCategory", "decisionRefs", "reason"],
|
|
192
|
+
properties: {
|
|
193
|
+
seedId: { type: "string", minLength: 1 },
|
|
194
|
+
action: { type: "string", enum: ["select", "skip"] },
|
|
195
|
+
selectedLensId: { type: "string" },
|
|
196
|
+
reasonCategory: { type: "string", enum: ["matched_change", "prompt_risk", "no_evidence", "cap_limit", "redundant", "out_of_scope", "tiny_change"] },
|
|
197
|
+
decisionRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
198
|
+
reason: { type: "string", minLength: 1 }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
lenses: {
|
|
203
|
+
type: "array",
|
|
204
|
+
items: {
|
|
205
|
+
type: "object",
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
required: ["id", "title", "focus", "kind"],
|
|
208
|
+
properties: {
|
|
209
|
+
id: { type: "string", minLength: 1, maxLength: 80 },
|
|
210
|
+
title: { type: "string", minLength: 1, maxLength: 120 },
|
|
211
|
+
focus: { type: "string", minLength: 1, maxLength: 1000 },
|
|
212
|
+
kind: { type: "string", enum: ["correctness", "security", "contract", "persistence", "coverage", "maintainability"] }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const finderSchema = {
|
|
219
|
+
type: "object",
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
required: ["candidates"],
|
|
222
|
+
properties: {
|
|
223
|
+
candidates: {
|
|
224
|
+
type: "array",
|
|
225
|
+
items: {
|
|
226
|
+
type: "object",
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
required: ["file", "summary", "failureScenario", "evidenceRefs"],
|
|
229
|
+
properties: {
|
|
230
|
+
file: { type: "string", minLength: 1, maxLength: 240 },
|
|
231
|
+
line: { type: "integer", minimum: 1 },
|
|
232
|
+
summary: { type: "string", minLength: 1 },
|
|
233
|
+
failureScenario: { type: "string", minLength: 1 },
|
|
234
|
+
evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
235
|
+
kind: { type: "string" }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const verifierSchema = {
|
|
242
|
+
type: "object",
|
|
243
|
+
additionalProperties: false,
|
|
244
|
+
required: ["verdict", "evidence", "evidenceRefs"],
|
|
245
|
+
properties: {
|
|
246
|
+
verdict: { type: "string", enum: ["CONFIRMED", "PLAUSIBLE", "REFUTED"] },
|
|
247
|
+
evidence: { type: "string", minLength: 1 },
|
|
248
|
+
evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
249
|
+
severity: { type: "string", enum: ["P0", "P1", "P2", "P3"] }
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const synthesisSchema = {
|
|
253
|
+
type: "object",
|
|
254
|
+
additionalProperties: false,
|
|
255
|
+
required: ["summary", "decisions"],
|
|
256
|
+
properties: {
|
|
257
|
+
summary: { type: "string", minLength: 1 },
|
|
258
|
+
decisions: {
|
|
259
|
+
type: "array",
|
|
260
|
+
items: {
|
|
261
|
+
type: "object",
|
|
262
|
+
additionalProperties: false,
|
|
263
|
+
required: ["index", "action", "reasonCategory", "reason"],
|
|
264
|
+
properties: {
|
|
265
|
+
index: { type: "integer", minimum: 0 },
|
|
266
|
+
action: { type: "string", enum: ["report", "merge", "drop"] },
|
|
267
|
+
merge: { type: "array", minItems: 1, items: { type: "integer", minimum: 0 } },
|
|
268
|
+
severity: { type: "string", enum: ["P0", "P1", "P2", "P3"] },
|
|
269
|
+
reasonCategory: { type: "string", enum: ["material", "duplicate", "not_material", "report_cap", "unsupported_evidence", "superseded"] },
|
|
270
|
+
reason: { type: "string", minLength: 1 }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
function fail(message) {
|
|
277
|
+
throw "code-review invalid: " + message;
|
|
278
|
+
}
|
|
279
|
+
function text(value) {
|
|
280
|
+
return value == null ? "" : "" + value;
|
|
281
|
+
}
|
|
282
|
+
function errorText(err) {
|
|
283
|
+
if (err && typeof err.message === "string") return err.message;
|
|
284
|
+
try {
|
|
285
|
+
const json = JSON.stringify(err);
|
|
286
|
+
if (json) return json;
|
|
287
|
+
} catch (_err) {}
|
|
288
|
+
return text(err);
|
|
289
|
+
}
|
|
290
|
+
function lines(value) {
|
|
291
|
+
return text(value).split(/\\r?\\n/);
|
|
292
|
+
}
|
|
293
|
+
function firstLineValue(context, prefix) {
|
|
294
|
+
const all = lines(context);
|
|
295
|
+
for (let index = 0; index < all.length; index += 1) {
|
|
296
|
+
if (all[index].indexOf(prefix) === 0) return all[index].slice(prefix.length).trim();
|
|
297
|
+
}
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
function sectionLines(context, title, endTitles) {
|
|
301
|
+
const all = lines(context);
|
|
302
|
+
let start = -1;
|
|
303
|
+
for (let index = 0; index < all.length; index += 1) {
|
|
304
|
+
if (all[index] === title) {
|
|
305
|
+
start = index + 1;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (start < 0) return [];
|
|
310
|
+
const out = [];
|
|
311
|
+
for (let index = start; index < all.length; index += 1) {
|
|
312
|
+
if (endTitles.indexOf(all[index]) >= 0) break;
|
|
313
|
+
const line = all[index].trim();
|
|
314
|
+
if (line && line !== "(none)") out.push(line);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
function objectMap(values) {
|
|
319
|
+
const map = {};
|
|
320
|
+
for (let index = 0; index < values.length; index += 1) map[values[index]] = true;
|
|
321
|
+
return map;
|
|
322
|
+
}
|
|
323
|
+
function normalizeKey(value, fallback) {
|
|
324
|
+
const raw = text(value || fallback).trim().toLowerCase();
|
|
325
|
+
const replaced = raw.replace(/[^a-z0-9_.:/@+-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
326
|
+
return replaced ? replaced.slice(0, 80) : fallback;
|
|
327
|
+
}
|
|
328
|
+
function uniquePush(list, item) {
|
|
329
|
+
if (list.indexOf(item) < 0) list.push(item);
|
|
330
|
+
}
|
|
331
|
+
function assertDecisionRefs(refs, label) {
|
|
332
|
+
if (!Array.isArray(refs) || refs.length < 1) fail(label + " must include evidence or decision refs.");
|
|
333
|
+
for (let index = 0; index < refs.length; index += 1) {
|
|
334
|
+
const ref = text(refs[index]);
|
|
335
|
+
if (!allowedEvidenceRefMap[ref] && !unavailableEvidenceRefMap[ref] && ref !== "prompt:request") {
|
|
336
|
+
fail(label + " includes unsupported decision ref " + ref);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function assertEvidenceRefs(refs, label) {
|
|
341
|
+
if (!Array.isArray(refs) || refs.length < 1) fail(label + " must include at least one evidence ref.");
|
|
342
|
+
for (let index = 0; index < refs.length; index += 1) {
|
|
343
|
+
const ref = text(refs[index]);
|
|
344
|
+
if (!allowedEvidenceRefMap[ref]) fail(label + " includes unsupported evidence ref " + ref);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function validateFile(file, label) {
|
|
348
|
+
const ref = "file:" + text(file);
|
|
349
|
+
if (!allowedFileRefMap[ref]) fail(label + " references unsupported file " + text(file));
|
|
350
|
+
}
|
|
351
|
+
function selectedDecisionMatches(decision, lens) {
|
|
352
|
+
if (decision.action !== "select") return false;
|
|
353
|
+
const selected = normalizeKey(decision.selectedLensId || decision.seedId, decision.seedId);
|
|
354
|
+
return selected === lens.lensKey || selected === normalizeKey(lens.id, lens.lensKey);
|
|
355
|
+
}
|
|
356
|
+
function validateScope(scope) {
|
|
357
|
+
for (let index = 0; index < scope.files.length; index += 1) validateFile(scope.files[index], "scope.files[" + index + "]");
|
|
358
|
+
for (let index = 0; index < scope.lensDecisions.length; index += 1) {
|
|
359
|
+
assertDecisionRefs(scope.lensDecisions[index].decisionRefs, "scope.lensDecisions[" + index + "]");
|
|
360
|
+
}
|
|
361
|
+
const selected = [];
|
|
362
|
+
const seen = {};
|
|
363
|
+
for (let index = 0; index < scope.lenses.length; index += 1) {
|
|
364
|
+
const rawLens = scope.lenses[index];
|
|
365
|
+
const lensKey = normalizeKey(rawLens.id, "lens-" + (index + 1));
|
|
366
|
+
if (seen[lensKey]) fail("duplicate selected lens " + lensKey);
|
|
367
|
+
const lens = {
|
|
368
|
+
id: text(rawLens.id),
|
|
369
|
+
lensKey: lensKey,
|
|
370
|
+
title: text(rawLens.title),
|
|
371
|
+
focus: text(rawLens.focus),
|
|
372
|
+
kind: text(rawLens.kind),
|
|
373
|
+
position: index
|
|
374
|
+
};
|
|
375
|
+
let matched = false;
|
|
376
|
+
for (let decisionIndex = 0; decisionIndex < scope.lensDecisions.length; decisionIndex += 1) {
|
|
377
|
+
if (selectedDecisionMatches(scope.lensDecisions[decisionIndex], lens)) matched = true;
|
|
378
|
+
}
|
|
379
|
+
if (!matched) fail("selected lens lacks matching select decision " + lensKey);
|
|
380
|
+
seen[lensKey] = true;
|
|
381
|
+
if (selected.length < caps.maxFinders) selected.push(lens);
|
|
382
|
+
}
|
|
383
|
+
return selected;
|
|
384
|
+
}
|
|
385
|
+
function validateCandidate(candidate, label) {
|
|
386
|
+
validateFile(candidate.file, label + ".file");
|
|
387
|
+
assertEvidenceRefs(candidate.evidenceRefs, label + ".evidenceRefs");
|
|
388
|
+
return {
|
|
389
|
+
file: text(candidate.file),
|
|
390
|
+
line: Number.isInteger(candidate.line) ? candidate.line : null,
|
|
391
|
+
summary: text(candidate.summary),
|
|
392
|
+
failureScenario: text(candidate.failureScenario),
|
|
393
|
+
evidenceRefs: candidate.evidenceRefs.map((item) => text(item)),
|
|
394
|
+
kind: text(candidate.kind || "unspecified")
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function validateVerifier(verifier, label) {
|
|
398
|
+
assertEvidenceRefs(verifier.evidenceRefs, label + ".evidenceRefs");
|
|
399
|
+
return {
|
|
400
|
+
verdict: verifier.verdict,
|
|
401
|
+
evidence: text(verifier.evidence),
|
|
402
|
+
evidenceRefs: verifier.evidenceRefs.map((item) => text(item)),
|
|
403
|
+
severity: verifier.severity || "P2"
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function finderLabel(lens) {
|
|
407
|
+
return "code-review-find-" + lens.lensKey;
|
|
408
|
+
}
|
|
409
|
+
function verifierLabel(envelope) {
|
|
410
|
+
return "code-review-verify-" + envelope.lensKey + "-c" + (envelope.candidateIndex + 1);
|
|
411
|
+
}
|
|
412
|
+
function verifierKey(envelope) {
|
|
413
|
+
return [
|
|
414
|
+
"code-review/verify",
|
|
415
|
+
envelope.lensKey,
|
|
416
|
+
"" + envelope.candidateIndex,
|
|
417
|
+
envelope.candidateDigest.slice(7, 23),
|
|
418
|
+
contextHash.slice(7, 23),
|
|
419
|
+
allowedEvidenceIndexDigest.slice(7, 23)
|
|
420
|
+
].join("/");
|
|
421
|
+
}
|
|
422
|
+
function reviewLensStage(lens) {
|
|
423
|
+
return agent([
|
|
424
|
+
"Code-review Finder",
|
|
425
|
+
"Lens: " + lens.title,
|
|
426
|
+
"Lens key: " + lens.lensKey,
|
|
427
|
+
"Focus: " + lens.focus,
|
|
428
|
+
"",
|
|
429
|
+
"Return only concrete defect candidates with a failure scenario and evidence refs from the allowed index.",
|
|
430
|
+
"User request:",
|
|
431
|
+
prompt,
|
|
432
|
+
"",
|
|
433
|
+
"Scope:",
|
|
434
|
+
JSON.stringify(scopeBlock, null, 2),
|
|
435
|
+
"",
|
|
436
|
+
context
|
|
437
|
+
].join("\\n"), {
|
|
438
|
+
label: finderLabel(lens),
|
|
439
|
+
phase: "Find",
|
|
440
|
+
schema: finderSchema,
|
|
441
|
+
key: "code-review/find/" + lens.lensKey + "/" + sourceSnapshotHashKey
|
|
442
|
+
}).then((finderOutput) => {
|
|
443
|
+
const rawCandidates = Array.isArray(finderOutput.candidates) ? finderOutput.candidates : [];
|
|
444
|
+
const capped = rawCandidates.slice(0, caps.maxCandidatesPerLens);
|
|
445
|
+
const envelopes = [];
|
|
446
|
+
for (let index = 0; index < capped.length; index += 1) {
|
|
447
|
+
const candidate = validateCandidate(capped[index], "candidate " + lens.lensKey + "/" + index);
|
|
448
|
+
const candidateDigest = hash({
|
|
449
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
450
|
+
contextHash: contextHash,
|
|
451
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
452
|
+
truncation: truncation,
|
|
453
|
+
scopeDigest: scopeDigest,
|
|
454
|
+
lensKey: lens.lensKey,
|
|
455
|
+
candidateIndex: index,
|
|
456
|
+
candidate: candidate
|
|
457
|
+
});
|
|
458
|
+
envelopes.push({
|
|
459
|
+
candidateId: "candidate_" + lens.lensKey + "_" + (index + 1),
|
|
460
|
+
candidateIndex: index,
|
|
461
|
+
candidateDigest: candidateDigest,
|
|
462
|
+
lensKey: lens.lensKey,
|
|
463
|
+
lensTitle: lens.title,
|
|
464
|
+
candidate: candidate
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (envelopes.length === 0) return [];
|
|
468
|
+
return parallel(envelopes.map((envelope) => () => agent([
|
|
469
|
+
"Code-review Verifier",
|
|
470
|
+
"Verify exactly one candidate. Confirm, refute, or mark plausible using only allowed evidence refs.",
|
|
471
|
+
"Candidate envelope:",
|
|
472
|
+
JSON.stringify(envelope, null, 2),
|
|
473
|
+
"",
|
|
474
|
+
"Review evidence:",
|
|
475
|
+
context
|
|
476
|
+
].join("\\n"), {
|
|
477
|
+
label: verifierLabel(envelope),
|
|
478
|
+
phase: "Verify",
|
|
479
|
+
schema: verifierSchema,
|
|
480
|
+
key: verifierKey(envelope)
|
|
481
|
+
}))).then((verifierResults) => {
|
|
482
|
+
if (verifierResults.length !== envelopes.length) fail("verifier count mismatch for " + lens.lensKey);
|
|
483
|
+
const verified = [];
|
|
484
|
+
for (let index = 0; index < envelopes.length; index += 1) {
|
|
485
|
+
if (verifierResults[index] == null) fail("missing verifier result for " + envelopes[index].candidateId);
|
|
486
|
+
verified.push({
|
|
487
|
+
candidateId: envelopes[index].candidateId,
|
|
488
|
+
candidateIndex: envelopes[index].candidateIndex,
|
|
489
|
+
candidateDigest: envelopes[index].candidateDigest,
|
|
490
|
+
lensKey: envelopes[index].lensKey,
|
|
491
|
+
lensTitle: envelopes[index].lensTitle,
|
|
492
|
+
candidate: envelopes[index].candidate,
|
|
493
|
+
verifier: validateVerifier(verifierResults[index], "verifier " + envelopes[index].candidateId)
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return verified;
|
|
497
|
+
});
|
|
498
|
+
}).catch((err) => ({
|
|
499
|
+
failed: true,
|
|
500
|
+
error: errorText(err)
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
function runSweep(kept, refutedCount) {
|
|
504
|
+
return agent([
|
|
505
|
+
"Code-review Sweep Finder",
|
|
506
|
+
"Find only new material candidates missed by the lens pass.",
|
|
507
|
+
"Kept candidate count: " + kept.length,
|
|
508
|
+
"Refuted candidate count: " + refutedCount,
|
|
509
|
+
"Scope:",
|
|
510
|
+
JSON.stringify(scopeBlock, null, 2),
|
|
511
|
+
"",
|
|
512
|
+
context
|
|
513
|
+
].join("\\n"), {
|
|
514
|
+
label: "code-review-sweep-finder",
|
|
515
|
+
phase: "Sweep",
|
|
516
|
+
schema: finderSchema,
|
|
517
|
+
key: "code-review/sweep/" + sourceSnapshotHashKey + "/" + scopeDigest.slice(7, 23)
|
|
518
|
+
}).then((sweepOutput) => {
|
|
519
|
+
const sweepLens = { id: "sweep", lensKey: "sweep", title: "Sweep", focus: "Final gap search", kind: "correctness", position: activeLenses.length };
|
|
520
|
+
const raw = Array.isArray(sweepOutput.candidates) ? sweepOutput.candidates : [];
|
|
521
|
+
if (raw.length === 0) return [];
|
|
522
|
+
return reviewSweepCandidates(sweepLens, raw.slice(0, 8));
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function reviewSweepCandidates(lens, rawCandidates) {
|
|
526
|
+
const envelopes = [];
|
|
527
|
+
for (let index = 0; index < rawCandidates.length; index += 1) {
|
|
528
|
+
const candidate = validateCandidate(rawCandidates[index], "sweep candidate " + index);
|
|
529
|
+
const candidateDigest = hash({
|
|
530
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
531
|
+
contextHash: contextHash,
|
|
532
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
533
|
+
truncation: truncation,
|
|
534
|
+
scopeDigest: scopeDigest,
|
|
535
|
+
lensKey: "sweep",
|
|
536
|
+
candidateIndex: index,
|
|
537
|
+
candidate: candidate
|
|
538
|
+
});
|
|
539
|
+
envelopes.push({
|
|
540
|
+
candidateId: "candidate_sweep_" + (index + 1),
|
|
541
|
+
candidateIndex: index,
|
|
542
|
+
candidateDigest: candidateDigest,
|
|
543
|
+
lensKey: "sweep",
|
|
544
|
+
lensTitle: "Sweep",
|
|
545
|
+
candidate: candidate
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return parallel(envelopes.map((envelope) => () => agent([
|
|
549
|
+
"Code-review Verifier",
|
|
550
|
+
"Verify exactly one sweep candidate.",
|
|
551
|
+
JSON.stringify(envelope, null, 2),
|
|
552
|
+
"",
|
|
553
|
+
context
|
|
554
|
+
].join("\\n"), {
|
|
555
|
+
label: verifierLabel(envelope),
|
|
556
|
+
phase: "Verify",
|
|
557
|
+
schema: verifierSchema,
|
|
558
|
+
key: verifierKey(envelope)
|
|
559
|
+
}))).then((verifierResults) => {
|
|
560
|
+
if (verifierResults.length !== envelopes.length) fail("sweep verifier count mismatch");
|
|
561
|
+
const verified = [];
|
|
562
|
+
for (let index = 0; index < envelopes.length; index += 1) {
|
|
563
|
+
if (verifierResults[index] == null) fail("missing sweep verifier result");
|
|
564
|
+
verified.push({
|
|
565
|
+
candidateId: envelopes[index].candidateId,
|
|
566
|
+
candidateIndex: envelopes[index].candidateIndex,
|
|
567
|
+
candidateDigest: envelopes[index].candidateDigest,
|
|
568
|
+
lensKey: envelopes[index].lensKey,
|
|
569
|
+
lensTitle: envelopes[index].lensTitle,
|
|
570
|
+
candidate: envelopes[index].candidate,
|
|
571
|
+
verifier: validateVerifier(verifierResults[index], "sweep verifier " + index)
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return verified;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function fallbackDecisions(items, reason) {
|
|
578
|
+
const decisions = [];
|
|
579
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
580
|
+
decisions.push({
|
|
581
|
+
index: index,
|
|
582
|
+
action: index < caps.reportCap ? "report" : "drop",
|
|
583
|
+
severity: items[index].verifier.severity || "P2",
|
|
584
|
+
reasonCategory: index < caps.reportCap ? "material" : "report_cap",
|
|
585
|
+
reason: reason,
|
|
586
|
+
merge: []
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return { mode: "script_fallback", summary: "Script fallback synthesis.", fallbackReason: reason, decisions: decisions };
|
|
590
|
+
}
|
|
591
|
+
function normalizeSynthesis(raw, items) {
|
|
592
|
+
try {
|
|
593
|
+
const decisions = Array.isArray(raw.decisions) ? raw.decisions : [];
|
|
594
|
+
const covered = {};
|
|
595
|
+
const normalized = [];
|
|
596
|
+
for (let index = 0; index < decisions.length; index += 1) {
|
|
597
|
+
const decision = decisions[index];
|
|
598
|
+
if (!Number.isInteger(decision.index) || decision.index < 0 || decision.index >= items.length) fail("synthesis index out of range");
|
|
599
|
+
if (covered[decision.index]) fail("duplicate synthesis coverage");
|
|
600
|
+
covered[decision.index] = true;
|
|
601
|
+
const merge = Array.isArray(decision.merge) ? decision.merge : [];
|
|
602
|
+
if (decision.action === "merge" && merge.length < 1) fail("merge decision requires merge indexes");
|
|
603
|
+
for (let mergeIndex = 0; mergeIndex < merge.length; mergeIndex += 1) {
|
|
604
|
+
if (!Number.isInteger(merge[mergeIndex]) || merge[mergeIndex] < 0 || merge[mergeIndex] >= items.length) {
|
|
605
|
+
fail("merge index out of range");
|
|
606
|
+
}
|
|
607
|
+
if (covered[merge[mergeIndex]]) fail("duplicate merge coverage");
|
|
608
|
+
covered[merge[mergeIndex]] = true;
|
|
609
|
+
}
|
|
610
|
+
normalized.push({
|
|
611
|
+
index: decision.index,
|
|
612
|
+
action: decision.action,
|
|
613
|
+
severity: decision.severity || items[decision.index].verifier.severity || "P2",
|
|
614
|
+
reasonCategory: decision.reasonCategory,
|
|
615
|
+
reason: text(decision.reason),
|
|
616
|
+
merge: merge
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
620
|
+
if (!covered[index]) fail("missing synthesis coverage for index " + index);
|
|
621
|
+
}
|
|
622
|
+
return { mode: "agent", summary: text(raw.summary), fallbackReason: null, decisions: normalized };
|
|
623
|
+
} catch (err) {
|
|
624
|
+
return fallbackDecisions(items, text(err && err.message ? err.message : err));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function finalDecisionRows(synthesis, items) {
|
|
628
|
+
const rows = [];
|
|
629
|
+
for (let index = 0; index < synthesis.decisions.length; index += 1) {
|
|
630
|
+
const decision = synthesis.decisions[index];
|
|
631
|
+
const item = items[decision.index];
|
|
632
|
+
const mergeCandidates = [];
|
|
633
|
+
for (let mergeIndex = 0; mergeIndex < decision.merge.length; mergeIndex += 1) {
|
|
634
|
+
const merged = items[decision.merge[mergeIndex]];
|
|
635
|
+
mergeCandidates.push({ candidateId: merged.candidateId, candidateDigest: merged.candidateDigest });
|
|
636
|
+
}
|
|
637
|
+
rows.push({
|
|
638
|
+
candidateId: item.candidateId,
|
|
639
|
+
candidateDigest: item.candidateDigest,
|
|
640
|
+
action: decision.action,
|
|
641
|
+
reasonCategory: decision.reasonCategory,
|
|
642
|
+
reason: decision.reason,
|
|
643
|
+
mergeCandidates: mergeCandidates,
|
|
644
|
+
severity: decision.severity
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return rows;
|
|
648
|
+
}
|
|
649
|
+
function droppedStats(decisions) {
|
|
650
|
+
const stats = { duplicate: 0, notMaterial: 0, reportCap: 0, unsupportedEvidence: 0, superseded: 0 };
|
|
651
|
+
for (let index = 0; index < decisions.length; index += 1) {
|
|
652
|
+
const row = decisions[index];
|
|
653
|
+
if (row.action !== "drop" && row.action !== "merge") continue;
|
|
654
|
+
if (row.reasonCategory === "duplicate") stats.duplicate += 1;
|
|
655
|
+
else if (row.reasonCategory === "not_material") stats.notMaterial += 1;
|
|
656
|
+
else if (row.reasonCategory === "report_cap") stats.reportCap += 1;
|
|
657
|
+
else if (row.reasonCategory === "unsupported_evidence") stats.unsupportedEvidence += 1;
|
|
658
|
+
else if (row.reasonCategory === "superseded") stats.superseded += 1;
|
|
659
|
+
}
|
|
660
|
+
return stats;
|
|
661
|
+
}
|
|
662
|
+
announcePlan({
|
|
663
|
+
mode: "phase_parallel",
|
|
664
|
+
rationale: "Collect bounded repository evidence, choose review lenses, verify every candidate, then synthesize by index.",
|
|
665
|
+
phases: [{
|
|
666
|
+
id: "scope",
|
|
667
|
+
title: "Scope",
|
|
668
|
+
goal: "Choose active review lenses from runtime-owned evidence.",
|
|
669
|
+
agents: [{ id: "scope", title: "Scope", label: "code-review-scope", focus: "Select lenses and evidence-bound review scope." }]
|
|
670
|
+
}]
|
|
671
|
+
});
|
|
672
|
+
phase("Evidence");
|
|
673
|
+
const context = await workspaceContext({
|
|
674
|
+
query: prompt,
|
|
675
|
+
includeDiff: true,
|
|
676
|
+
diffBaseRef: workflowInput.diffBaseRef
|
|
677
|
+
});
|
|
678
|
+
const allowedEvidenceRefs = sectionLines(context, "### Allowed Evidence Refs", ["### Unavailable Evidence", "### Git Status"]);
|
|
679
|
+
const unavailableEvidenceRefs = sectionLines(context, "### Unavailable Evidence", ["### Git Status"]);
|
|
680
|
+
const allowedEvidenceRefMap = objectMap(allowedEvidenceRefs);
|
|
681
|
+
const unavailableEvidenceRefMap = objectMap(unavailableEvidenceRefs);
|
|
682
|
+
const allowedFileRefs = [];
|
|
683
|
+
for (let index = 0; index < allowedEvidenceRefs.length; index += 1) {
|
|
684
|
+
if (allowedEvidenceRefs[index].indexOf("file:") === 0) uniquePush(allowedFileRefs, allowedEvidenceRefs[index]);
|
|
685
|
+
}
|
|
686
|
+
const allowedFileRefMap = objectMap(allowedFileRefs);
|
|
687
|
+
const sourceSnapshotId = firstLineValue(context, "Source Snapshot: ") || firstLineValue(context, "sourceSnapshotId: ") || hash(context);
|
|
688
|
+
const contextHash = firstLineValue(context, "Context Hash: ") || firstLineValue(context, "contextHash: ") || hash({ context: context });
|
|
689
|
+
const allowedEvidenceIndexDigest = firstLineValue(context, "allowedEvidenceIndexDigest: ") || hash(allowedEvidenceRefs);
|
|
690
|
+
const diffBaseRef = firstLineValue(context, "diffBaseRef: ");
|
|
691
|
+
const truncation = firstLineValue(context, "truncation: ") || "{}";
|
|
692
|
+
const sourceSnapshotHashKey = hash({ sourceSnapshotId: sourceSnapshotId, contextHash: contextHash, allowedEvidenceIndexDigest: allowedEvidenceIndexDigest }).slice(7, 39);
|
|
693
|
+
announcePhasePlan({
|
|
694
|
+
id: "scope",
|
|
695
|
+
title: "Scope",
|
|
696
|
+
goal: "Select active review lenses from the bounded evidence context.",
|
|
697
|
+
agents: [{ id: "scope", title: "Scope", label: "code-review-scope", focus: "Return files, lens decisions, and active lenses." }]
|
|
698
|
+
});
|
|
699
|
+
phase("Scope");
|
|
700
|
+
const scope = await agent([
|
|
701
|
+
"Code-review Scope",
|
|
702
|
+
"Select active lenses from the seed list and the runtime-owned evidence context.",
|
|
703
|
+
"Use decisionRefs only from allowed evidence refs, unavailable evidence refs, or prompt:request.",
|
|
704
|
+
"Level: " + level,
|
|
705
|
+
"Max active lenses: " + caps.maxFinders,
|
|
706
|
+
"",
|
|
707
|
+
"Seed lenses:",
|
|
708
|
+
JSON.stringify(seedLenses, null, 2),
|
|
709
|
+
"",
|
|
710
|
+
"User request:",
|
|
711
|
+
prompt,
|
|
712
|
+
"",
|
|
713
|
+
context
|
|
714
|
+
].join("\\n"), {
|
|
715
|
+
label: "code-review-scope",
|
|
716
|
+
phase: "Scope",
|
|
717
|
+
schema: scopeSchema,
|
|
718
|
+
key: "code-review/scope/" + sourceSnapshotHashKey
|
|
719
|
+
});
|
|
720
|
+
const activeLenses = validateScope(scope);
|
|
721
|
+
const scopeBlock = {
|
|
722
|
+
files: scope.files,
|
|
723
|
+
summary: scope.summary,
|
|
724
|
+
instructions: scope.instructions || "",
|
|
725
|
+
lenses: activeLenses,
|
|
726
|
+
lensDecisions: scope.lensDecisions
|
|
727
|
+
};
|
|
728
|
+
const scopeDigest = hash({ sourceSnapshotId: sourceSnapshotId, contextHash: contextHash, scope: scopeBlock });
|
|
729
|
+
if (activeLenses.length === 0) {
|
|
730
|
+
return {
|
|
731
|
+
level: level,
|
|
732
|
+
provenance: {
|
|
733
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
734
|
+
contextHash: contextHash,
|
|
735
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
736
|
+
diffBaseRef: diffBaseRef || null,
|
|
737
|
+
truncation: { raw: truncation }
|
|
738
|
+
},
|
|
739
|
+
summary: scope.summary,
|
|
740
|
+
findings: [],
|
|
741
|
+
synthesis: { mode: "script_fallback", fallbackReason: "no active lenses", decisions: [] },
|
|
742
|
+
stats: { finders: 0, candidates: 0, verifierAttempts: 0, verified: 0, refuted: 0, invalid: 0, reported: 0, dropped: droppedStats([]) }
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
announcePhasePlan({
|
|
746
|
+
id: "find",
|
|
747
|
+
title: "Find",
|
|
748
|
+
goal: "Run one finder per active review lens.",
|
|
749
|
+
agents: activeLenses.map((lens) => ({
|
|
750
|
+
id: lens.lensKey,
|
|
751
|
+
title: lens.title,
|
|
752
|
+
label: finderLabel(lens),
|
|
753
|
+
focus: lens.focus
|
|
754
|
+
}))
|
|
755
|
+
});
|
|
756
|
+
announcePhasePlan({
|
|
757
|
+
id: "verify",
|
|
758
|
+
title: "Verify",
|
|
759
|
+
goal: "Verify candidates as soon as each finder emits them.",
|
|
760
|
+
agents: [{ id: "dynamic-candidates", title: "Dynamic candidate verifiers", label: "code-review-verify-dynamic", focus: "One verifier runs for each emitted candidate." }]
|
|
761
|
+
});
|
|
762
|
+
phase("Find");
|
|
763
|
+
phase("Verify");
|
|
764
|
+
const lensResults = await pipeline(activeLenses, reviewLensStage);
|
|
765
|
+
const verifiedCandidates = [];
|
|
766
|
+
for (let lensIndex = 0; lensIndex < lensResults.length; lensIndex += 1) {
|
|
767
|
+
if (lensResults[lensIndex] && lensResults[lensIndex].failed) fail(lensResults[lensIndex].error);
|
|
768
|
+
if (lensResults[lensIndex] == null) fail("lens review failed for " + activeLenses[lensIndex].lensKey);
|
|
769
|
+
for (let candidateIndex = 0; candidateIndex < lensResults[lensIndex].length; candidateIndex += 1) {
|
|
770
|
+
verifiedCandidates.push(lensResults[lensIndex][candidateIndex]);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const nonRefuted = [];
|
|
774
|
+
let refuted = 0;
|
|
775
|
+
for (let index = 0; index < verifiedCandidates.length; index += 1) {
|
|
776
|
+
if (verifiedCandidates[index].verifier.verdict === "REFUTED") refuted += 1;
|
|
777
|
+
else nonRefuted.push(verifiedCandidates[index]);
|
|
778
|
+
}
|
|
779
|
+
let sweepResults = [];
|
|
780
|
+
if (caps.sweep) {
|
|
781
|
+
announcePhasePlan({
|
|
782
|
+
id: "sweep",
|
|
783
|
+
title: "Sweep",
|
|
784
|
+
goal: "Search for missed candidates after initial verification.",
|
|
785
|
+
agents: [{ id: "sweep", title: "Sweep Finder", label: "code-review-sweep-finder", focus: "Find only new material missed candidates." }]
|
|
786
|
+
});
|
|
787
|
+
phase("Sweep");
|
|
788
|
+
sweepResults = await runSweep(nonRefuted, refuted);
|
|
789
|
+
for (let index = 0; index < sweepResults.length; index += 1) {
|
|
790
|
+
verifiedCandidates.push(sweepResults[index]);
|
|
791
|
+
if (sweepResults[index].verifier.verdict === "REFUTED") refuted += 1;
|
|
792
|
+
else nonRefuted.push(sweepResults[index]);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
let synthesis = { mode: "script_fallback", summary: "No confirmed or plausible candidates.", fallbackReason: "no reportable candidates", decisions: [] };
|
|
796
|
+
if (nonRefuted.length > 0) {
|
|
797
|
+
announcePhasePlan({
|
|
798
|
+
id: "synthesize",
|
|
799
|
+
title: "Synthesize",
|
|
800
|
+
goal: "Select final findings by verified candidate index.",
|
|
801
|
+
agents: [{ id: "synthesis", title: "Synthesis", label: "code-review-synthesis", focus: "Report, merge, or drop every verified non-refuted candidate." }]
|
|
802
|
+
});
|
|
803
|
+
phase("Synthesize");
|
|
804
|
+
const rawSynthesis = await agent([
|
|
805
|
+
"Code-review Synthesis",
|
|
806
|
+
"Select final findings by index only. Do not invent files, refs, or candidate ids.",
|
|
807
|
+
"Every candidate index must be reported, merged, dropped, or covered as a merge target.",
|
|
808
|
+
"Report cap: " + caps.reportCap,
|
|
809
|
+
"",
|
|
810
|
+
"Verified candidates:",
|
|
811
|
+
JSON.stringify(nonRefuted.map((item, index) => ({
|
|
812
|
+
index: index,
|
|
813
|
+
candidateId: item.candidateId,
|
|
814
|
+
candidateDigest: item.candidateDigest,
|
|
815
|
+
lensKey: item.lensKey,
|
|
816
|
+
file: item.candidate.file,
|
|
817
|
+
line: item.candidate.line,
|
|
818
|
+
summary: item.candidate.summary,
|
|
819
|
+
failureScenario: item.candidate.failureScenario,
|
|
820
|
+
verdict: item.verifier.verdict,
|
|
821
|
+
severity: item.verifier.severity,
|
|
822
|
+
evidenceRefs: item.verifier.evidenceRefs
|
|
823
|
+
})), null, 2)
|
|
824
|
+
].join("\\n"), {
|
|
825
|
+
label: "code-review-synthesis",
|
|
826
|
+
phase: "Synthesize",
|
|
827
|
+
schema: synthesisSchema,
|
|
828
|
+
key: "code-review/synthesis/" + sourceSnapshotHashKey + "/" + scopeDigest.slice(7, 23) + "/" + hash(nonRefuted).slice(7, 23)
|
|
829
|
+
});
|
|
830
|
+
synthesis = normalizeSynthesis(rawSynthesis, nonRefuted);
|
|
831
|
+
}
|
|
832
|
+
const decisionRows = finalDecisionRows(synthesis, nonRefuted);
|
|
833
|
+
const findings = [];
|
|
834
|
+
for (let index = 0; index < synthesis.decisions.length; index += 1) {
|
|
835
|
+
const decision = synthesis.decisions[index];
|
|
836
|
+
if (decision.action !== "report") continue;
|
|
837
|
+
const item = nonRefuted[decision.index];
|
|
838
|
+
const row = decisionRows[index];
|
|
839
|
+
findings.push({
|
|
840
|
+
candidateId: item.candidateId,
|
|
841
|
+
candidateDigest: item.candidateDigest,
|
|
842
|
+
severity: decision.severity || item.verifier.severity || "P2",
|
|
843
|
+
file: item.candidate.file,
|
|
844
|
+
line: item.candidate.line,
|
|
845
|
+
summary: item.candidate.summary,
|
|
846
|
+
failureScenario: item.candidate.failureScenario,
|
|
847
|
+
verdict: item.verifier.verdict,
|
|
848
|
+
evidence: item.verifier.evidence,
|
|
849
|
+
evidenceRefs: item.verifier.evidenceRefs,
|
|
850
|
+
lens: { key: item.lensKey, title: item.lensTitle },
|
|
851
|
+
synthesisDecision: {
|
|
852
|
+
action: decision.action,
|
|
853
|
+
reasonCategory: decision.reasonCategory,
|
|
854
|
+
reason: decision.reason,
|
|
855
|
+
mergeCandidates: row.mergeCandidates
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
level: level,
|
|
861
|
+
provenance: {
|
|
862
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
863
|
+
contextHash: contextHash,
|
|
864
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
865
|
+
diffBaseRef: diffBaseRef || null,
|
|
866
|
+
truncation: { raw: truncation }
|
|
867
|
+
},
|
|
868
|
+
summary: synthesis.summary,
|
|
869
|
+
findings: findings,
|
|
870
|
+
synthesis: {
|
|
871
|
+
mode: synthesis.mode,
|
|
872
|
+
fallbackReason: synthesis.fallbackReason,
|
|
873
|
+
decisions: decisionRows
|
|
874
|
+
},
|
|
875
|
+
stats: {
|
|
876
|
+
finders: activeLenses.length + (caps.sweep ? 1 : 0),
|
|
877
|
+
candidates: verifiedCandidates.length,
|
|
878
|
+
verifierAttempts: verifiedCandidates.length,
|
|
879
|
+
verified: verifiedCandidates.length,
|
|
880
|
+
refuted: refuted,
|
|
881
|
+
invalid: 0,
|
|
882
|
+
reported: findings.length,
|
|
883
|
+
dropped: droppedStats(decisionRows)
|
|
884
|
+
}
|
|
885
|
+
};`;
|
|
886
|
+
}
|
|
160
887
|
function phaseWiseBuiltinWorkflowScript(input) {
|
|
161
888
|
return `export const meta = {
|
|
162
889
|
name: ${JSON.stringify(input.name)},
|
|
@@ -530,6 +1257,8 @@ export class WorkflowTaskRegistry {
|
|
|
530
1257
|
}
|
|
531
1258
|
return {
|
|
532
1259
|
entries: cacheEntries,
|
|
1260
|
+
byCallKey: new Map(cacheEntries.map((entry) => [entry.agentCallKey, entry])),
|
|
1261
|
+
usedCallKeys: new Set(),
|
|
533
1262
|
nextIndex: 0,
|
|
534
1263
|
prefixOpen: true,
|
|
535
1264
|
};
|
|
@@ -1086,6 +1815,7 @@ export class WorkflowTaskRegistry {
|
|
|
1086
1815
|
host.pipeline = hardenCallable((items, ...stages) => {
|
|
1087
1816
|
return this.trackWorkflowPromise(ctx, this.pipeline(ctx, items, stages));
|
|
1088
1817
|
});
|
|
1818
|
+
host.hash = hardenCallable((value) => workflowValueHash(value));
|
|
1089
1819
|
host.workspaceContext = hardenCallable((options) => {
|
|
1090
1820
|
return this.trackWorkflowPromise(ctx, this.workspaceContext(ctx, options));
|
|
1091
1821
|
});
|
|
@@ -1151,6 +1881,7 @@ export class WorkflowTaskRegistry {
|
|
|
1151
1881
|
}
|
|
1152
1882
|
const schema = normalizeStructuredOutputSchema(options?.schema);
|
|
1153
1883
|
const isolation = normalizeAgentIsolation(options?.isolation);
|
|
1884
|
+
const logicalKey = normalizeAgentLogicalKey(options?.key);
|
|
1154
1885
|
if (isolation && !workflowIsolationReviewAllowsMode(ctx.isolationReview, isolation)) {
|
|
1155
1886
|
throw workflowInputError(`agent ${isolation} isolation was not covered by the current workflow permission review.`);
|
|
1156
1887
|
}
|
|
@@ -1162,6 +1893,7 @@ export class WorkflowTaskRegistry {
|
|
|
1162
1893
|
effort: 'xhigh',
|
|
1163
1894
|
schema,
|
|
1164
1895
|
isolation,
|
|
1896
|
+
logicalKey,
|
|
1165
1897
|
});
|
|
1166
1898
|
const previousAgentCallKey = ctx.previousAgentCallKey;
|
|
1167
1899
|
const agentCallKey = computeWorkflowAgentCallKey({
|
|
@@ -1771,16 +2503,38 @@ function normalizeAgentIsolation(value) {
|
|
|
1771
2503
|
return 'worktree';
|
|
1772
2504
|
throw workflowInputError('agent isolation must be "worktree" when provided.');
|
|
1773
2505
|
}
|
|
2506
|
+
function normalizeAgentLogicalKey(value) {
|
|
2507
|
+
if (value === undefined)
|
|
2508
|
+
return undefined;
|
|
2509
|
+
if (typeof value !== 'string')
|
|
2510
|
+
throw workflowInputError('agent key must be a non-empty string when provided.');
|
|
2511
|
+
const key = value.trim();
|
|
2512
|
+
if (!key)
|
|
2513
|
+
throw workflowInputError('agent key must be a non-empty string when provided.');
|
|
2514
|
+
if (key.length > 160)
|
|
2515
|
+
throw workflowInputError('agent key must be at most 160 characters.');
|
|
2516
|
+
if (!/^[A-Za-z0-9_.:/@+-]+$/.test(key)) {
|
|
2517
|
+
throw workflowInputError('agent key may only contain letters, numbers, "_", "-", ".", ":", "/", "@", and "+".');
|
|
2518
|
+
}
|
|
2519
|
+
return key;
|
|
2520
|
+
}
|
|
1774
2521
|
function takeResumeCacheHit(cache, agentCallKey) {
|
|
1775
|
-
if (!cache
|
|
2522
|
+
if (!cache)
|
|
1776
2523
|
return null;
|
|
1777
|
-
|
|
1778
|
-
|
|
2524
|
+
if (cache.prefixOpen) {
|
|
2525
|
+
const entry = cache.entries[cache.nextIndex];
|
|
2526
|
+
if (entry?.agentCallKey === agentCallKey && !cache.usedCallKeys.has(agentCallKey)) {
|
|
2527
|
+
cache.usedCallKeys.add(agentCallKey);
|
|
2528
|
+
cache.nextIndex += 1;
|
|
2529
|
+
return entry;
|
|
2530
|
+
}
|
|
1779
2531
|
cache.prefixOpen = false;
|
|
1780
|
-
return null;
|
|
1781
2532
|
}
|
|
1782
|
-
cache.
|
|
1783
|
-
|
|
2533
|
+
const keyed = cache.byCallKey.get(agentCallKey);
|
|
2534
|
+
if (!keyed || cache.usedCallKeys.has(agentCallKey))
|
|
2535
|
+
return null;
|
|
2536
|
+
cache.usedCallKeys.add(agentCallKey);
|
|
2537
|
+
return keyed;
|
|
1784
2538
|
}
|
|
1785
2539
|
async function gitOutput(cwd, args) {
|
|
1786
2540
|
try {
|
|
@@ -1800,7 +2554,10 @@ async function gitOutput(cwd, args) {
|
|
|
1800
2554
|
}
|
|
1801
2555
|
async function buildWorkspaceContext(cwd, options) {
|
|
1802
2556
|
const root = await workspaceContextRoot(cwd);
|
|
1803
|
-
const gitStatus = await gitOutput(root, ['status', '--short']).catch(() => '');
|
|
2557
|
+
const gitStatus = await gitOutput(root, ['status', '--short', '--untracked-files=all']).catch(() => '');
|
|
2558
|
+
const reviewEvidence = options.includeDiff
|
|
2559
|
+
? await buildReviewEvidenceContext(root, gitStatus, options)
|
|
2560
|
+
: undefined;
|
|
1804
2561
|
const explicitPaths = [
|
|
1805
2562
|
...options.files,
|
|
1806
2563
|
...extractMentionedWorkspacePaths(options.query ?? ''),
|
|
@@ -1834,6 +2591,19 @@ async function buildWorkspaceContext(cwd, options) {
|
|
|
1834
2591
|
return [
|
|
1835
2592
|
'## Workspace Context',
|
|
1836
2593
|
`Root: ${root}`,
|
|
2594
|
+
...(reviewEvidence ? [
|
|
2595
|
+
`Source Snapshot: ${reviewEvidence.sourceSnapshotId}`,
|
|
2596
|
+
`Context Hash: ${reviewEvidence.contextHash}`,
|
|
2597
|
+
'',
|
|
2598
|
+
'### Review Evidence',
|
|
2599
|
+
reviewEvidence.text,
|
|
2600
|
+
'',
|
|
2601
|
+
'### Allowed Evidence Refs',
|
|
2602
|
+
reviewEvidence.allowedEvidenceRefs.length ? reviewEvidence.allowedEvidenceRefs.join('\n') : '(none)',
|
|
2603
|
+
'',
|
|
2604
|
+
'### Unavailable Evidence',
|
|
2605
|
+
reviewEvidence.unavailableEvidence.length ? reviewEvidence.unavailableEvidence.join('\n') : '(none)',
|
|
2606
|
+
] : []),
|
|
1837
2607
|
'',
|
|
1838
2608
|
'### Git Status',
|
|
1839
2609
|
gitStatus.trim() ? gitStatus : '(clean or unavailable)',
|
|
@@ -1842,18 +2612,159 @@ async function buildWorkspaceContext(cwd, options) {
|
|
|
1842
2612
|
fileBlocks.length ? fileBlocks.join('\n\n') : '(no readable text files selected)',
|
|
1843
2613
|
].join('\n');
|
|
1844
2614
|
}
|
|
2615
|
+
async function buildReviewEvidenceContext(root, gitStatus, options) {
|
|
2616
|
+
const unavailableEvidence = [];
|
|
2617
|
+
const changedPaths = pathsFromGitStatus(gitStatus);
|
|
2618
|
+
const head = await gitOutput(root, ['rev-parse', '--verify', 'HEAD']).catch((err) => {
|
|
2619
|
+
unavailableEvidence.push(`unavailable:git-head:${workflowErrorMessage(err)}`);
|
|
2620
|
+
return 'unavailable';
|
|
2621
|
+
});
|
|
2622
|
+
const unstaged = await boundedGitOutput(root, [
|
|
2623
|
+
'diff',
|
|
2624
|
+
'--no-ext-diff',
|
|
2625
|
+
'--patch',
|
|
2626
|
+
'--find-renames',
|
|
2627
|
+
'--',
|
|
2628
|
+
], options.maxDiffBytes).catch((err) => {
|
|
2629
|
+
unavailableEvidence.push(`unavailable:diff-unstaged:${workflowErrorMessage(err)}`);
|
|
2630
|
+
return { text: '', truncated: false };
|
|
2631
|
+
});
|
|
2632
|
+
const staged = await boundedGitOutput(root, [
|
|
2633
|
+
'diff',
|
|
2634
|
+
'--cached',
|
|
2635
|
+
'--no-ext-diff',
|
|
2636
|
+
'--patch',
|
|
2637
|
+
'--find-renames',
|
|
2638
|
+
'--',
|
|
2639
|
+
], options.maxDiffBytes).catch((err) => {
|
|
2640
|
+
unavailableEvidence.push(`unavailable:diff-staged:${workflowErrorMessage(err)}`);
|
|
2641
|
+
return { text: '', truncated: false };
|
|
2642
|
+
});
|
|
2643
|
+
let committed = { text: '', truncated: false };
|
|
2644
|
+
let acceptedDiffBaseRef = '';
|
|
2645
|
+
if (options.diffBaseRef) {
|
|
2646
|
+
const baseCommit = await gitOutput(root, ['rev-parse', '--verify', `${options.diffBaseRef}^{commit}`]).catch((err) => {
|
|
2647
|
+
unavailableEvidence.push(`unavailable:diff-base:${options.diffBaseRef}:${workflowErrorMessage(err)}`);
|
|
2648
|
+
return '';
|
|
2649
|
+
});
|
|
2650
|
+
if (baseCommit) {
|
|
2651
|
+
acceptedDiffBaseRef = options.diffBaseRef;
|
|
2652
|
+
committed = await boundedGitOutput(root, [
|
|
2653
|
+
'diff',
|
|
2654
|
+
'--no-ext-diff',
|
|
2655
|
+
'--patch',
|
|
2656
|
+
'--find-renames',
|
|
2657
|
+
`${baseCommit}..HEAD`,
|
|
2658
|
+
'--',
|
|
2659
|
+
], options.maxDiffBytes).catch((err) => {
|
|
2660
|
+
unavailableEvidence.push(`unavailable:diff-committed:${options.diffBaseRef}:${workflowErrorMessage(err)}`);
|
|
2661
|
+
return { text: '', truncated: false };
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
const diffEvidence = [
|
|
2666
|
+
{ kind: 'unstaged', value: unstaged },
|
|
2667
|
+
{ kind: 'staged', value: staged },
|
|
2668
|
+
{ kind: 'committed', value: committed },
|
|
2669
|
+
];
|
|
2670
|
+
const allowedEvidenceRefs = uniqueStrings([
|
|
2671
|
+
...changedPaths.map((path) => `file:${path}`),
|
|
2672
|
+
...diffEvidence.flatMap((entry) => diffEvidenceRefs(entry.kind, entry.value.text)),
|
|
2673
|
+
]);
|
|
2674
|
+
const allowedEvidenceIndexDigest = fullHash(allowedEvidenceRefs.join('\n'));
|
|
2675
|
+
const sourceSnapshotId = `git:${head}:${fullHash([
|
|
2676
|
+
gitStatus,
|
|
2677
|
+
unstaged.text,
|
|
2678
|
+
staged.text,
|
|
2679
|
+
committed.text,
|
|
2680
|
+
].join('\n\0\n'))}`;
|
|
2681
|
+
const truncation = {
|
|
2682
|
+
unstaged: unstaged.truncated,
|
|
2683
|
+
staged: staged.truncated,
|
|
2684
|
+
committed: committed.truncated,
|
|
2685
|
+
};
|
|
2686
|
+
const contextHash = fullHash(JSON.stringify({
|
|
2687
|
+
root,
|
|
2688
|
+
sourceSnapshotId,
|
|
2689
|
+
gitStatus,
|
|
2690
|
+
acceptedDiffBaseRef,
|
|
2691
|
+
truncation,
|
|
2692
|
+
allowedEvidenceRefs,
|
|
2693
|
+
}));
|
|
2694
|
+
const sections = [
|
|
2695
|
+
`sourceSnapshotId: ${sourceSnapshotId}`,
|
|
2696
|
+
`contextHash: ${contextHash}`,
|
|
2697
|
+
`allowedEvidenceIndexDigest: ${allowedEvidenceIndexDigest}`,
|
|
2698
|
+
`diffBaseRef: ${acceptedDiffBaseRef || '(none)'}`,
|
|
2699
|
+
`truncation: ${JSON.stringify(truncation)}`,
|
|
2700
|
+
'',
|
|
2701
|
+
'#### Changed Files',
|
|
2702
|
+
changedPaths.length ? changedPaths.join('\n') : '(none)',
|
|
2703
|
+
'',
|
|
2704
|
+
'#### Unstaged Diff',
|
|
2705
|
+
unstaged.text || '(none)',
|
|
2706
|
+
'',
|
|
2707
|
+
'#### Staged Diff',
|
|
2708
|
+
staged.text || '(none)',
|
|
2709
|
+
'',
|
|
2710
|
+
'#### Committed Diff',
|
|
2711
|
+
committed.text || (options.diffBaseRef ? '(none)' : '(not requested)'),
|
|
2712
|
+
];
|
|
2713
|
+
return {
|
|
2714
|
+
sourceSnapshotId,
|
|
2715
|
+
contextHash,
|
|
2716
|
+
allowedEvidenceRefs,
|
|
2717
|
+
unavailableEvidence,
|
|
2718
|
+
text: sections.join('\n'),
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
async function boundedGitOutput(root, args, maxBytes) {
|
|
2722
|
+
const text = await gitOutput(root, args);
|
|
2723
|
+
if (Buffer.byteLength(text, 'utf8') <= maxBytes)
|
|
2724
|
+
return { text, truncated: false };
|
|
2725
|
+
return {
|
|
2726
|
+
text: Buffer.from(text, 'utf8').subarray(0, maxBytes).toString('utf8'),
|
|
2727
|
+
truncated: true,
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
function diffEvidenceRefs(kind, diff) {
|
|
2731
|
+
const refs = [];
|
|
2732
|
+
let currentPath = '';
|
|
2733
|
+
let hunkIndex = 0;
|
|
2734
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
2735
|
+
const header = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
|
|
2736
|
+
if (header) {
|
|
2737
|
+
currentPath = header[2] ?? header[1] ?? '';
|
|
2738
|
+
hunkIndex = 0;
|
|
2739
|
+
if (currentPath && currentPath !== '/dev/null')
|
|
2740
|
+
refs.push(`diff:${kind}:${currentPath}`);
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
if (currentPath && line.startsWith('@@')) {
|
|
2744
|
+
hunkIndex += 1;
|
|
2745
|
+
refs.push(`hunk:${kind}:${currentPath}:${hunkIndex}`);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return refs;
|
|
2749
|
+
}
|
|
1845
2750
|
function normalizeWorkspaceContextOptions(value) {
|
|
1846
2751
|
const options = asRecord(value) ?? {};
|
|
1847
2752
|
const query = typeof options.query === 'string' ? options.query : undefined;
|
|
1848
2753
|
const files = Array.isArray(options.files)
|
|
1849
2754
|
? options.files.filter((item) => typeof item === 'string' && item.trim() !== '')
|
|
1850
2755
|
: [];
|
|
2756
|
+
const diffBaseRef = typeof options.diffBaseRef === 'string' && options.diffBaseRef.trim()
|
|
2757
|
+
? options.diffBaseRef.trim()
|
|
2758
|
+
: undefined;
|
|
1851
2759
|
return {
|
|
1852
2760
|
...(query ? { query } : {}),
|
|
1853
2761
|
files,
|
|
2762
|
+
includeDiff: options.includeDiff === true,
|
|
2763
|
+
...(diffBaseRef ? { diffBaseRef } : {}),
|
|
1854
2764
|
maxFiles: boundedPositiveInteger(options.maxFiles, DEFAULT_WORKSPACE_CONTEXT_MAX_FILES, 1, 100),
|
|
1855
2765
|
maxFileBytes: boundedPositiveInteger(options.maxFileBytes, DEFAULT_WORKSPACE_CONTEXT_MAX_FILE_BYTES, 1_000, 50_000),
|
|
1856
2766
|
maxBytes: boundedPositiveInteger(options.maxBytes, DEFAULT_WORKSPACE_CONTEXT_MAX_BYTES, 10_000, 200_000),
|
|
2767
|
+
maxDiffBytes: boundedPositiveInteger(options.maxDiffBytes, DEFAULT_WORKSPACE_CONTEXT_MAX_DIFF_BYTES, 1_000, 200_000),
|
|
1857
2768
|
};
|
|
1858
2769
|
}
|
|
1859
2770
|
function boundedPositiveInteger(value, fallback, min, max) {
|
|
@@ -2039,6 +2950,17 @@ function agentCompletionProgress(ctx, phase) {
|
|
|
2039
2950
|
function shortHash(value) {
|
|
2040
2951
|
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
2041
2952
|
}
|
|
2953
|
+
function fullHash(value) {
|
|
2954
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
2955
|
+
}
|
|
2956
|
+
function workflowValueHash(value) {
|
|
2957
|
+
try {
|
|
2958
|
+
return fullHash(stableJson(value));
|
|
2959
|
+
}
|
|
2960
|
+
catch (err) {
|
|
2961
|
+
throw workflowInputError(workflowErrorMessage(err, 'workflow hash value must be JSON-serializable.'));
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2042
2964
|
function workflowScriptHash(script) {
|
|
2043
2965
|
return `sha256:${createHash('sha256').update(script).digest('hex')}`;
|
|
2044
2966
|
}
|
|
@@ -3030,6 +3952,7 @@ function installWorkflowVmGlobals(context, globals) {
|
|
|
3030
3952
|
' define(globalThis, "agent", { value: (...values) => __host.agent(...values), writable: false, configurable: false });',
|
|
3031
3953
|
' define(globalThis, "parallel", { value: (...values) => __host.parallel(...values), writable: false, configurable: false });',
|
|
3032
3954
|
' define(globalThis, "pipeline", { value: (...values) => __host.pipeline(...values), writable: false, configurable: false });',
|
|
3955
|
+
' define(globalThis, "hash", { value: (...values) => __host.hash(...values), writable: false, configurable: false });',
|
|
3033
3956
|
' define(globalThis, "workspaceContext", { value: (...values) => __host.workspaceContext(...values), writable: false, configurable: false });',
|
|
3034
3957
|
' define(globalThis, "announcePlan", { value: (...values) => __host.announcePlan(...values), writable: false, configurable: false });',
|
|
3035
3958
|
' define(globalThis, "announcePhasePlan", { value: (...values) => __host.announcePhasePlan(...values), writable: false, configurable: false });',
|
|
@@ -3789,6 +4712,7 @@ function workflowAgentSemanticOpts(input) {
|
|
|
3789
4712
|
model: input.model,
|
|
3790
4713
|
effort: input.effort,
|
|
3791
4714
|
...(input.isolation ? { isolation: input.isolation } : {}),
|
|
4715
|
+
...(input.logicalKey ? { logicalKey: input.logicalKey } : {}),
|
|
3792
4716
|
};
|
|
3793
4717
|
}
|
|
3794
4718
|
function workflowUsage(usage) {
|