ultracode-for-codex 0.3.1 → 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.
@@ -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: phaseWiseBuiltinWorkflowScript({
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 || !cache.prefixOpen)
2522
+ if (!cache)
1776
2523
  return null;
1777
- const entry = cache.entries[cache.nextIndex];
1778
- if (!entry || entry.agentCallKey !== agentCallKey) {
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.nextIndex += 1;
1783
- return entry;
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) {