ultracode-for-codex 0.3.2 → 0.3.4
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 +154 -189
- package/ULTRACODE_INSTALL.md +35 -17
- package/dist/cli.js +74 -25
- package/dist/runtime/state-root.d.ts +3 -0
- package/dist/runtime/state-root.js +29 -0
- package/dist/runtime/workflow-journal.d.ts +1 -0
- package/dist/runtime/workflow-journal.js +5 -2
- package/dist/runtime/workflow-runtime.d.ts +3 -0
- package/dist/runtime/workflow-runtime.js +1529 -46
- package/docs/provenance-audit.md +4 -4
- package/docs/ultracode-p3b-resume-cache.md +27 -8
- package/package.json +1 -1
- package/settings.json +1 -1
- package/skills/ultracode-for-codex-cli/SKILL.md +18 -5
|
@@ -6,7 +6,8 @@ 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
|
+
import { defaultUltracodeStateRoot, defaultWorkflowStateDir } from './state-root.js';
|
|
10
11
|
const MAX_SCRIPT_BYTES = 64 * 1024;
|
|
11
12
|
const MAX_AGENT_CALLS = 1000;
|
|
12
13
|
const MAX_PARALLELISM = 16;
|
|
@@ -14,6 +15,7 @@ const DEFAULT_AGENT_STALL_RETRY_LIMIT = 5;
|
|
|
14
15
|
const DEFAULT_WORKSPACE_CONTEXT_MAX_FILES = 24;
|
|
15
16
|
const DEFAULT_WORKSPACE_CONTEXT_MAX_FILE_BYTES = 12_000;
|
|
16
17
|
const DEFAULT_WORKSPACE_CONTEXT_MAX_BYTES = 80_000;
|
|
18
|
+
const DEFAULT_WORKSPACE_CONTEXT_MAX_DIFF_BYTES = 60_000;
|
|
17
19
|
const execFileAsync = promisify(execFile);
|
|
18
20
|
const PROJECT_WORKFLOW_DIRS = ['.codex/workflows'];
|
|
19
21
|
const WORKFLOW_PERMISSION_REQUIRED_SOURCES = new Set(['script_path', 'project', 'user', 'plugin']);
|
|
@@ -68,6 +70,7 @@ const WORKSPACE_CONTEXT_EXCLUDED_DIRS = new Set([
|
|
|
68
70
|
'node_modules',
|
|
69
71
|
'out',
|
|
70
72
|
]);
|
|
73
|
+
const EMPTY_WORKSPACE_PATH_EXCLUSIONS = new Set();
|
|
71
74
|
const WORKSPACE_CONTEXT_ALLOWED_EXTENSIONS = new Set([
|
|
72
75
|
'.cjs',
|
|
73
76
|
'.css',
|
|
@@ -123,15 +126,7 @@ const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
|
123
126
|
},
|
|
124
127
|
{
|
|
125
128
|
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
|
-
}),
|
|
129
|
+
script: codeReviewBuiltinWorkflowScript(),
|
|
135
130
|
},
|
|
136
131
|
{
|
|
137
132
|
name: 'batch',
|
|
@@ -157,6 +152,740 @@ return await parallel(prompts.map((prompt, index) => () => agent(
|
|
|
157
152
|
)));`,
|
|
158
153
|
},
|
|
159
154
|
];
|
|
155
|
+
function codeReviewBuiltinWorkflowScript() {
|
|
156
|
+
return `export const meta = {
|
|
157
|
+
name: "code-review",
|
|
158
|
+
description: "Run a dynamic evidence-bound code review workflow"
|
|
159
|
+
};
|
|
160
|
+
const workflowInput = args && typeof args === "object" ? args : {};
|
|
161
|
+
const prompt = typeof workflowInput.prompt === "string" && workflowInput.prompt.trim()
|
|
162
|
+
? workflowInput.prompt
|
|
163
|
+
: "Review the current repository for correctness risks.";
|
|
164
|
+
const level = workflowInput.level === "high" ? "high" : "xhigh";
|
|
165
|
+
const caps = level === "high"
|
|
166
|
+
? { maxFinders: 8, maxCandidatesPerLens: 6, sweep: false, reportCap: 10 }
|
|
167
|
+
: { maxFinders: 10, maxCandidatesPerLens: 8, sweep: true, reportCap: 15 };
|
|
168
|
+
const seedLenses = [
|
|
169
|
+
{ id: "diff-correctness", title: "Diff correctness", kind: "correctness", focus: "Inspect touched hunks and enclosing behavior for runtime bugs." },
|
|
170
|
+
{ id: "removed-behavior", title: "Removed behavior", kind: "correctness", focus: "Check deleted or replaced guards, validation, errors, and tests." },
|
|
171
|
+
{ id: "cross-file-contract", title: "Cross-file contract", kind: "contract", focus: "Trace callers, callees, preconditions, and return shapes." },
|
|
172
|
+
{ id: "language-platform", title: "Language/platform pitfalls", kind: "correctness", focus: "Look for language, framework, and environment-sensitive footguns." },
|
|
173
|
+
{ id: "wrapper-delegation", title: "Wrapper/delegation correctness", kind: "contract", focus: "Check adapters, proxies, caches, decorators, and delegation paths." },
|
|
174
|
+
{ id: "security-boundary", title: "Security/capability boundary", kind: "security", focus: "Check authority, permissions, credential handling, and local state exposure." },
|
|
175
|
+
{ id: "persistence-retry-cancel", title: "Persistence/retry/cancel", kind: "persistence", focus: "Check journals, resume/cache, retries, cancellation, and terminal states." },
|
|
176
|
+
{ id: "cli-user-contract", title: "CLI/user contract", kind: "contract", focus: "Check commands, settings, progress, package contents, and documented behavior." },
|
|
177
|
+
{ id: "tests-package-coverage", title: "Tests/package coverage", kind: "coverage", focus: "Check whether tests and packaged artifacts cover changed behavior." },
|
|
178
|
+
{ id: "maintainability", title: "Maintainability/conventions", kind: "maintainability", focus: "Check duplication, altitude, and repository instruction alignment." }
|
|
179
|
+
];
|
|
180
|
+
const scopeSchema = {
|
|
181
|
+
type: "object",
|
|
182
|
+
additionalProperties: false,
|
|
183
|
+
required: ["files", "summary", "instructions", "lensDecisions", "lenses"],
|
|
184
|
+
properties: {
|
|
185
|
+
files: { type: "array", items: { type: "string", minLength: 1, maxLength: 240 } },
|
|
186
|
+
summary: { type: "string", minLength: 1 },
|
|
187
|
+
instructions: { type: ["string", "null"] },
|
|
188
|
+
lensDecisions: {
|
|
189
|
+
type: "array",
|
|
190
|
+
items: {
|
|
191
|
+
type: "object",
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
required: ["seedId", "action", "selectedLensId", "reasonCategory", "decisionRefs", "reason"],
|
|
194
|
+
properties: {
|
|
195
|
+
seedId: { type: "string", minLength: 1 },
|
|
196
|
+
action: { type: "string", enum: ["select", "skip"] },
|
|
197
|
+
selectedLensId: { type: ["string", "null"] },
|
|
198
|
+
reasonCategory: { type: "string", enum: ["matched_change", "prompt_risk", "no_evidence", "cap_limit", "redundant", "out_of_scope", "tiny_change"] },
|
|
199
|
+
decisionRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
200
|
+
reason: { type: "string", minLength: 1 }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
lenses: {
|
|
205
|
+
type: "array",
|
|
206
|
+
items: {
|
|
207
|
+
type: "object",
|
|
208
|
+
additionalProperties: false,
|
|
209
|
+
required: ["id", "title", "focus", "kind"],
|
|
210
|
+
properties: {
|
|
211
|
+
id: { type: "string", minLength: 1, maxLength: 80 },
|
|
212
|
+
title: { type: "string", minLength: 1, maxLength: 120 },
|
|
213
|
+
focus: { type: "string", minLength: 1, maxLength: 1000 },
|
|
214
|
+
kind: { type: "string", enum: ["correctness", "security", "contract", "persistence", "coverage", "maintainability"] }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const finderSchema = {
|
|
221
|
+
type: "object",
|
|
222
|
+
additionalProperties: false,
|
|
223
|
+
required: ["candidates"],
|
|
224
|
+
properties: {
|
|
225
|
+
candidates: {
|
|
226
|
+
type: "array",
|
|
227
|
+
items: {
|
|
228
|
+
type: "object",
|
|
229
|
+
additionalProperties: false,
|
|
230
|
+
required: ["file", "line", "summary", "failureScenario", "evidenceRefs", "kind"],
|
|
231
|
+
properties: {
|
|
232
|
+
file: { type: "string", minLength: 1, maxLength: 240 },
|
|
233
|
+
line: { type: ["integer", "null"], minimum: 1 },
|
|
234
|
+
summary: { type: "string", minLength: 1 },
|
|
235
|
+
failureScenario: { type: "string", minLength: 1 },
|
|
236
|
+
evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
237
|
+
kind: { type: ["string", "null"] }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const verifierSchema = {
|
|
244
|
+
type: "object",
|
|
245
|
+
additionalProperties: false,
|
|
246
|
+
required: ["verdict", "evidence", "evidenceRefs", "severity"],
|
|
247
|
+
properties: {
|
|
248
|
+
verdict: { type: "string", enum: ["CONFIRMED", "PLAUSIBLE", "REFUTED"] },
|
|
249
|
+
evidence: { type: "string", minLength: 1 },
|
|
250
|
+
evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
251
|
+
severity: { type: ["string", "null"], enum: ["P0", "P1", "P2", "P3", null] }
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const synthesisSchema = {
|
|
255
|
+
type: "object",
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
required: ["summary", "decisions"],
|
|
258
|
+
properties: {
|
|
259
|
+
summary: { type: "string", minLength: 1 },
|
|
260
|
+
decisions: {
|
|
261
|
+
type: "array",
|
|
262
|
+
items: {
|
|
263
|
+
type: "object",
|
|
264
|
+
additionalProperties: false,
|
|
265
|
+
required: ["index", "action", "merge", "severity", "reasonCategory", "reason"],
|
|
266
|
+
properties: {
|
|
267
|
+
index: { type: "integer", minimum: 0 },
|
|
268
|
+
action: { type: "string", enum: ["report", "merge", "drop"] },
|
|
269
|
+
merge: { type: ["array", "null"], minItems: 1, items: { type: "integer", minimum: 0 } },
|
|
270
|
+
severity: { type: ["string", "null"], enum: ["P0", "P1", "P2", "P3", null] },
|
|
271
|
+
reasonCategory: { type: "string", enum: ["material", "duplicate", "not_material", "report_cap", "unsupported_evidence", "superseded"] },
|
|
272
|
+
reason: { type: "string", minLength: 1 }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
function fail(message) {
|
|
279
|
+
throw "code-review invalid: " + message;
|
|
280
|
+
}
|
|
281
|
+
function text(value) {
|
|
282
|
+
return value == null ? "" : "" + value;
|
|
283
|
+
}
|
|
284
|
+
function errorText(err) {
|
|
285
|
+
if (err && typeof err.message === "string") return err.message;
|
|
286
|
+
try {
|
|
287
|
+
const json = JSON.stringify(err);
|
|
288
|
+
if (json) return json;
|
|
289
|
+
} catch (_err) {}
|
|
290
|
+
return text(err);
|
|
291
|
+
}
|
|
292
|
+
function lines(value) {
|
|
293
|
+
return text(value).split(/\\r?\\n/);
|
|
294
|
+
}
|
|
295
|
+
function firstLineValue(context, prefix) {
|
|
296
|
+
const all = lines(context);
|
|
297
|
+
for (let index = 0; index < all.length; index += 1) {
|
|
298
|
+
if (all[index].indexOf(prefix) === 0) return all[index].slice(prefix.length).trim();
|
|
299
|
+
}
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
function sectionLines(context, title, endTitles) {
|
|
303
|
+
const all = lines(context);
|
|
304
|
+
let start = -1;
|
|
305
|
+
for (let index = 0; index < all.length; index += 1) {
|
|
306
|
+
if (all[index] === title) {
|
|
307
|
+
start = index + 1;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (start < 0) return [];
|
|
312
|
+
const out = [];
|
|
313
|
+
for (let index = start; index < all.length; index += 1) {
|
|
314
|
+
if (endTitles.indexOf(all[index]) >= 0) break;
|
|
315
|
+
const line = all[index].trim();
|
|
316
|
+
if (line && line !== "(none)") out.push(line);
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
function objectMap(values) {
|
|
321
|
+
const map = {};
|
|
322
|
+
for (let index = 0; index < values.length; index += 1) map[values[index]] = true;
|
|
323
|
+
return map;
|
|
324
|
+
}
|
|
325
|
+
function normalizeKey(value, fallback) {
|
|
326
|
+
const raw = text(value || fallback).trim().toLowerCase();
|
|
327
|
+
const replaced = raw.replace(/[^a-z0-9_.:/@+-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
328
|
+
return replaced ? replaced.slice(0, 80) : fallback;
|
|
329
|
+
}
|
|
330
|
+
function uniquePush(list, item) {
|
|
331
|
+
if (list.indexOf(item) < 0) list.push(item);
|
|
332
|
+
}
|
|
333
|
+
function assertDecisionRefs(refs, label) {
|
|
334
|
+
if (!Array.isArray(refs) || refs.length < 1) fail(label + " must include evidence or decision refs.");
|
|
335
|
+
for (let index = 0; index < refs.length; index += 1) {
|
|
336
|
+
const ref = text(refs[index]);
|
|
337
|
+
if (!allowedEvidenceRefMap[ref] && !unavailableEvidenceRefMap[ref] && ref !== "prompt:request") {
|
|
338
|
+
fail(label + " includes unsupported decision ref " + ref);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function assertEvidenceRefs(refs, label) {
|
|
343
|
+
if (!Array.isArray(refs) || refs.length < 1) fail(label + " must include at least one evidence ref.");
|
|
344
|
+
for (let index = 0; index < refs.length; index += 1) {
|
|
345
|
+
const ref = text(refs[index]);
|
|
346
|
+
if (!allowedEvidenceRefMap[ref]) fail(label + " includes unsupported evidence ref " + ref);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function validateFile(file, label) {
|
|
350
|
+
const ref = "file:" + text(file);
|
|
351
|
+
if (!allowedFileRefMap[ref]) fail(label + " references unsupported file " + text(file));
|
|
352
|
+
}
|
|
353
|
+
function selectedDecisionMatches(decision, lens) {
|
|
354
|
+
if (decision.action !== "select") return false;
|
|
355
|
+
const selected = normalizeKey(decision.selectedLensId || decision.seedId, decision.seedId);
|
|
356
|
+
return selected === lens.lensKey || selected === normalizeKey(lens.id, lens.lensKey);
|
|
357
|
+
}
|
|
358
|
+
function validateScope(scope) {
|
|
359
|
+
for (let index = 0; index < scope.files.length; index += 1) validateFile(scope.files[index], "scope.files[" + index + "]");
|
|
360
|
+
for (let index = 0; index < scope.lensDecisions.length; index += 1) {
|
|
361
|
+
assertDecisionRefs(scope.lensDecisions[index].decisionRefs, "scope.lensDecisions[" + index + "]");
|
|
362
|
+
}
|
|
363
|
+
const selected = [];
|
|
364
|
+
const seen = {};
|
|
365
|
+
for (let index = 0; index < scope.lenses.length; index += 1) {
|
|
366
|
+
const rawLens = scope.lenses[index];
|
|
367
|
+
const lensKey = normalizeKey(rawLens.id, "lens-" + (index + 1));
|
|
368
|
+
if (seen[lensKey]) fail("duplicate selected lens " + lensKey);
|
|
369
|
+
const lens = {
|
|
370
|
+
id: text(rawLens.id),
|
|
371
|
+
lensKey: lensKey,
|
|
372
|
+
title: text(rawLens.title),
|
|
373
|
+
focus: text(rawLens.focus),
|
|
374
|
+
kind: text(rawLens.kind),
|
|
375
|
+
position: index
|
|
376
|
+
};
|
|
377
|
+
let matched = false;
|
|
378
|
+
for (let decisionIndex = 0; decisionIndex < scope.lensDecisions.length; decisionIndex += 1) {
|
|
379
|
+
if (selectedDecisionMatches(scope.lensDecisions[decisionIndex], lens)) matched = true;
|
|
380
|
+
}
|
|
381
|
+
if (!matched) fail("selected lens lacks matching select decision " + lensKey);
|
|
382
|
+
seen[lensKey] = true;
|
|
383
|
+
if (selected.length < caps.maxFinders) selected.push(lens);
|
|
384
|
+
}
|
|
385
|
+
return selected;
|
|
386
|
+
}
|
|
387
|
+
function validateCandidate(candidate, label) {
|
|
388
|
+
validateFile(candidate.file, label + ".file");
|
|
389
|
+
assertEvidenceRefs(candidate.evidenceRefs, label + ".evidenceRefs");
|
|
390
|
+
return {
|
|
391
|
+
file: text(candidate.file),
|
|
392
|
+
line: Number.isInteger(candidate.line) ? candidate.line : null,
|
|
393
|
+
summary: text(candidate.summary),
|
|
394
|
+
failureScenario: text(candidate.failureScenario),
|
|
395
|
+
evidenceRefs: candidate.evidenceRefs.map((item) => text(item)),
|
|
396
|
+
kind: text(candidate.kind || "unspecified")
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function validateVerifier(verifier, label) {
|
|
400
|
+
assertEvidenceRefs(verifier.evidenceRefs, label + ".evidenceRefs");
|
|
401
|
+
return {
|
|
402
|
+
verdict: verifier.verdict,
|
|
403
|
+
evidence: text(verifier.evidence),
|
|
404
|
+
evidenceRefs: verifier.evidenceRefs.map((item) => text(item)),
|
|
405
|
+
severity: verifier.severity || "P2"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function finderLabel(lens) {
|
|
409
|
+
return "code-review-find-" + lens.lensKey;
|
|
410
|
+
}
|
|
411
|
+
function verifierLabel(envelope) {
|
|
412
|
+
return "code-review-verify-" + envelope.lensKey + "-c" + (envelope.candidateIndex + 1);
|
|
413
|
+
}
|
|
414
|
+
function verifierKey(envelope) {
|
|
415
|
+
return [
|
|
416
|
+
"code-review/verify",
|
|
417
|
+
envelope.lensKey,
|
|
418
|
+
"" + envelope.candidateIndex,
|
|
419
|
+
envelope.candidateDigest.slice(7, 23),
|
|
420
|
+
contextHash.slice(7, 23),
|
|
421
|
+
allowedEvidenceIndexDigest.slice(7, 23)
|
|
422
|
+
].join("/");
|
|
423
|
+
}
|
|
424
|
+
function reviewLensStage(lens) {
|
|
425
|
+
return agent([
|
|
426
|
+
"Code-review Finder",
|
|
427
|
+
"Lens: " + lens.title,
|
|
428
|
+
"Lens key: " + lens.lensKey,
|
|
429
|
+
"Focus: " + lens.focus,
|
|
430
|
+
"",
|
|
431
|
+
"Return only concrete defect candidates with a failure scenario and evidence refs from the allowed index.",
|
|
432
|
+
"User request:",
|
|
433
|
+
prompt,
|
|
434
|
+
"",
|
|
435
|
+
"Scope:",
|
|
436
|
+
JSON.stringify(scopeBlock, null, 2),
|
|
437
|
+
"",
|
|
438
|
+
context
|
|
439
|
+
].join("\\n"), {
|
|
440
|
+
label: finderLabel(lens),
|
|
441
|
+
phase: "Find",
|
|
442
|
+
schema: finderSchema,
|
|
443
|
+
key: "code-review/find/" + lens.lensKey + "/" + sourceSnapshotHashKey
|
|
444
|
+
}).then((finderOutput) => {
|
|
445
|
+
const rawCandidates = Array.isArray(finderOutput.candidates) ? finderOutput.candidates : [];
|
|
446
|
+
const capped = rawCandidates.slice(0, caps.maxCandidatesPerLens);
|
|
447
|
+
const envelopes = [];
|
|
448
|
+
for (let index = 0; index < capped.length; index += 1) {
|
|
449
|
+
const candidate = validateCandidate(capped[index], "candidate " + lens.lensKey + "/" + index);
|
|
450
|
+
const candidateDigest = hash({
|
|
451
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
452
|
+
contextHash: contextHash,
|
|
453
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
454
|
+
truncation: truncation,
|
|
455
|
+
scopeDigest: scopeDigest,
|
|
456
|
+
lensKey: lens.lensKey,
|
|
457
|
+
candidateIndex: index,
|
|
458
|
+
candidate: candidate
|
|
459
|
+
});
|
|
460
|
+
envelopes.push({
|
|
461
|
+
candidateId: "candidate_" + lens.lensKey + "_" + (index + 1),
|
|
462
|
+
candidateIndex: index,
|
|
463
|
+
candidateDigest: candidateDigest,
|
|
464
|
+
lensKey: lens.lensKey,
|
|
465
|
+
lensTitle: lens.title,
|
|
466
|
+
candidate: candidate
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (envelopes.length === 0) return [];
|
|
470
|
+
return parallel(envelopes.map((envelope) => () => agent([
|
|
471
|
+
"Code-review Verifier",
|
|
472
|
+
"Verify exactly one candidate. Confirm, refute, or mark plausible using only allowed evidence refs.",
|
|
473
|
+
"Candidate envelope:",
|
|
474
|
+
JSON.stringify(envelope, null, 2),
|
|
475
|
+
"",
|
|
476
|
+
"Review evidence:",
|
|
477
|
+
context
|
|
478
|
+
].join("\\n"), {
|
|
479
|
+
label: verifierLabel(envelope),
|
|
480
|
+
phase: "Verify",
|
|
481
|
+
schema: verifierSchema,
|
|
482
|
+
key: verifierKey(envelope)
|
|
483
|
+
}))).then((verifierResults) => {
|
|
484
|
+
if (verifierResults.length !== envelopes.length) fail("verifier count mismatch for " + lens.lensKey);
|
|
485
|
+
const verified = [];
|
|
486
|
+
for (let index = 0; index < envelopes.length; index += 1) {
|
|
487
|
+
if (verifierResults[index] == null) fail("missing verifier result for " + envelopes[index].candidateId);
|
|
488
|
+
verified.push({
|
|
489
|
+
candidateId: envelopes[index].candidateId,
|
|
490
|
+
candidateIndex: envelopes[index].candidateIndex,
|
|
491
|
+
candidateDigest: envelopes[index].candidateDigest,
|
|
492
|
+
lensKey: envelopes[index].lensKey,
|
|
493
|
+
lensTitle: envelopes[index].lensTitle,
|
|
494
|
+
candidate: envelopes[index].candidate,
|
|
495
|
+
verifier: validateVerifier(verifierResults[index], "verifier " + envelopes[index].candidateId)
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return verified;
|
|
499
|
+
});
|
|
500
|
+
}).catch((err) => ({
|
|
501
|
+
failed: true,
|
|
502
|
+
error: errorText(err)
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
function runSweep(kept, refutedCount) {
|
|
506
|
+
return agent([
|
|
507
|
+
"Code-review Sweep Finder",
|
|
508
|
+
"Find only new material candidates missed by the lens pass.",
|
|
509
|
+
"Kept candidate count: " + kept.length,
|
|
510
|
+
"Refuted candidate count: " + refutedCount,
|
|
511
|
+
"Scope:",
|
|
512
|
+
JSON.stringify(scopeBlock, null, 2),
|
|
513
|
+
"",
|
|
514
|
+
context
|
|
515
|
+
].join("\\n"), {
|
|
516
|
+
label: "code-review-sweep-finder",
|
|
517
|
+
phase: "Sweep",
|
|
518
|
+
schema: finderSchema,
|
|
519
|
+
key: "code-review/sweep/" + sourceSnapshotHashKey + "/" + scopeDigest.slice(7, 23)
|
|
520
|
+
}).then((sweepOutput) => {
|
|
521
|
+
const sweepLens = { id: "sweep", lensKey: "sweep", title: "Sweep", focus: "Final gap search", kind: "correctness", position: activeLenses.length };
|
|
522
|
+
const raw = Array.isArray(sweepOutput.candidates) ? sweepOutput.candidates : [];
|
|
523
|
+
if (raw.length === 0) return [];
|
|
524
|
+
return reviewSweepCandidates(sweepLens, raw.slice(0, 8));
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function reviewSweepCandidates(lens, rawCandidates) {
|
|
528
|
+
const envelopes = [];
|
|
529
|
+
for (let index = 0; index < rawCandidates.length; index += 1) {
|
|
530
|
+
const candidate = validateCandidate(rawCandidates[index], "sweep candidate " + index);
|
|
531
|
+
const candidateDigest = hash({
|
|
532
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
533
|
+
contextHash: contextHash,
|
|
534
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
535
|
+
truncation: truncation,
|
|
536
|
+
scopeDigest: scopeDigest,
|
|
537
|
+
lensKey: "sweep",
|
|
538
|
+
candidateIndex: index,
|
|
539
|
+
candidate: candidate
|
|
540
|
+
});
|
|
541
|
+
envelopes.push({
|
|
542
|
+
candidateId: "candidate_sweep_" + (index + 1),
|
|
543
|
+
candidateIndex: index,
|
|
544
|
+
candidateDigest: candidateDigest,
|
|
545
|
+
lensKey: "sweep",
|
|
546
|
+
lensTitle: "Sweep",
|
|
547
|
+
candidate: candidate
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return parallel(envelopes.map((envelope) => () => agent([
|
|
551
|
+
"Code-review Verifier",
|
|
552
|
+
"Verify exactly one sweep candidate.",
|
|
553
|
+
JSON.stringify(envelope, null, 2),
|
|
554
|
+
"",
|
|
555
|
+
context
|
|
556
|
+
].join("\\n"), {
|
|
557
|
+
label: verifierLabel(envelope),
|
|
558
|
+
phase: "Verify",
|
|
559
|
+
schema: verifierSchema,
|
|
560
|
+
key: verifierKey(envelope)
|
|
561
|
+
}))).then((verifierResults) => {
|
|
562
|
+
if (verifierResults.length !== envelopes.length) fail("sweep verifier count mismatch");
|
|
563
|
+
const verified = [];
|
|
564
|
+
for (let index = 0; index < envelopes.length; index += 1) {
|
|
565
|
+
if (verifierResults[index] == null) fail("missing sweep verifier result");
|
|
566
|
+
verified.push({
|
|
567
|
+
candidateId: envelopes[index].candidateId,
|
|
568
|
+
candidateIndex: envelopes[index].candidateIndex,
|
|
569
|
+
candidateDigest: envelopes[index].candidateDigest,
|
|
570
|
+
lensKey: envelopes[index].lensKey,
|
|
571
|
+
lensTitle: envelopes[index].lensTitle,
|
|
572
|
+
candidate: envelopes[index].candidate,
|
|
573
|
+
verifier: validateVerifier(verifierResults[index], "sweep verifier " + index)
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return verified;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function fallbackDecisions(items, reason) {
|
|
580
|
+
const decisions = [];
|
|
581
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
582
|
+
decisions.push({
|
|
583
|
+
index: index,
|
|
584
|
+
action: index < caps.reportCap ? "report" : "drop",
|
|
585
|
+
severity: items[index].verifier.severity || "P2",
|
|
586
|
+
reasonCategory: index < caps.reportCap ? "material" : "report_cap",
|
|
587
|
+
reason: reason,
|
|
588
|
+
merge: []
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return { mode: "script_fallback", summary: "Script fallback synthesis.", fallbackReason: reason, decisions: decisions };
|
|
592
|
+
}
|
|
593
|
+
function normalizeSynthesis(raw, items) {
|
|
594
|
+
try {
|
|
595
|
+
const decisions = Array.isArray(raw.decisions) ? raw.decisions : [];
|
|
596
|
+
const covered = {};
|
|
597
|
+
const normalized = [];
|
|
598
|
+
for (let index = 0; index < decisions.length; index += 1) {
|
|
599
|
+
const decision = decisions[index];
|
|
600
|
+
if (!Number.isInteger(decision.index) || decision.index < 0 || decision.index >= items.length) fail("synthesis index out of range");
|
|
601
|
+
if (covered[decision.index]) fail("duplicate synthesis coverage");
|
|
602
|
+
covered[decision.index] = true;
|
|
603
|
+
const merge = Array.isArray(decision.merge) ? decision.merge : [];
|
|
604
|
+
if (decision.action === "merge" && merge.length < 1) fail("merge decision requires merge indexes");
|
|
605
|
+
for (let mergeIndex = 0; mergeIndex < merge.length; mergeIndex += 1) {
|
|
606
|
+
if (!Number.isInteger(merge[mergeIndex]) || merge[mergeIndex] < 0 || merge[mergeIndex] >= items.length) {
|
|
607
|
+
fail("merge index out of range");
|
|
608
|
+
}
|
|
609
|
+
if (covered[merge[mergeIndex]]) fail("duplicate merge coverage");
|
|
610
|
+
covered[merge[mergeIndex]] = true;
|
|
611
|
+
}
|
|
612
|
+
normalized.push({
|
|
613
|
+
index: decision.index,
|
|
614
|
+
action: decision.action,
|
|
615
|
+
severity: decision.severity || items[decision.index].verifier.severity || "P2",
|
|
616
|
+
reasonCategory: decision.reasonCategory,
|
|
617
|
+
reason: text(decision.reason),
|
|
618
|
+
merge: merge
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
622
|
+
if (!covered[index]) fail("missing synthesis coverage for index " + index);
|
|
623
|
+
}
|
|
624
|
+
return { mode: "agent", summary: text(raw.summary), fallbackReason: null, decisions: normalized };
|
|
625
|
+
} catch (err) {
|
|
626
|
+
return fallbackDecisions(items, text(err && err.message ? err.message : err));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function finalDecisionRows(synthesis, items) {
|
|
630
|
+
const rows = [];
|
|
631
|
+
for (let index = 0; index < synthesis.decisions.length; index += 1) {
|
|
632
|
+
const decision = synthesis.decisions[index];
|
|
633
|
+
const item = items[decision.index];
|
|
634
|
+
const mergeCandidates = [];
|
|
635
|
+
for (let mergeIndex = 0; mergeIndex < decision.merge.length; mergeIndex += 1) {
|
|
636
|
+
const merged = items[decision.merge[mergeIndex]];
|
|
637
|
+
mergeCandidates.push({ candidateId: merged.candidateId, candidateDigest: merged.candidateDigest });
|
|
638
|
+
}
|
|
639
|
+
rows.push({
|
|
640
|
+
candidateId: item.candidateId,
|
|
641
|
+
candidateDigest: item.candidateDigest,
|
|
642
|
+
action: decision.action,
|
|
643
|
+
reasonCategory: decision.reasonCategory,
|
|
644
|
+
reason: decision.reason,
|
|
645
|
+
mergeCandidates: mergeCandidates,
|
|
646
|
+
severity: decision.severity
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return rows;
|
|
650
|
+
}
|
|
651
|
+
function droppedStats(decisions) {
|
|
652
|
+
const stats = { duplicate: 0, notMaterial: 0, reportCap: 0, unsupportedEvidence: 0, superseded: 0 };
|
|
653
|
+
for (let index = 0; index < decisions.length; index += 1) {
|
|
654
|
+
const row = decisions[index];
|
|
655
|
+
if (row.action !== "drop" && row.action !== "merge") continue;
|
|
656
|
+
if (row.reasonCategory === "duplicate") stats.duplicate += 1;
|
|
657
|
+
else if (row.reasonCategory === "not_material") stats.notMaterial += 1;
|
|
658
|
+
else if (row.reasonCategory === "report_cap") stats.reportCap += 1;
|
|
659
|
+
else if (row.reasonCategory === "unsupported_evidence") stats.unsupportedEvidence += 1;
|
|
660
|
+
else if (row.reasonCategory === "superseded") stats.superseded += 1;
|
|
661
|
+
}
|
|
662
|
+
return stats;
|
|
663
|
+
}
|
|
664
|
+
announcePlan({
|
|
665
|
+
mode: "phase_parallel",
|
|
666
|
+
rationale: "Collect bounded repository evidence, choose review lenses, verify every candidate, then synthesize by index.",
|
|
667
|
+
phases: [{
|
|
668
|
+
id: "scope",
|
|
669
|
+
title: "Scope",
|
|
670
|
+
goal: "Choose active review lenses from runtime-owned evidence.",
|
|
671
|
+
agents: [{ id: "scope", title: "Scope", label: "code-review-scope", focus: "Select lenses and evidence-bound review scope." }]
|
|
672
|
+
}]
|
|
673
|
+
});
|
|
674
|
+
phase("Evidence");
|
|
675
|
+
const context = await workspaceContext({
|
|
676
|
+
query: prompt,
|
|
677
|
+
includeDiff: true,
|
|
678
|
+
diffBaseRef: workflowInput.diffBaseRef
|
|
679
|
+
});
|
|
680
|
+
const allowedEvidenceRefs = sectionLines(context, "### Allowed Evidence Refs", ["### Unavailable Evidence", "### Git Status"]);
|
|
681
|
+
const unavailableEvidenceRefs = sectionLines(context, "### Unavailable Evidence", ["### Git Status"]);
|
|
682
|
+
const allowedEvidenceRefMap = objectMap(allowedEvidenceRefs);
|
|
683
|
+
const unavailableEvidenceRefMap = objectMap(unavailableEvidenceRefs);
|
|
684
|
+
const allowedFileRefs = [];
|
|
685
|
+
for (let index = 0; index < allowedEvidenceRefs.length; index += 1) {
|
|
686
|
+
if (allowedEvidenceRefs[index].indexOf("file:") === 0) uniquePush(allowedFileRefs, allowedEvidenceRefs[index]);
|
|
687
|
+
}
|
|
688
|
+
const allowedFileRefMap = objectMap(allowedFileRefs);
|
|
689
|
+
const sourceSnapshotId = firstLineValue(context, "Source Snapshot: ") || firstLineValue(context, "sourceSnapshotId: ") || hash(context);
|
|
690
|
+
const contextHash = firstLineValue(context, "Context Hash: ") || firstLineValue(context, "contextHash: ") || hash({ context: context });
|
|
691
|
+
const allowedEvidenceIndexDigest = firstLineValue(context, "allowedEvidenceIndexDigest: ") || hash(allowedEvidenceRefs);
|
|
692
|
+
const diffBaseRef = firstLineValue(context, "diffBaseRef: ");
|
|
693
|
+
const truncation = firstLineValue(context, "truncation: ") || "{}";
|
|
694
|
+
const sourceSnapshotHashKey = hash({ sourceSnapshotId: sourceSnapshotId, contextHash: contextHash, allowedEvidenceIndexDigest: allowedEvidenceIndexDigest }).slice(7, 39);
|
|
695
|
+
announcePhasePlan({
|
|
696
|
+
id: "scope",
|
|
697
|
+
title: "Scope",
|
|
698
|
+
goal: "Select active review lenses from the bounded evidence context.",
|
|
699
|
+
agents: [{ id: "scope", title: "Scope", label: "code-review-scope", focus: "Return files, lens decisions, and active lenses." }]
|
|
700
|
+
});
|
|
701
|
+
phase("Scope");
|
|
702
|
+
const scope = await agent([
|
|
703
|
+
"Code-review Scope",
|
|
704
|
+
"Select active lenses from the seed list and the runtime-owned evidence context.",
|
|
705
|
+
"Use decisionRefs only from allowed evidence refs, unavailable evidence refs, or prompt:request.",
|
|
706
|
+
"Level: " + level,
|
|
707
|
+
"Max active lenses: " + caps.maxFinders,
|
|
708
|
+
"",
|
|
709
|
+
"Seed lenses:",
|
|
710
|
+
JSON.stringify(seedLenses, null, 2),
|
|
711
|
+
"",
|
|
712
|
+
"User request:",
|
|
713
|
+
prompt,
|
|
714
|
+
"",
|
|
715
|
+
context
|
|
716
|
+
].join("\\n"), {
|
|
717
|
+
label: "code-review-scope",
|
|
718
|
+
phase: "Scope",
|
|
719
|
+
schema: scopeSchema,
|
|
720
|
+
key: "code-review/scope/" + sourceSnapshotHashKey
|
|
721
|
+
});
|
|
722
|
+
const activeLenses = validateScope(scope);
|
|
723
|
+
const scopeBlock = {
|
|
724
|
+
files: scope.files,
|
|
725
|
+
summary: scope.summary,
|
|
726
|
+
instructions: scope.instructions || "",
|
|
727
|
+
lenses: activeLenses,
|
|
728
|
+
lensDecisions: scope.lensDecisions
|
|
729
|
+
};
|
|
730
|
+
const scopeDigest = hash({ sourceSnapshotId: sourceSnapshotId, contextHash: contextHash, scope: scopeBlock });
|
|
731
|
+
if (activeLenses.length === 0) {
|
|
732
|
+
return {
|
|
733
|
+
level: level,
|
|
734
|
+
provenance: {
|
|
735
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
736
|
+
contextHash: contextHash,
|
|
737
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
738
|
+
diffBaseRef: diffBaseRef || null,
|
|
739
|
+
truncation: { raw: truncation }
|
|
740
|
+
},
|
|
741
|
+
summary: scope.summary,
|
|
742
|
+
findings: [],
|
|
743
|
+
synthesis: { mode: "script_fallback", fallbackReason: "no active lenses", decisions: [] },
|
|
744
|
+
stats: { finders: 0, candidates: 0, verifierAttempts: 0, verified: 0, refuted: 0, invalid: 0, reported: 0, dropped: droppedStats([]) }
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
announcePhasePlan({
|
|
748
|
+
id: "find",
|
|
749
|
+
title: "Find",
|
|
750
|
+
goal: "Run one finder per active review lens.",
|
|
751
|
+
agents: activeLenses.map((lens) => ({
|
|
752
|
+
id: lens.lensKey,
|
|
753
|
+
title: lens.title,
|
|
754
|
+
label: finderLabel(lens),
|
|
755
|
+
focus: lens.focus
|
|
756
|
+
}))
|
|
757
|
+
});
|
|
758
|
+
announcePhasePlan({
|
|
759
|
+
id: "verify",
|
|
760
|
+
title: "Verify",
|
|
761
|
+
goal: "Verify candidates as soon as each finder emits them.",
|
|
762
|
+
agents: [{ id: "dynamic-candidates", title: "Dynamic candidate verifiers", label: "code-review-verify-dynamic", focus: "One verifier runs for each emitted candidate." }]
|
|
763
|
+
});
|
|
764
|
+
phase("Find");
|
|
765
|
+
phase("Verify");
|
|
766
|
+
const lensResults = await pipeline(activeLenses, reviewLensStage);
|
|
767
|
+
const verifiedCandidates = [];
|
|
768
|
+
for (let lensIndex = 0; lensIndex < lensResults.length; lensIndex += 1) {
|
|
769
|
+
if (lensResults[lensIndex] && lensResults[lensIndex].failed) fail(lensResults[lensIndex].error);
|
|
770
|
+
if (lensResults[lensIndex] == null) fail("lens review failed for " + activeLenses[lensIndex].lensKey);
|
|
771
|
+
for (let candidateIndex = 0; candidateIndex < lensResults[lensIndex].length; candidateIndex += 1) {
|
|
772
|
+
verifiedCandidates.push(lensResults[lensIndex][candidateIndex]);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const nonRefuted = [];
|
|
776
|
+
let refuted = 0;
|
|
777
|
+
for (let index = 0; index < verifiedCandidates.length; index += 1) {
|
|
778
|
+
if (verifiedCandidates[index].verifier.verdict === "REFUTED") refuted += 1;
|
|
779
|
+
else nonRefuted.push(verifiedCandidates[index]);
|
|
780
|
+
}
|
|
781
|
+
let sweepResults = [];
|
|
782
|
+
if (caps.sweep) {
|
|
783
|
+
announcePhasePlan({
|
|
784
|
+
id: "sweep",
|
|
785
|
+
title: "Sweep",
|
|
786
|
+
goal: "Search for missed candidates after initial verification.",
|
|
787
|
+
agents: [{ id: "sweep", title: "Sweep Finder", label: "code-review-sweep-finder", focus: "Find only new material missed candidates." }]
|
|
788
|
+
});
|
|
789
|
+
phase("Sweep");
|
|
790
|
+
sweepResults = await runSweep(nonRefuted, refuted);
|
|
791
|
+
for (let index = 0; index < sweepResults.length; index += 1) {
|
|
792
|
+
verifiedCandidates.push(sweepResults[index]);
|
|
793
|
+
if (sweepResults[index].verifier.verdict === "REFUTED") refuted += 1;
|
|
794
|
+
else nonRefuted.push(sweepResults[index]);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
let synthesis = { mode: "script_fallback", summary: "No confirmed or plausible candidates.", fallbackReason: "no reportable candidates", decisions: [] };
|
|
798
|
+
if (nonRefuted.length > 0) {
|
|
799
|
+
announcePhasePlan({
|
|
800
|
+
id: "synthesize",
|
|
801
|
+
title: "Synthesize",
|
|
802
|
+
goal: "Select final findings by verified candidate index.",
|
|
803
|
+
agents: [{ id: "synthesis", title: "Synthesis", label: "code-review-synthesis", focus: "Report, merge, or drop every verified non-refuted candidate." }]
|
|
804
|
+
});
|
|
805
|
+
phase("Synthesize");
|
|
806
|
+
const rawSynthesis = await agent([
|
|
807
|
+
"Code-review Synthesis",
|
|
808
|
+
"Select final findings by index only. Do not invent files, refs, or candidate ids.",
|
|
809
|
+
"Every candidate index must be reported, merged, dropped, or covered as a merge target.",
|
|
810
|
+
"Report cap: " + caps.reportCap,
|
|
811
|
+
"",
|
|
812
|
+
"Verified candidates:",
|
|
813
|
+
JSON.stringify(nonRefuted.map((item, index) => ({
|
|
814
|
+
index: index,
|
|
815
|
+
candidateId: item.candidateId,
|
|
816
|
+
candidateDigest: item.candidateDigest,
|
|
817
|
+
lensKey: item.lensKey,
|
|
818
|
+
file: item.candidate.file,
|
|
819
|
+
line: item.candidate.line,
|
|
820
|
+
summary: item.candidate.summary,
|
|
821
|
+
failureScenario: item.candidate.failureScenario,
|
|
822
|
+
verdict: item.verifier.verdict,
|
|
823
|
+
severity: item.verifier.severity,
|
|
824
|
+
evidenceRefs: item.verifier.evidenceRefs
|
|
825
|
+
})), null, 2)
|
|
826
|
+
].join("\\n"), {
|
|
827
|
+
label: "code-review-synthesis",
|
|
828
|
+
phase: "Synthesize",
|
|
829
|
+
schema: synthesisSchema,
|
|
830
|
+
key: "code-review/synthesis/" + sourceSnapshotHashKey + "/" + scopeDigest.slice(7, 23) + "/" + hash(nonRefuted).slice(7, 23)
|
|
831
|
+
});
|
|
832
|
+
synthesis = normalizeSynthesis(rawSynthesis, nonRefuted);
|
|
833
|
+
}
|
|
834
|
+
const decisionRows = finalDecisionRows(synthesis, nonRefuted);
|
|
835
|
+
const findings = [];
|
|
836
|
+
for (let index = 0; index < synthesis.decisions.length; index += 1) {
|
|
837
|
+
const decision = synthesis.decisions[index];
|
|
838
|
+
if (decision.action !== "report") continue;
|
|
839
|
+
const item = nonRefuted[decision.index];
|
|
840
|
+
const row = decisionRows[index];
|
|
841
|
+
findings.push({
|
|
842
|
+
candidateId: item.candidateId,
|
|
843
|
+
candidateDigest: item.candidateDigest,
|
|
844
|
+
severity: decision.severity || item.verifier.severity || "P2",
|
|
845
|
+
file: item.candidate.file,
|
|
846
|
+
line: item.candidate.line,
|
|
847
|
+
summary: item.candidate.summary,
|
|
848
|
+
failureScenario: item.candidate.failureScenario,
|
|
849
|
+
verdict: item.verifier.verdict,
|
|
850
|
+
evidence: item.verifier.evidence,
|
|
851
|
+
evidenceRefs: item.verifier.evidenceRefs,
|
|
852
|
+
lens: { key: item.lensKey, title: item.lensTitle },
|
|
853
|
+
synthesisDecision: {
|
|
854
|
+
action: decision.action,
|
|
855
|
+
reasonCategory: decision.reasonCategory,
|
|
856
|
+
reason: decision.reason,
|
|
857
|
+
mergeCandidates: row.mergeCandidates
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
level: level,
|
|
863
|
+
provenance: {
|
|
864
|
+
sourceSnapshotId: sourceSnapshotId,
|
|
865
|
+
contextHash: contextHash,
|
|
866
|
+
allowedEvidenceIndexDigest: allowedEvidenceIndexDigest,
|
|
867
|
+
diffBaseRef: diffBaseRef || null,
|
|
868
|
+
truncation: { raw: truncation }
|
|
869
|
+
},
|
|
870
|
+
summary: synthesis.summary,
|
|
871
|
+
findings: findings,
|
|
872
|
+
synthesis: {
|
|
873
|
+
mode: synthesis.mode,
|
|
874
|
+
fallbackReason: synthesis.fallbackReason,
|
|
875
|
+
decisions: decisionRows
|
|
876
|
+
},
|
|
877
|
+
stats: {
|
|
878
|
+
finders: activeLenses.length + (caps.sweep ? 1 : 0),
|
|
879
|
+
candidates: verifiedCandidates.length,
|
|
880
|
+
verifierAttempts: verifiedCandidates.length,
|
|
881
|
+
verified: verifiedCandidates.length,
|
|
882
|
+
refuted: refuted,
|
|
883
|
+
invalid: 0,
|
|
884
|
+
reported: findings.length,
|
|
885
|
+
dropped: droppedStats(decisionRows)
|
|
886
|
+
}
|
|
887
|
+
};`;
|
|
888
|
+
}
|
|
160
889
|
function phaseWiseBuiltinWorkflowScript(input) {
|
|
161
890
|
return `export const meta = {
|
|
162
891
|
name: ${JSON.stringify(input.name)},
|
|
@@ -358,14 +1087,14 @@ export class WorkflowTaskRegistry {
|
|
|
358
1087
|
agentStallRetryLimit;
|
|
359
1088
|
constructor(options) {
|
|
360
1089
|
this.options = options;
|
|
361
|
-
this.stateDir = options.stateDir ??
|
|
1090
|
+
this.stateDir = options.stateDir ?? defaultWorkflowStateDir(options.cwd ?? process.cwd());
|
|
362
1091
|
this.agentStallRetryLimit = normalizeAgentStallRetryLimit(options.agentStallRetryLimit);
|
|
363
1092
|
this.agentStallTimeoutMs = normalizeAgentStallTimeoutMs(options.agentStallTimeoutMs, options.requestTimeoutMs);
|
|
364
1093
|
}
|
|
365
1094
|
async launch(input) {
|
|
366
1095
|
if (this.closed)
|
|
367
1096
|
throw workflowInputError('Workflow runtime is closed.');
|
|
368
|
-
const resumePlan = this.prepareResumePlan(input);
|
|
1097
|
+
const resumePlan = await this.prepareResumePlan(input);
|
|
369
1098
|
let resolved = await this.resolveLaunchInput(resumePlan.launchInput);
|
|
370
1099
|
const parsed = parseInlineWorkflowScript(resolved.script);
|
|
371
1100
|
const scriptHash = workflowScriptHash(resolved.script);
|
|
@@ -462,29 +1191,31 @@ export class WorkflowTaskRegistry {
|
|
|
462
1191
|
scriptHash,
|
|
463
1192
|
};
|
|
464
1193
|
}
|
|
465
|
-
|
|
1194
|
+
async validateResumeSource(resumeFromRunId) {
|
|
1195
|
+
const runId = normalizeResumeFromRunId(resumeFromRunId);
|
|
1196
|
+
const sourceTask = await this.workflowTaskByRunId(runId);
|
|
1197
|
+
if (!sourceTask)
|
|
1198
|
+
throw workflowInputError(`Unknown workflow run for resume: ${runId}`);
|
|
1199
|
+
if (sourceTask.status === 'running')
|
|
1200
|
+
throw workflowResumeRunningError(runId);
|
|
1201
|
+
await this.createResumeCache(sourceTask);
|
|
1202
|
+
}
|
|
1203
|
+
async prepareResumePlan(input) {
|
|
466
1204
|
if (!Object.prototype.hasOwnProperty.call(input, 'resumeFromRunId')) {
|
|
467
1205
|
return { launchInput: input };
|
|
468
1206
|
}
|
|
469
1207
|
const resumeFromRunId = normalizeResumeFromRunId(input.resumeFromRunId);
|
|
470
|
-
const sourceTask = this.workflowTaskByRunId(resumeFromRunId);
|
|
1208
|
+
const sourceTask = await this.workflowTaskByRunId(resumeFromRunId);
|
|
471
1209
|
if (!sourceTask)
|
|
472
1210
|
throw workflowInputError(`Unknown workflow run for resume: ${resumeFromRunId}`);
|
|
473
1211
|
if (sourceTask.status === 'running')
|
|
474
1212
|
throw workflowResumeRunningError(resumeFromRunId);
|
|
1213
|
+
if (workflowLaunchHasSourceSelector(input)) {
|
|
1214
|
+
throw workflowInputError('resumeFromRunId cannot be combined with script, scriptPath, or name. Resume uses the original persisted workflow source.');
|
|
1215
|
+
}
|
|
475
1216
|
const inheritedArgs = !Object.prototype.hasOwnProperty.call(input, 'args') && sourceTask.retryInput.args !== undefined
|
|
476
1217
|
? { args: sourceTask.retryInput.args }
|
|
477
1218
|
: {};
|
|
478
|
-
if (workflowLaunchHasSourceSelector(input)) {
|
|
479
|
-
return {
|
|
480
|
-
sourceTask,
|
|
481
|
-
launchInput: {
|
|
482
|
-
...inheritedArgs,
|
|
483
|
-
...input,
|
|
484
|
-
resumeFromRunId,
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
1219
|
return {
|
|
489
1220
|
sourceTask,
|
|
490
1221
|
launchInput: {
|
|
@@ -496,21 +1227,66 @@ export class WorkflowTaskRegistry {
|
|
|
496
1227
|
},
|
|
497
1228
|
};
|
|
498
1229
|
}
|
|
499
|
-
workflowTaskByRunId(runId) {
|
|
1230
|
+
async workflowTaskByRunId(runId) {
|
|
500
1231
|
for (const task of this.tasks.values()) {
|
|
501
1232
|
if (task.runId === runId)
|
|
502
1233
|
return task;
|
|
503
1234
|
}
|
|
504
|
-
return
|
|
1235
|
+
return await this.durableWorkflowResumeSource(runId);
|
|
505
1236
|
}
|
|
506
|
-
async
|
|
507
|
-
|
|
1237
|
+
async durableWorkflowResumeSource(runId) {
|
|
1238
|
+
const resultPath = join(this.stateDir, 'workflows', `${runId}.result.json`);
|
|
1239
|
+
let record;
|
|
508
1240
|
try {
|
|
509
|
-
|
|
1241
|
+
record = durableWorkflowResultRecordFromUnknown(JSON.parse(await readFile(resultPath, 'utf8')));
|
|
510
1242
|
}
|
|
511
1243
|
catch {
|
|
512
|
-
|
|
1244
|
+
return undefined;
|
|
1245
|
+
}
|
|
1246
|
+
if (!record || record.runId !== runId || !record.retryInput)
|
|
1247
|
+
return undefined;
|
|
1248
|
+
let scriptRecord;
|
|
1249
|
+
try {
|
|
1250
|
+
scriptRecord = await this.readRuntimeWorkflowScript(record.retryInput.scriptPath ?? '');
|
|
513
1251
|
}
|
|
1252
|
+
catch {
|
|
1253
|
+
return undefined;
|
|
1254
|
+
}
|
|
1255
|
+
const actualScriptHash = workflowScriptHash(scriptRecord.script);
|
|
1256
|
+
if (actualScriptHash !== record.scriptHash)
|
|
1257
|
+
return undefined;
|
|
1258
|
+
if (scriptRecord.metadata?.scriptHash !== record.scriptHash)
|
|
1259
|
+
return undefined;
|
|
1260
|
+
if (scriptRecord.metadata?.workflowName !== record.workflowName)
|
|
1261
|
+
return undefined;
|
|
1262
|
+
const transcriptDir = join(this.stateDir, 'subagents', 'workflows', runId);
|
|
1263
|
+
let completedJournal;
|
|
1264
|
+
try {
|
|
1265
|
+
completedJournal = await this.readCompletedResumeJournal({
|
|
1266
|
+
runId,
|
|
1267
|
+
transcriptDir,
|
|
1268
|
+
workflowName: record.workflowName,
|
|
1269
|
+
scriptHash: record.scriptHash,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
catch {
|
|
1273
|
+
return undefined;
|
|
1274
|
+
}
|
|
1275
|
+
if (!durableScriptRecordMatchesJournal(scriptRecord, completedJournal.started))
|
|
1276
|
+
return undefined;
|
|
1277
|
+
if (!durableRetryInputArgsMatchJournal(record.retryInput, completedJournal.started.args))
|
|
1278
|
+
return undefined;
|
|
1279
|
+
return {
|
|
1280
|
+
runId,
|
|
1281
|
+
status: 'completed',
|
|
1282
|
+
transcriptDir,
|
|
1283
|
+
retryInput: durableRetryInputWithJournalArgs(record.retryInput, completedJournal.started.args),
|
|
1284
|
+
workflowName: record.workflowName,
|
|
1285
|
+
scriptHash: record.scriptHash,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async createResumeCache(sourceTask) {
|
|
1289
|
+
const { entries } = await this.readCompletedResumeJournal(sourceTask);
|
|
514
1290
|
const completedByCallKey = new Map();
|
|
515
1291
|
for (const entry of entries) {
|
|
516
1292
|
if (entry.kind === 'workflow.agent.completed')
|
|
@@ -530,10 +1306,36 @@ export class WorkflowTaskRegistry {
|
|
|
530
1306
|
}
|
|
531
1307
|
return {
|
|
532
1308
|
entries: cacheEntries,
|
|
1309
|
+
byCallKey: new Map(cacheEntries.map((entry) => [entry.agentCallKey, entry])),
|
|
1310
|
+
usedCallKeys: new Set(),
|
|
533
1311
|
nextIndex: 0,
|
|
534
1312
|
prefixOpen: true,
|
|
535
1313
|
};
|
|
536
1314
|
}
|
|
1315
|
+
async readCompletedResumeJournal(sourceTask) {
|
|
1316
|
+
let journal;
|
|
1317
|
+
try {
|
|
1318
|
+
journal = await readWorkflowJournal(workflowJournalPath(sourceTask.transcriptDir));
|
|
1319
|
+
}
|
|
1320
|
+
catch {
|
|
1321
|
+
throw workflowInputError(`Workflow run cannot be used as a resume source: ${sourceTask.runId}`);
|
|
1322
|
+
}
|
|
1323
|
+
const entries = journal.entries;
|
|
1324
|
+
const started = entries[0];
|
|
1325
|
+
const terminal = entries.at(-1);
|
|
1326
|
+
if (journal.truncatedTail
|
|
1327
|
+
|| !started
|
|
1328
|
+
|| started.kind !== 'workflow.run.started'
|
|
1329
|
+
|| started.runId !== sourceTask.runId
|
|
1330
|
+
|| (sourceTask.scriptHash && started.scriptHash !== sourceTask.scriptHash)
|
|
1331
|
+
|| (sourceTask.workflowName && started.workflowName !== sourceTask.workflowName)
|
|
1332
|
+
|| !terminal
|
|
1333
|
+
|| terminal.kind !== 'workflow.run.completed'
|
|
1334
|
+
|| terminal.runId !== sourceTask.runId) {
|
|
1335
|
+
throw workflowInputError(`Workflow run cannot be used as a resume source: ${sourceTask.runId}`);
|
|
1336
|
+
}
|
|
1337
|
+
return { entries, started };
|
|
1338
|
+
}
|
|
537
1339
|
async resolveLaunchInput(input) {
|
|
538
1340
|
const normalized = normalizeLaunchInput(input);
|
|
539
1341
|
if (normalized.scriptPath) {
|
|
@@ -1003,6 +1805,7 @@ export class WorkflowTaskRegistry {
|
|
|
1003
1805
|
workflowSource: task.workflowSource,
|
|
1004
1806
|
...(task.workflowSourcePath ? { workflowSourcePath: task.workflowSourcePath } : {}),
|
|
1005
1807
|
scriptHash: task.scriptHash,
|
|
1808
|
+
retryInput: durableWorkflowRetryInput(task.retryInput),
|
|
1006
1809
|
result: journalResult,
|
|
1007
1810
|
}, null, 2)}\n`);
|
|
1008
1811
|
const completedSnapshot = await this.completeTask(ctx, journalResult, {
|
|
@@ -1086,6 +1889,7 @@ export class WorkflowTaskRegistry {
|
|
|
1086
1889
|
host.pipeline = hardenCallable((items, ...stages) => {
|
|
1087
1890
|
return this.trackWorkflowPromise(ctx, this.pipeline(ctx, items, stages));
|
|
1088
1891
|
});
|
|
1892
|
+
host.hash = hardenCallable((value) => workflowValueHash(value));
|
|
1089
1893
|
host.workspaceContext = hardenCallable((options) => {
|
|
1090
1894
|
return this.trackWorkflowPromise(ctx, this.workspaceContext(ctx, options));
|
|
1091
1895
|
});
|
|
@@ -1151,6 +1955,7 @@ export class WorkflowTaskRegistry {
|
|
|
1151
1955
|
}
|
|
1152
1956
|
const schema = normalizeStructuredOutputSchema(options?.schema);
|
|
1153
1957
|
const isolation = normalizeAgentIsolation(options?.isolation);
|
|
1958
|
+
const logicalKey = normalizeAgentLogicalKey(options?.key);
|
|
1154
1959
|
if (isolation && !workflowIsolationReviewAllowsMode(ctx.isolationReview, isolation)) {
|
|
1155
1960
|
throw workflowInputError(`agent ${isolation} isolation was not covered by the current workflow permission review.`);
|
|
1156
1961
|
}
|
|
@@ -1162,6 +1967,7 @@ export class WorkflowTaskRegistry {
|
|
|
1162
1967
|
effort: 'xhigh',
|
|
1163
1968
|
schema,
|
|
1164
1969
|
isolation,
|
|
1970
|
+
logicalKey,
|
|
1165
1971
|
});
|
|
1166
1972
|
const previousAgentCallKey = ctx.previousAgentCallKey;
|
|
1167
1973
|
const agentCallKey = computeWorkflowAgentCallKey({
|
|
@@ -1762,6 +2568,9 @@ function normalizeResumeFromRunId(value) {
|
|
|
1762
2568
|
const runId = value.trim();
|
|
1763
2569
|
if (!runId)
|
|
1764
2570
|
throw workflowInputError('resumeFromRunId must be a non-empty workflow runId string.');
|
|
2571
|
+
if (!/^run_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(runId)) {
|
|
2572
|
+
throw workflowInputError('resumeFromRunId must be a workflow runId in run_<uuid> format.');
|
|
2573
|
+
}
|
|
1765
2574
|
return runId;
|
|
1766
2575
|
}
|
|
1767
2576
|
function normalizeAgentIsolation(value) {
|
|
@@ -1771,25 +2580,50 @@ function normalizeAgentIsolation(value) {
|
|
|
1771
2580
|
return 'worktree';
|
|
1772
2581
|
throw workflowInputError('agent isolation must be "worktree" when provided.');
|
|
1773
2582
|
}
|
|
2583
|
+
function normalizeAgentLogicalKey(value) {
|
|
2584
|
+
if (value === undefined)
|
|
2585
|
+
return undefined;
|
|
2586
|
+
if (typeof value !== 'string')
|
|
2587
|
+
throw workflowInputError('agent key must be a non-empty string when provided.');
|
|
2588
|
+
const key = value.trim();
|
|
2589
|
+
if (!key)
|
|
2590
|
+
throw workflowInputError('agent key must be a non-empty string when provided.');
|
|
2591
|
+
if (key.length > 160)
|
|
2592
|
+
throw workflowInputError('agent key must be at most 160 characters.');
|
|
2593
|
+
if (!/^[A-Za-z0-9_.:/@+-]+$/.test(key)) {
|
|
2594
|
+
throw workflowInputError('agent key may only contain letters, numbers, "_", "-", ".", ":", "/", "@", and "+".');
|
|
2595
|
+
}
|
|
2596
|
+
return key;
|
|
2597
|
+
}
|
|
1774
2598
|
function takeResumeCacheHit(cache, agentCallKey) {
|
|
1775
|
-
if (!cache
|
|
2599
|
+
if (!cache)
|
|
1776
2600
|
return null;
|
|
1777
|
-
|
|
1778
|
-
|
|
2601
|
+
if (cache.prefixOpen) {
|
|
2602
|
+
const entry = cache.entries[cache.nextIndex];
|
|
2603
|
+
if (entry?.agentCallKey === agentCallKey && !cache.usedCallKeys.has(agentCallKey)) {
|
|
2604
|
+
cache.usedCallKeys.add(agentCallKey);
|
|
2605
|
+
cache.nextIndex += 1;
|
|
2606
|
+
return entry;
|
|
2607
|
+
}
|
|
1779
2608
|
cache.prefixOpen = false;
|
|
1780
|
-
return null;
|
|
1781
2609
|
}
|
|
1782
|
-
cache.
|
|
1783
|
-
|
|
2610
|
+
const keyed = cache.byCallKey.get(agentCallKey);
|
|
2611
|
+
if (!keyed || cache.usedCallKeys.has(agentCallKey))
|
|
2612
|
+
return null;
|
|
2613
|
+
cache.usedCallKeys.add(agentCallKey);
|
|
2614
|
+
return keyed;
|
|
1784
2615
|
}
|
|
1785
2616
|
async function gitOutput(cwd, args) {
|
|
2617
|
+
return (await gitOutputRaw(cwd, args)).trim();
|
|
2618
|
+
}
|
|
2619
|
+
async function gitOutputRaw(cwd, args) {
|
|
1786
2620
|
try {
|
|
1787
2621
|
const result = await execFileAsync('git', args, {
|
|
1788
2622
|
cwd,
|
|
1789
2623
|
encoding: 'utf8',
|
|
1790
2624
|
maxBuffer: 1024 * 1024,
|
|
1791
2625
|
});
|
|
1792
|
-
return result.stdout
|
|
2626
|
+
return result.stdout;
|
|
1793
2627
|
}
|
|
1794
2628
|
catch (err) {
|
|
1795
2629
|
const record = err;
|
|
@@ -1800,19 +2634,39 @@ async function gitOutput(cwd, args) {
|
|
|
1800
2634
|
}
|
|
1801
2635
|
async function buildWorkspaceContext(cwd, options) {
|
|
1802
2636
|
const root = await workspaceContextRoot(cwd);
|
|
1803
|
-
const
|
|
2637
|
+
const runtimeStateExcludedPaths = workspaceRuntimeStateExcludedPaths(root);
|
|
2638
|
+
const statusUnavailableEvidence = [];
|
|
2639
|
+
let gitStatusRaw = '';
|
|
2640
|
+
try {
|
|
2641
|
+
gitStatusRaw = await gitOutputRaw(root, ['status', '--short', '--untracked-files=all', '--', '.']);
|
|
2642
|
+
}
|
|
2643
|
+
catch (err) {
|
|
2644
|
+
statusUnavailableEvidence.push(`unavailable:git-status:${gitFailureToken(err)}`);
|
|
2645
|
+
}
|
|
2646
|
+
const gitStatus = statusUnavailableEvidence.length
|
|
2647
|
+
? `(unavailable: ${statusUnavailableEvidence[0]})`
|
|
2648
|
+
: formatGitStatusDisplay(gitStatusRaw, runtimeStateExcludedPaths);
|
|
2649
|
+
const gitStatusPaths = await gitOutputRaw(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--', '.']).catch((err) => {
|
|
2650
|
+
statusUnavailableEvidence.push(`unavailable:git-status-raw:${gitFailureToken(err)}`);
|
|
2651
|
+
return gitStatusRaw;
|
|
2652
|
+
});
|
|
2653
|
+
const gitStatusPathParse = parseGitStatusPaths(gitStatusPaths, runtimeStateExcludedPaths);
|
|
2654
|
+
const excludedWorkspacePaths = new Set(gitStatusPathParse.excludedPaths.map(workspacePathKey));
|
|
2655
|
+
const reviewEvidence = options.includeDiff
|
|
2656
|
+
? await buildReviewEvidenceContext(root, gitStatusPaths, options, statusUnavailableEvidence, runtimeStateExcludedPaths)
|
|
2657
|
+
: undefined;
|
|
1804
2658
|
const explicitPaths = [
|
|
1805
2659
|
...options.files,
|
|
1806
2660
|
...extractMentionedWorkspacePaths(options.query ?? ''),
|
|
1807
2661
|
];
|
|
1808
|
-
const changedPaths =
|
|
2662
|
+
const changedPaths = gitStatusPathParse.paths.filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths));
|
|
1809
2663
|
const listedPaths = await listWorkspaceContextCandidates(root);
|
|
1810
2664
|
const candidates = uniqueStrings([
|
|
1811
2665
|
...explicitPaths,
|
|
1812
2666
|
...changedPaths,
|
|
1813
2667
|
...WORKSPACE_CONTEXT_PRIORITY_FILES,
|
|
1814
2668
|
...listedPaths,
|
|
1815
|
-
]).filter(shouldIncludeWorkspaceContextPath);
|
|
2669
|
+
]).filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths) && !excludedWorkspacePaths.has(workspacePathKey(path)));
|
|
1816
2670
|
const fileBlocks = [];
|
|
1817
2671
|
let usedBytes = 0;
|
|
1818
2672
|
for (const candidate of candidates) {
|
|
@@ -1834,26 +2688,259 @@ async function buildWorkspaceContext(cwd, options) {
|
|
|
1834
2688
|
return [
|
|
1835
2689
|
'## Workspace Context',
|
|
1836
2690
|
`Root: ${root}`,
|
|
2691
|
+
...(reviewEvidence ? [
|
|
2692
|
+
`Source Snapshot: ${reviewEvidence.sourceSnapshotId}`,
|
|
2693
|
+
`Context Hash: ${reviewEvidence.contextHash}`,
|
|
2694
|
+
'',
|
|
2695
|
+
'### Review Evidence',
|
|
2696
|
+
reviewEvidence.text,
|
|
2697
|
+
'',
|
|
2698
|
+
'### Allowed Evidence Refs',
|
|
2699
|
+
reviewEvidence.allowedEvidenceRefs.length ? reviewEvidence.allowedEvidenceRefs.join('\n') : '(none)',
|
|
2700
|
+
'',
|
|
2701
|
+
'### Unavailable Evidence',
|
|
2702
|
+
reviewEvidence.unavailableEvidence.length ? reviewEvidence.unavailableEvidence.join('\n') : '(none)',
|
|
2703
|
+
] : []),
|
|
1837
2704
|
'',
|
|
1838
2705
|
'### Git Status',
|
|
1839
|
-
gitStatus
|
|
2706
|
+
gitStatus,
|
|
1840
2707
|
'',
|
|
1841
2708
|
'### Included Files',
|
|
1842
2709
|
fileBlocks.length ? fileBlocks.join('\n\n') : '(no readable text files selected)',
|
|
1843
2710
|
].join('\n');
|
|
1844
2711
|
}
|
|
2712
|
+
async function buildReviewEvidenceContext(root, gitStatus, options, initialUnavailableEvidence = [], runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
2713
|
+
const unavailableEvidence = [...initialUnavailableEvidence];
|
|
2714
|
+
const gitStatusPaths = parseGitStatusPaths(gitStatus, runtimeStateExcludedPaths);
|
|
2715
|
+
const changedPaths = gitStatusPaths.paths.filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths));
|
|
2716
|
+
const excludedDiffPaths = new Set([
|
|
2717
|
+
...gitStatusPaths.excludedPaths.map(workspacePathKey),
|
|
2718
|
+
...runtimeStateExcludedPaths,
|
|
2719
|
+
]);
|
|
2720
|
+
unavailableEvidence.push(...gitStatusPaths.unavailableEvidence);
|
|
2721
|
+
const head = await gitOutput(root, ['rev-parse', '--verify', 'HEAD']).catch((err) => {
|
|
2722
|
+
unavailableEvidence.push(unavailableGitEvidence('git-head', err));
|
|
2723
|
+
return 'unavailable';
|
|
2724
|
+
});
|
|
2725
|
+
const unstaged = filterWorkspaceContextDiff(await boundedGitOutput(root, [
|
|
2726
|
+
'diff',
|
|
2727
|
+
'--no-ext-diff',
|
|
2728
|
+
'--patch',
|
|
2729
|
+
'--find-renames',
|
|
2730
|
+
'--',
|
|
2731
|
+
], options.maxDiffBytes).catch((err) => {
|
|
2732
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-unstaged', err));
|
|
2733
|
+
return { text: '', truncated: false };
|
|
2734
|
+
}), excludedDiffPaths, runtimeStateExcludedPaths);
|
|
2735
|
+
const staged = filterWorkspaceContextDiff(await boundedGitOutput(root, [
|
|
2736
|
+
'diff',
|
|
2737
|
+
'--cached',
|
|
2738
|
+
'--no-ext-diff',
|
|
2739
|
+
'--patch',
|
|
2740
|
+
'--find-renames',
|
|
2741
|
+
'--',
|
|
2742
|
+
], options.maxDiffBytes).catch((err) => {
|
|
2743
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-staged', err));
|
|
2744
|
+
return { text: '', truncated: false };
|
|
2745
|
+
}), excludedDiffPaths, runtimeStateExcludedPaths);
|
|
2746
|
+
let committed = { text: '', truncated: false };
|
|
2747
|
+
let acceptedDiffBaseRef = '';
|
|
2748
|
+
if (options.diffBaseRef) {
|
|
2749
|
+
const baseCommit = await gitOutput(root, ['rev-parse', '--verify', `${options.diffBaseRef}^{commit}`]).catch((err) => {
|
|
2750
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-base', err, options.diffBaseRef));
|
|
2751
|
+
return '';
|
|
2752
|
+
});
|
|
2753
|
+
if (baseCommit) {
|
|
2754
|
+
acceptedDiffBaseRef = options.diffBaseRef;
|
|
2755
|
+
committed = filterWorkspaceContextDiff(await boundedGitOutput(root, [
|
|
2756
|
+
'diff',
|
|
2757
|
+
'--no-ext-diff',
|
|
2758
|
+
'--patch',
|
|
2759
|
+
'--find-renames',
|
|
2760
|
+
`${baseCommit}..HEAD`,
|
|
2761
|
+
'--',
|
|
2762
|
+
], options.maxDiffBytes).catch((err) => {
|
|
2763
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-committed', err, options.diffBaseRef));
|
|
2764
|
+
return { text: '', truncated: false };
|
|
2765
|
+
}), excludedDiffPaths, runtimeStateExcludedPaths);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const diffEvidence = [
|
|
2769
|
+
{ kind: 'unstaged', value: unstaged },
|
|
2770
|
+
{ kind: 'staged', value: staged },
|
|
2771
|
+
{ kind: 'committed', value: committed },
|
|
2772
|
+
];
|
|
2773
|
+
const allowedEvidenceRefs = uniqueStrings([
|
|
2774
|
+
...changedPaths.map((path) => `file:${path}`),
|
|
2775
|
+
...diffEvidence.flatMap((entry) => diffEvidenceRefs(entry.kind, entry.value.text, runtimeStateExcludedPaths)),
|
|
2776
|
+
]);
|
|
2777
|
+
const allowedEvidenceIndexDigest = fullHash(allowedEvidenceRefs.join('\n'));
|
|
2778
|
+
const sourceSnapshotId = `git:${head}:${fullHash([
|
|
2779
|
+
gitStatus,
|
|
2780
|
+
unstaged.text,
|
|
2781
|
+
staged.text,
|
|
2782
|
+
committed.text,
|
|
2783
|
+
].join('\n\0\n'))}`;
|
|
2784
|
+
const truncation = {
|
|
2785
|
+
unstaged: unstaged.truncated,
|
|
2786
|
+
staged: staged.truncated,
|
|
2787
|
+
committed: committed.truncated,
|
|
2788
|
+
};
|
|
2789
|
+
const contextHash = fullHash(JSON.stringify({
|
|
2790
|
+
root,
|
|
2791
|
+
sourceSnapshotId,
|
|
2792
|
+
gitStatus,
|
|
2793
|
+
acceptedDiffBaseRef,
|
|
2794
|
+
truncation,
|
|
2795
|
+
allowedEvidenceRefs,
|
|
2796
|
+
unavailableEvidence,
|
|
2797
|
+
}));
|
|
2798
|
+
const sections = [
|
|
2799
|
+
`sourceSnapshotId: ${sourceSnapshotId}`,
|
|
2800
|
+
`contextHash: ${contextHash}`,
|
|
2801
|
+
`allowedEvidenceIndexDigest: ${allowedEvidenceIndexDigest}`,
|
|
2802
|
+
`diffBaseRef: ${acceptedDiffBaseRef || '(none)'}`,
|
|
2803
|
+
`truncation: ${JSON.stringify(truncation)}`,
|
|
2804
|
+
'',
|
|
2805
|
+
'#### Changed Files',
|
|
2806
|
+
changedPaths.length ? changedPaths.join('\n') : '(none)',
|
|
2807
|
+
'',
|
|
2808
|
+
'#### Unstaged Diff',
|
|
2809
|
+
unstaged.text || '(none)',
|
|
2810
|
+
'',
|
|
2811
|
+
'#### Staged Diff',
|
|
2812
|
+
staged.text || '(none)',
|
|
2813
|
+
'',
|
|
2814
|
+
'#### Committed Diff',
|
|
2815
|
+
committed.text || (options.diffBaseRef ? '(none)' : '(not requested)'),
|
|
2816
|
+
];
|
|
2817
|
+
return {
|
|
2818
|
+
sourceSnapshotId,
|
|
2819
|
+
contextHash,
|
|
2820
|
+
allowedEvidenceRefs,
|
|
2821
|
+
unavailableEvidence,
|
|
2822
|
+
text: sections.join('\n'),
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
async function boundedGitOutput(root, args, maxBytes) {
|
|
2826
|
+
const text = await gitOutput(root, args);
|
|
2827
|
+
if (Buffer.byteLength(text, 'utf8') <= maxBytes)
|
|
2828
|
+
return { text, truncated: false };
|
|
2829
|
+
return {
|
|
2830
|
+
text: Buffer.from(text, 'utf8').subarray(0, maxBytes).toString('utf8'),
|
|
2831
|
+
truncated: true,
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
function filterWorkspaceContextDiff(value, excludedPaths, runtimeStateExcludedPaths) {
|
|
2835
|
+
if (!value.text)
|
|
2836
|
+
return value;
|
|
2837
|
+
return {
|
|
2838
|
+
...value,
|
|
2839
|
+
text: filterWorkspaceContextDiffText(value.text, excludedPaths, runtimeStateExcludedPaths),
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
function filterWorkspaceContextDiffText(text, excludedPaths, runtimeStateExcludedPaths) {
|
|
2843
|
+
const kept = [];
|
|
2844
|
+
let block = [];
|
|
2845
|
+
let includeBlock = true;
|
|
2846
|
+
const flush = () => {
|
|
2847
|
+
if (includeBlock && block.length > 0)
|
|
2848
|
+
kept.push(block.join('\n'));
|
|
2849
|
+
block = [];
|
|
2850
|
+
};
|
|
2851
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2852
|
+
if (line.startsWith('diff --git ')) {
|
|
2853
|
+
flush();
|
|
2854
|
+
const header = parseGitDiffHeader(line);
|
|
2855
|
+
includeBlock = header
|
|
2856
|
+
? workspaceContextDiffPathAllowed(header.oldPath, excludedPaths, runtimeStateExcludedPaths)
|
|
2857
|
+
&& workspaceContextDiffPathAllowed(header.newPath, excludedPaths, runtimeStateExcludedPaths)
|
|
2858
|
+
: false;
|
|
2859
|
+
}
|
|
2860
|
+
block.push(line);
|
|
2861
|
+
}
|
|
2862
|
+
flush();
|
|
2863
|
+
return kept.join('\n');
|
|
2864
|
+
}
|
|
2865
|
+
function workspaceContextDiffPathAllowed(path, excludedPaths, runtimeStateExcludedPaths) {
|
|
2866
|
+
if (!path || path === '/dev/null')
|
|
2867
|
+
return true;
|
|
2868
|
+
const key = workspacePathKey(path);
|
|
2869
|
+
return shouldIncludeWorkspaceContextPath(key, runtimeStateExcludedPaths) && !workspacePathExcludedBySet(key, excludedPaths);
|
|
2870
|
+
}
|
|
2871
|
+
function diffEvidenceRefs(kind, diff, runtimeStateExcludedPaths) {
|
|
2872
|
+
const refs = [];
|
|
2873
|
+
let currentPath = '';
|
|
2874
|
+
let hunkIndex = 0;
|
|
2875
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
2876
|
+
const header = parseGitDiffHeader(line);
|
|
2877
|
+
if (header) {
|
|
2878
|
+
currentPath = header.newPath || header.oldPath;
|
|
2879
|
+
hunkIndex = 0;
|
|
2880
|
+
if (currentPath && currentPath !== '/dev/null' && shouldIncludeWorkspaceContextPath(currentPath, runtimeStateExcludedPaths))
|
|
2881
|
+
refs.push(`diff:${kind}:${currentPath}`);
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
if (currentPath && shouldIncludeWorkspaceContextPath(currentPath, runtimeStateExcludedPaths) && line.startsWith('@@')) {
|
|
2885
|
+
hunkIndex += 1;
|
|
2886
|
+
refs.push(`hunk:${kind}:${currentPath}:${hunkIndex}`);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
return refs;
|
|
2890
|
+
}
|
|
2891
|
+
function parseGitDiffHeader(line) {
|
|
2892
|
+
if (!line.startsWith('diff --git '))
|
|
2893
|
+
return undefined;
|
|
2894
|
+
const first = readGitDiffHeaderToken(line.slice('diff --git '.length));
|
|
2895
|
+
if (!first)
|
|
2896
|
+
return undefined;
|
|
2897
|
+
const second = readGitDiffHeaderToken(first.rest.trimStart());
|
|
2898
|
+
if (!second || second.rest.trim())
|
|
2899
|
+
return undefined;
|
|
2900
|
+
const oldPath = gitDiffHeaderTokenPath(first.token, 'a/');
|
|
2901
|
+
const newPath = gitDiffHeaderTokenPath(second.token, 'b/');
|
|
2902
|
+
if (oldPath === undefined || newPath === undefined)
|
|
2903
|
+
return undefined;
|
|
2904
|
+
return { oldPath, newPath };
|
|
2905
|
+
}
|
|
2906
|
+
function readGitDiffHeaderToken(value) {
|
|
2907
|
+
if (!value)
|
|
2908
|
+
return undefined;
|
|
2909
|
+
if (value.startsWith('"')) {
|
|
2910
|
+
const end = gitQuotedPathEnd(value);
|
|
2911
|
+
if (end === -1)
|
|
2912
|
+
return undefined;
|
|
2913
|
+
return { token: value.slice(0, end), rest: value.slice(end) };
|
|
2914
|
+
}
|
|
2915
|
+
const separator = value.indexOf(' ');
|
|
2916
|
+
if (separator === -1)
|
|
2917
|
+
return { token: value, rest: '' };
|
|
2918
|
+
return { token: value.slice(0, separator), rest: value.slice(separator + 1) };
|
|
2919
|
+
}
|
|
2920
|
+
function gitDiffHeaderTokenPath(token, prefix) {
|
|
2921
|
+
const path = normalizeGitStatusPath(token);
|
|
2922
|
+
if (!path.startsWith(prefix))
|
|
2923
|
+
return undefined;
|
|
2924
|
+
return path.slice(prefix.length);
|
|
2925
|
+
}
|
|
1845
2926
|
function normalizeWorkspaceContextOptions(value) {
|
|
1846
2927
|
const options = asRecord(value) ?? {};
|
|
1847
2928
|
const query = typeof options.query === 'string' ? options.query : undefined;
|
|
1848
2929
|
const files = Array.isArray(options.files)
|
|
1849
2930
|
? options.files.filter((item) => typeof item === 'string' && item.trim() !== '')
|
|
1850
2931
|
: [];
|
|
2932
|
+
const diffBaseRef = typeof options.diffBaseRef === 'string' && options.diffBaseRef.trim()
|
|
2933
|
+
? options.diffBaseRef.trim()
|
|
2934
|
+
: undefined;
|
|
1851
2935
|
return {
|
|
1852
2936
|
...(query ? { query } : {}),
|
|
1853
2937
|
files,
|
|
2938
|
+
includeDiff: options.includeDiff === true,
|
|
2939
|
+
...(diffBaseRef ? { diffBaseRef } : {}),
|
|
1854
2940
|
maxFiles: boundedPositiveInteger(options.maxFiles, DEFAULT_WORKSPACE_CONTEXT_MAX_FILES, 1, 100),
|
|
1855
2941
|
maxFileBytes: boundedPositiveInteger(options.maxFileBytes, DEFAULT_WORKSPACE_CONTEXT_MAX_FILE_BYTES, 1_000, 50_000),
|
|
1856
2942
|
maxBytes: boundedPositiveInteger(options.maxBytes, DEFAULT_WORKSPACE_CONTEXT_MAX_BYTES, 10_000, 200_000),
|
|
2943
|
+
maxDiffBytes: boundedPositiveInteger(options.maxDiffBytes, DEFAULT_WORKSPACE_CONTEXT_MAX_DIFF_BYTES, 1_000, 200_000),
|
|
1857
2944
|
};
|
|
1858
2945
|
}
|
|
1859
2946
|
function boundedPositiveInteger(value, fallback, min, max) {
|
|
@@ -1869,6 +2956,14 @@ async function workspaceContextRoot(cwd) {
|
|
|
1869
2956
|
return await realpath(cwd).catch(() => resolve(cwd));
|
|
1870
2957
|
}
|
|
1871
2958
|
}
|
|
2959
|
+
function workspaceRuntimeStateExcludedPaths(root) {
|
|
2960
|
+
const stateRoot = resolve(defaultUltracodeStateRoot());
|
|
2961
|
+
const workspaceRoot = resolve(root);
|
|
2962
|
+
if (!pathInsideOrEqual(workspaceRoot, stateRoot))
|
|
2963
|
+
return EMPTY_WORKSPACE_PATH_EXCLUSIONS;
|
|
2964
|
+
const relativeStateRoot = workspacePathKey(relative(workspaceRoot, stateRoot));
|
|
2965
|
+
return new Set([relativeStateRoot || '.']);
|
|
2966
|
+
}
|
|
1872
2967
|
async function listWorkspaceContextCandidates(root) {
|
|
1873
2968
|
try {
|
|
1874
2969
|
return splitLines(await gitOutput(root, ['ls-files', '--cached', '--others', '--exclude-standard']));
|
|
@@ -1915,15 +3010,311 @@ function extractMentionedWorkspacePaths(query) {
|
|
|
1915
3010
|
return [...out];
|
|
1916
3011
|
}
|
|
1917
3012
|
function pathsFromGitStatus(status) {
|
|
3013
|
+
return parseGitStatusPaths(status).paths;
|
|
3014
|
+
}
|
|
3015
|
+
function parseGitStatusPaths(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3016
|
+
if (status.includes('\0'))
|
|
3017
|
+
return parseGitStatusPathsZ(status, runtimeStateExcludedPaths);
|
|
1918
3018
|
const paths = [];
|
|
3019
|
+
const excludedPaths = [];
|
|
3020
|
+
const unavailableEvidence = [];
|
|
3021
|
+
let entryIndex = 0;
|
|
1919
3022
|
for (const line of status.split(/\r?\n/).filter(Boolean)) {
|
|
1920
|
-
|
|
3023
|
+
entryIndex += 1;
|
|
3024
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(line);
|
|
3025
|
+
if (!match) {
|
|
3026
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unparseable`);
|
|
3027
|
+
continue;
|
|
3028
|
+
}
|
|
3029
|
+
const statusCode = match[1];
|
|
3030
|
+
const rawPath = match[2];
|
|
1921
3031
|
if (!rawPath)
|
|
1922
3032
|
continue;
|
|
1923
|
-
const
|
|
1924
|
-
|
|
3033
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3034
|
+
const renameParts = renameOrCopy ? splitGitStatusRename(rawPath) : undefined;
|
|
3035
|
+
if (renameOrCopy && !renameParts) {
|
|
3036
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unsafe-path`);
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
if (renameParts) {
|
|
3040
|
+
const sourcePath = normalizeGitStatusPath(renameParts.source);
|
|
3041
|
+
if (!isWorkspaceEvidencePathSafe(sourcePath)) {
|
|
3042
|
+
const targetPath = normalizeGitStatusPath(renameParts.target);
|
|
3043
|
+
if (targetPath)
|
|
3044
|
+
excludedPaths.push(targetPath);
|
|
3045
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unsafe-source`);
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
else if (!shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths)) {
|
|
3049
|
+
const targetPath = normalizeGitStatusPath(renameParts.target);
|
|
3050
|
+
if (targetPath)
|
|
3051
|
+
excludedPaths.push(targetPath);
|
|
3052
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:excluded-source`);
|
|
3053
|
+
continue;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
const selectedPath = renameParts ? renameParts.target : rawPath;
|
|
3057
|
+
const path = normalizeGitStatusPath(selectedPath);
|
|
3058
|
+
if (isWorkspaceEvidencePathSafe(path) && shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths))
|
|
3059
|
+
paths.push(path);
|
|
3060
|
+
else if (isWorkspaceEvidencePathSafe(path))
|
|
3061
|
+
excludedPaths.push(path);
|
|
3062
|
+
else
|
|
3063
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:${renameParts ? 'unsafe-target' : 'unsafe-path'}`);
|
|
1925
3064
|
}
|
|
1926
|
-
return paths;
|
|
3065
|
+
return { paths, excludedPaths, unavailableEvidence };
|
|
3066
|
+
}
|
|
3067
|
+
function parseGitStatusPathsZ(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3068
|
+
const paths = [];
|
|
3069
|
+
const excludedPaths = [];
|
|
3070
|
+
const unavailableEvidence = [];
|
|
3071
|
+
const entries = status.split('\0').filter((entry) => entry !== '');
|
|
3072
|
+
let entryIndex = 0;
|
|
3073
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
3074
|
+
entryIndex += 1;
|
|
3075
|
+
const entry = entries[index] ?? '';
|
|
3076
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(entry);
|
|
3077
|
+
const statusCode = match?.[1] ?? '';
|
|
3078
|
+
const path = match?.[2] ?? '';
|
|
3079
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3080
|
+
let excludedBySource = false;
|
|
3081
|
+
if (renameOrCopy) {
|
|
3082
|
+
const sourcePath = entries[index + 1];
|
|
3083
|
+
if (sourcePath === undefined)
|
|
3084
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:missing-source`);
|
|
3085
|
+
else if (!isWorkspaceEvidencePathSafe(sourcePath)) {
|
|
3086
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unsafe-source`);
|
|
3087
|
+
excludedBySource = true;
|
|
3088
|
+
}
|
|
3089
|
+
else if (!shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths)) {
|
|
3090
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:excluded-source`);
|
|
3091
|
+
excludedBySource = true;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
if (isWorkspaceEvidencePathSafe(path) && shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths) && !excludedBySource)
|
|
3095
|
+
paths.push(path);
|
|
3096
|
+
else if (isWorkspaceEvidencePathSafe(path))
|
|
3097
|
+
excludedPaths.push(path);
|
|
3098
|
+
else
|
|
3099
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:${renameOrCopy ? 'unsafe-target' : 'unsafe-path'}`);
|
|
3100
|
+
if (renameOrCopy) {
|
|
3101
|
+
index += 1;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return { paths, excludedPaths, unavailableEvidence };
|
|
3105
|
+
}
|
|
3106
|
+
function formatGitStatusDisplay(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3107
|
+
if (!status.trim())
|
|
3108
|
+
return '(clean or unavailable)';
|
|
3109
|
+
if (status.includes('\0'))
|
|
3110
|
+
return formatGitStatusZDisplay(status, runtimeStateExcludedPaths);
|
|
3111
|
+
const lines = [];
|
|
3112
|
+
let entryIndex = 0;
|
|
3113
|
+
for (const line of status.split(/\r?\n/).filter(Boolean)) {
|
|
3114
|
+
entryIndex += 1;
|
|
3115
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(line);
|
|
3116
|
+
if (!match) {
|
|
3117
|
+
lines.push(`${entryIndex}: <unparseable status omitted>`);
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
const statusCode = match[1];
|
|
3121
|
+
const rawPath = match[2];
|
|
3122
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3123
|
+
if (renameOrCopy) {
|
|
3124
|
+
const renameParts = splitGitStatusRename(rawPath);
|
|
3125
|
+
if (!renameParts) {
|
|
3126
|
+
lines.push(`${statusCode} <unsafe rename omitted>`);
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
const sourcePath = normalizeGitStatusPath(renameParts.source);
|
|
3130
|
+
const targetPath = normalizeGitStatusPath(renameParts.target);
|
|
3131
|
+
if ((isWorkspaceEvidencePathSafe(sourcePath) && !shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths))
|
|
3132
|
+
|| (isWorkspaceEvidencePathSafe(targetPath) && !shouldExposeWorkspaceStatusPath(targetPath, runtimeStateExcludedPaths))) {
|
|
3133
|
+
lines.push(`${statusCode} <excluded rename omitted>`);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(sourcePath, 'source', runtimeStateExcludedPaths)} -> ${formatGitStatusPathForDisplay(targetPath, 'target', runtimeStateExcludedPaths)}`);
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
const path = normalizeGitStatusPath(rawPath);
|
|
3140
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(path, 'path', runtimeStateExcludedPaths)}`);
|
|
3141
|
+
}
|
|
3142
|
+
return lines.length ? lines.join('\n') : '(clean or unavailable)';
|
|
3143
|
+
}
|
|
3144
|
+
function formatGitStatusZDisplay(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3145
|
+
const lines = [];
|
|
3146
|
+
const entries = status.split('\0').filter((entry) => entry !== '');
|
|
3147
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
3148
|
+
const entry = entries[index] ?? '';
|
|
3149
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(entry);
|
|
3150
|
+
if (!match) {
|
|
3151
|
+
lines.push(`${index + 1}: <unparseable status omitted>`);
|
|
3152
|
+
continue;
|
|
3153
|
+
}
|
|
3154
|
+
const statusCode = match[1];
|
|
3155
|
+
const path = match[2];
|
|
3156
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3157
|
+
if (renameOrCopy) {
|
|
3158
|
+
const sourcePath = entries[index + 1] ?? '';
|
|
3159
|
+
if ((isWorkspaceEvidencePathSafe(sourcePath) && !shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths))
|
|
3160
|
+
|| (isWorkspaceEvidencePathSafe(path) && !shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths))) {
|
|
3161
|
+
lines.push(`${statusCode} <excluded rename omitted>`);
|
|
3162
|
+
index += 1;
|
|
3163
|
+
continue;
|
|
3164
|
+
}
|
|
3165
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(sourcePath, 'source', runtimeStateExcludedPaths)} -> ${formatGitStatusPathForDisplay(path, 'target', runtimeStateExcludedPaths)}`);
|
|
3166
|
+
index += 1;
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(path, 'path', runtimeStateExcludedPaths)}`);
|
|
3170
|
+
}
|
|
3171
|
+
return lines.length ? lines.join('\n') : '(clean or unavailable)';
|
|
3172
|
+
}
|
|
3173
|
+
function formatGitStatusPathForDisplay(path, label, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3174
|
+
if (!isWorkspaceEvidencePathSafe(path))
|
|
3175
|
+
return `<unsafe ${label} omitted>`;
|
|
3176
|
+
if (!shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths))
|
|
3177
|
+
return `<excluded ${label} omitted>`;
|
|
3178
|
+
if (/^\s|\s$| -> /.test(path))
|
|
3179
|
+
return JSON.stringify(path);
|
|
3180
|
+
return path;
|
|
3181
|
+
}
|
|
3182
|
+
function isWorkspaceEvidencePathSafe(path) {
|
|
3183
|
+
return path !== '' && !/[\uFFFD\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(path);
|
|
3184
|
+
}
|
|
3185
|
+
function splitGitStatusRename(rawPath) {
|
|
3186
|
+
const separator = gitStatusRenameSeparator(rawPath);
|
|
3187
|
+
if (separator === -1)
|
|
3188
|
+
return undefined;
|
|
3189
|
+
return {
|
|
3190
|
+
source: rawPath.slice(0, separator),
|
|
3191
|
+
target: rawPath.slice(separator + 4),
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
function gitStatusRenameSeparator(value) {
|
|
3195
|
+
let separator = -1;
|
|
3196
|
+
let inQuote = false;
|
|
3197
|
+
let escaped = false;
|
|
3198
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3199
|
+
const char = value[index];
|
|
3200
|
+
if (inQuote) {
|
|
3201
|
+
if (escaped) {
|
|
3202
|
+
escaped = false;
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
if (char === '\\') {
|
|
3206
|
+
escaped = true;
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
if (char === '"')
|
|
3210
|
+
inQuote = false;
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (char === '"') {
|
|
3214
|
+
inQuote = true;
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
if (value.startsWith(' -> ', index)) {
|
|
3218
|
+
if (separator !== -1)
|
|
3219
|
+
return -1;
|
|
3220
|
+
separator = index;
|
|
3221
|
+
index += 3;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return inQuote ? -1 : separator;
|
|
3225
|
+
}
|
|
3226
|
+
function gitFailureToken(err) {
|
|
3227
|
+
const record = err;
|
|
3228
|
+
if (typeof record.signal === 'string' && record.signal)
|
|
3229
|
+
return 'signal';
|
|
3230
|
+
if (typeof record.code === 'number')
|
|
3231
|
+
return `exit-${record.code}`;
|
|
3232
|
+
return 'failed';
|
|
3233
|
+
}
|
|
3234
|
+
function unavailableGitEvidence(kind, err, detail) {
|
|
3235
|
+
const safeDetail = detail && /^[A-Za-z0-9._/@+-]{1,160}$/.test(detail) ? `:${detail}` : '';
|
|
3236
|
+
return `unavailable:${kind}${safeDetail}:${gitFailureToken(err)}`;
|
|
3237
|
+
}
|
|
3238
|
+
function gitQuotedPathEnd(value) {
|
|
3239
|
+
let escaped = false;
|
|
3240
|
+
for (let index = 1; index < value.length; index += 1) {
|
|
3241
|
+
const char = value[index];
|
|
3242
|
+
if (escaped) {
|
|
3243
|
+
escaped = false;
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
if (char === '\\') {
|
|
3247
|
+
escaped = true;
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
if (char === '"')
|
|
3251
|
+
return index + 1;
|
|
3252
|
+
}
|
|
3253
|
+
return -1;
|
|
3254
|
+
}
|
|
3255
|
+
function normalizeGitStatusPath(value) {
|
|
3256
|
+
const trimmed = value.trim();
|
|
3257
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"'))
|
|
3258
|
+
return value;
|
|
3259
|
+
try {
|
|
3260
|
+
const parsed = JSON.parse(trimmed);
|
|
3261
|
+
if (typeof parsed === 'string')
|
|
3262
|
+
return parsed;
|
|
3263
|
+
}
|
|
3264
|
+
catch {
|
|
3265
|
+
return decodeGitQuotedPath(trimmed);
|
|
3266
|
+
}
|
|
3267
|
+
return decodeGitQuotedPath(trimmed);
|
|
3268
|
+
}
|
|
3269
|
+
function decodeGitQuotedPath(value) {
|
|
3270
|
+
const body = value.slice(1, -1);
|
|
3271
|
+
let out = '';
|
|
3272
|
+
let bytes = [];
|
|
3273
|
+
const flushBytes = () => {
|
|
3274
|
+
if (bytes.length === 0)
|
|
3275
|
+
return;
|
|
3276
|
+
out += Buffer.from(bytes).toString('utf8');
|
|
3277
|
+
bytes = [];
|
|
3278
|
+
};
|
|
3279
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
3280
|
+
const char = body[index] ?? '';
|
|
3281
|
+
if (char !== '\\') {
|
|
3282
|
+
flushBytes();
|
|
3283
|
+
out += char;
|
|
3284
|
+
continue;
|
|
3285
|
+
}
|
|
3286
|
+
const next = body[index + 1] ?? '';
|
|
3287
|
+
if (/[0-7]/.test(next)) {
|
|
3288
|
+
let octal = next;
|
|
3289
|
+
index += 1;
|
|
3290
|
+
for (let count = 0; count < 2 && /[0-7]/.test(body[index + 1] ?? ''); count += 1) {
|
|
3291
|
+
index += 1;
|
|
3292
|
+
octal += body[index] ?? '';
|
|
3293
|
+
}
|
|
3294
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3297
|
+
flushBytes();
|
|
3298
|
+
index += 1;
|
|
3299
|
+
if (next === 'n')
|
|
3300
|
+
out += '\n';
|
|
3301
|
+
else if (next === 't')
|
|
3302
|
+
out += '\t';
|
|
3303
|
+
else if (next === 'r')
|
|
3304
|
+
out += '\r';
|
|
3305
|
+
else if (next === 'b')
|
|
3306
|
+
out += '\b';
|
|
3307
|
+
else if (next === 'f')
|
|
3308
|
+
out += '\f';
|
|
3309
|
+
else if (next === 'v')
|
|
3310
|
+
out += '\v';
|
|
3311
|
+
else if (next === 'a')
|
|
3312
|
+
out += '\x07';
|
|
3313
|
+
else
|
|
3314
|
+
out += next;
|
|
3315
|
+
}
|
|
3316
|
+
flushBytes();
|
|
3317
|
+
return out;
|
|
1927
3318
|
}
|
|
1928
3319
|
async function workspaceContextFileBlock(root, requestedPath, maxFileBytes) {
|
|
1929
3320
|
const resolved = await resolveWorkspaceContextPath(root, requestedPath);
|
|
@@ -1955,10 +3346,12 @@ async function resolveWorkspaceContextPath(root, requestedPath) {
|
|
|
1955
3346
|
relativePath: relative(root, canonical) || '.',
|
|
1956
3347
|
};
|
|
1957
3348
|
}
|
|
1958
|
-
function shouldIncludeWorkspaceContextPath(path) {
|
|
3349
|
+
function shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
1959
3350
|
const normalized = path.replaceAll('\\', '/').replace(/^\.\/+/, '');
|
|
1960
3351
|
if (!normalized || normalized.startsWith('../') || normalized.includes('/../'))
|
|
1961
3352
|
return false;
|
|
3353
|
+
if (workspacePathExcludedBySet(normalized, runtimeStateExcludedPaths))
|
|
3354
|
+
return false;
|
|
1962
3355
|
const parts = normalized.split('/');
|
|
1963
3356
|
if (parts.some((part) => WORKSPACE_CONTEXT_EXCLUDED_DIRS.has(part)))
|
|
1964
3357
|
return false;
|
|
@@ -1970,6 +3363,29 @@ function shouldIncludeWorkspaceContextPath(path) {
|
|
|
1970
3363
|
return false;
|
|
1971
3364
|
return WORKSPACE_CONTEXT_ALLOWED_EXTENSIONS.has(name.slice(dot).toLowerCase());
|
|
1972
3365
|
}
|
|
3366
|
+
function shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3367
|
+
const normalized = workspacePathKey(path);
|
|
3368
|
+
if (!normalized || normalized.startsWith('../') || normalized.includes('/../'))
|
|
3369
|
+
return false;
|
|
3370
|
+
if (workspacePathExcludedBySet(normalized, runtimeStateExcludedPaths))
|
|
3371
|
+
return false;
|
|
3372
|
+
return !normalized.split('/').some((part) => WORKSPACE_CONTEXT_EXCLUDED_DIRS.has(part));
|
|
3373
|
+
}
|
|
3374
|
+
function workspacePathKey(path) {
|
|
3375
|
+
return path.replaceAll('\\', '/').replace(/^\.\/+/, '');
|
|
3376
|
+
}
|
|
3377
|
+
function workspacePathExcludedBySet(path, excludedPaths) {
|
|
3378
|
+
const key = workspacePathKey(path);
|
|
3379
|
+
if (excludedPaths.has('.'))
|
|
3380
|
+
return true;
|
|
3381
|
+
for (const excludedPath of excludedPaths) {
|
|
3382
|
+
if (!excludedPath || excludedPath === '.')
|
|
3383
|
+
continue;
|
|
3384
|
+
if (key === excludedPath || key.startsWith(`${excludedPath}/`))
|
|
3385
|
+
return true;
|
|
3386
|
+
}
|
|
3387
|
+
return false;
|
|
3388
|
+
}
|
|
1973
3389
|
function numberWorkspaceContextLines(text) {
|
|
1974
3390
|
return text.split(/\r?\n/).map((line, index) => {
|
|
1975
3391
|
return `${String(index + 1).padStart(4, ' ')} | ${line}`;
|
|
@@ -2039,6 +3455,17 @@ function agentCompletionProgress(ctx, phase) {
|
|
|
2039
3455
|
function shortHash(value) {
|
|
2040
3456
|
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
2041
3457
|
}
|
|
3458
|
+
function fullHash(value) {
|
|
3459
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
3460
|
+
}
|
|
3461
|
+
function workflowValueHash(value) {
|
|
3462
|
+
try {
|
|
3463
|
+
return fullHash(stableJson(value));
|
|
3464
|
+
}
|
|
3465
|
+
catch (err) {
|
|
3466
|
+
throw workflowInputError(workflowErrorMessage(err, 'workflow hash value must be JSON-serializable.'));
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
2042
3469
|
function workflowScriptHash(script) {
|
|
2043
3470
|
return `sha256:${createHash('sha256').update(script).digest('hex')}`;
|
|
2044
3471
|
}
|
|
@@ -2610,6 +4037,60 @@ function workflowScriptMetadataFromUnknown(value) {
|
|
|
2610
4037
|
...(typeof record.permissionKey === 'string' ? { permissionKey: record.permissionKey } : {}),
|
|
2611
4038
|
};
|
|
2612
4039
|
}
|
|
4040
|
+
function durableWorkflowRetryInput(input) {
|
|
4041
|
+
const scriptPath = typeof input.scriptPath === 'string' ? input.scriptPath : '';
|
|
4042
|
+
if (!scriptPath)
|
|
4043
|
+
throw workflowInputError('Workflow result resume input requires a persisted scriptPath.');
|
|
4044
|
+
return {
|
|
4045
|
+
scriptPath,
|
|
4046
|
+
...(input.args !== undefined ? { args: journalJsonValueOrInputError(input.args, 'workflow args') } : {}),
|
|
4047
|
+
...(typeof input.toolName === 'string' && input.toolName ? { toolName: input.toolName } : {}),
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
function durableWorkflowResultRecordFromUnknown(value) {
|
|
4051
|
+
const record = asRecord(value);
|
|
4052
|
+
if (!record || typeof record.runId !== 'string' || !record.runId.trim())
|
|
4053
|
+
return null;
|
|
4054
|
+
if (typeof record.workflowName !== 'string' || !record.workflowName.trim())
|
|
4055
|
+
return null;
|
|
4056
|
+
if (typeof record.scriptHash !== 'string' || !record.scriptHash.startsWith('sha256:'))
|
|
4057
|
+
return null;
|
|
4058
|
+
const retryInput = durableWorkflowRetryInputFromUnknown(record.retryInput);
|
|
4059
|
+
return {
|
|
4060
|
+
runId: record.runId,
|
|
4061
|
+
workflowName: record.workflowName,
|
|
4062
|
+
scriptHash: record.scriptHash,
|
|
4063
|
+
...(retryInput ? { retryInput } : {}),
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
function durableWorkflowRetryInputFromUnknown(value) {
|
|
4067
|
+
const record = asRecord(value);
|
|
4068
|
+
if (!record || typeof record.scriptPath !== 'string' || !record.scriptPath.trim())
|
|
4069
|
+
return null;
|
|
4070
|
+
return {
|
|
4071
|
+
scriptPath: record.scriptPath.trim(),
|
|
4072
|
+
...(record.args !== undefined ? { args: normalizeJournalJsonValue(record.args, 'workflow args') } : {}),
|
|
4073
|
+
...(typeof record.toolName === 'string' && record.toolName.trim() ? { toolName: record.toolName.trim() } : {}),
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
function durableScriptRecordMatchesJournal(scriptRecord, started) {
|
|
4077
|
+
const metadata = scriptRecord.metadata;
|
|
4078
|
+
return scriptRecord.scriptPath === started.scriptPath
|
|
4079
|
+
&& metadata !== undefined
|
|
4080
|
+
&& metadata.workflowSource === started.workflowSource
|
|
4081
|
+
&& metadata.workflowSourcePath === started.workflowSourcePath;
|
|
4082
|
+
}
|
|
4083
|
+
function durableRetryInputArgsMatchJournal(input, journalArgs) {
|
|
4084
|
+
if (!Object.prototype.hasOwnProperty.call(input, 'args'))
|
|
4085
|
+
return true;
|
|
4086
|
+
return stableJson(input.args) === stableJson(journalArgs);
|
|
4087
|
+
}
|
|
4088
|
+
function durableRetryInputWithJournalArgs(input, journalArgs) {
|
|
4089
|
+
return {
|
|
4090
|
+
...input,
|
|
4091
|
+
args: journalArgs,
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
2613
4094
|
function workflowPermissionRecordFromUnknown(value) {
|
|
2614
4095
|
const record = asRecord(value);
|
|
2615
4096
|
if (!record)
|
|
@@ -3030,6 +4511,7 @@ function installWorkflowVmGlobals(context, globals) {
|
|
|
3030
4511
|
' define(globalThis, "agent", { value: (...values) => __host.agent(...values), writable: false, configurable: false });',
|
|
3031
4512
|
' define(globalThis, "parallel", { value: (...values) => __host.parallel(...values), writable: false, configurable: false });',
|
|
3032
4513
|
' define(globalThis, "pipeline", { value: (...values) => __host.pipeline(...values), writable: false, configurable: false });',
|
|
4514
|
+
' define(globalThis, "hash", { value: (...values) => __host.hash(...values), writable: false, configurable: false });',
|
|
3033
4515
|
' define(globalThis, "workspaceContext", { value: (...values) => __host.workspaceContext(...values), writable: false, configurable: false });',
|
|
3034
4516
|
' define(globalThis, "announcePlan", { value: (...values) => __host.announcePlan(...values), writable: false, configurable: false });',
|
|
3035
4517
|
' define(globalThis, "announcePhasePlan", { value: (...values) => __host.announcePhasePlan(...values), writable: false, configurable: false });',
|
|
@@ -3789,6 +5271,7 @@ function workflowAgentSemanticOpts(input) {
|
|
|
3789
5271
|
model: input.model,
|
|
3790
5272
|
effort: input.effort,
|
|
3791
5273
|
...(input.isolation ? { isolation: input.isolation } : {}),
|
|
5274
|
+
...(input.logicalKey ? { logicalKey: input.logicalKey } : {}),
|
|
3792
5275
|
};
|
|
3793
5276
|
}
|
|
3794
5277
|
function workflowUsage(usage) {
|