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.
@@ -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: 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
- }),
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 ?? join(options.cwd ?? process.cwd(), '.ultracode-for-codex');
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
- prepareResumePlan(input) {
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 undefined;
1235
+ return await this.durableWorkflowResumeSource(runId);
505
1236
  }
506
- async createResumeCache(sourceTask) {
507
- let entries;
1237
+ async durableWorkflowResumeSource(runId) {
1238
+ const resultPath = join(this.stateDir, 'workflows', `${runId}.result.json`);
1239
+ let record;
508
1240
  try {
509
- entries = (await readWorkflowJournal(workflowJournalPath(sourceTask.transcriptDir))).entries;
1241
+ record = durableWorkflowResultRecordFromUnknown(JSON.parse(await readFile(resultPath, 'utf8')));
510
1242
  }
511
1243
  catch {
512
- throw workflowInputError(`Workflow run cannot be used as a resume source: ${sourceTask.runId}`);
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 || !cache.prefixOpen)
2599
+ if (!cache)
1776
2600
  return null;
1777
- const entry = cache.entries[cache.nextIndex];
1778
- if (!entry || entry.agentCallKey !== agentCallKey) {
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.nextIndex += 1;
1783
- return entry;
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.trim();
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 gitStatus = await gitOutput(root, ['status', '--short']).catch(() => '');
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 = pathsFromGitStatus(gitStatus);
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.trim() ? gitStatus : '(clean or unavailable)',
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
- const rawPath = line.length > 3 ? line.slice(3).trim() : line.trim();
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 path = rawPath.includes(' -> ') ? rawPath.split(' -> ').at(-1) ?? rawPath : rawPath;
1924
- paths.push(path.replace(/^"|"$/g, ''));
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) {