ultracode-for-codex 0.3.3 → 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.
@@ -7,6 +7,7 @@ import { promisify } from 'node:util';
7
7
  import { createContext, runInContext } from 'node:vm';
8
8
  import { UltracodeRequestError, estimateTokens } from './types.js';
9
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;
@@ -69,6 +70,7 @@ const WORKSPACE_CONTEXT_EXCLUDED_DIRS = new Set([
69
70
  'node_modules',
70
71
  'out',
71
72
  ]);
73
+ const EMPTY_WORKSPACE_PATH_EXCLUSIONS = new Set();
72
74
  const WORKSPACE_CONTEXT_ALLOWED_EXTENSIONS = new Set([
73
75
  '.cjs',
74
76
  '.css',
@@ -178,21 +180,21 @@ const seedLenses = [
178
180
  const scopeSchema = {
179
181
  type: "object",
180
182
  additionalProperties: false,
181
- required: ["files", "summary", "lensDecisions", "lenses"],
183
+ required: ["files", "summary", "instructions", "lensDecisions", "lenses"],
182
184
  properties: {
183
185
  files: { type: "array", items: { type: "string", minLength: 1, maxLength: 240 } },
184
186
  summary: { type: "string", minLength: 1 },
185
- instructions: { type: "string" },
187
+ instructions: { type: ["string", "null"] },
186
188
  lensDecisions: {
187
189
  type: "array",
188
190
  items: {
189
191
  type: "object",
190
192
  additionalProperties: false,
191
- required: ["seedId", "action", "reasonCategory", "decisionRefs", "reason"],
193
+ required: ["seedId", "action", "selectedLensId", "reasonCategory", "decisionRefs", "reason"],
192
194
  properties: {
193
195
  seedId: { type: "string", minLength: 1 },
194
196
  action: { type: "string", enum: ["select", "skip"] },
195
- selectedLensId: { type: "string" },
197
+ selectedLensId: { type: ["string", "null"] },
196
198
  reasonCategory: { type: "string", enum: ["matched_change", "prompt_risk", "no_evidence", "cap_limit", "redundant", "out_of_scope", "tiny_change"] },
197
199
  decisionRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
198
200
  reason: { type: "string", minLength: 1 }
@@ -225,14 +227,14 @@ const finderSchema = {
225
227
  items: {
226
228
  type: "object",
227
229
  additionalProperties: false,
228
- required: ["file", "summary", "failureScenario", "evidenceRefs"],
230
+ required: ["file", "line", "summary", "failureScenario", "evidenceRefs", "kind"],
229
231
  properties: {
230
232
  file: { type: "string", minLength: 1, maxLength: 240 },
231
- line: { type: "integer", minimum: 1 },
233
+ line: { type: ["integer", "null"], minimum: 1 },
232
234
  summary: { type: "string", minLength: 1 },
233
235
  failureScenario: { type: "string", minLength: 1 },
234
236
  evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
235
- kind: { type: "string" }
237
+ kind: { type: ["string", "null"] }
236
238
  }
237
239
  }
238
240
  }
@@ -241,12 +243,12 @@ const finderSchema = {
241
243
  const verifierSchema = {
242
244
  type: "object",
243
245
  additionalProperties: false,
244
- required: ["verdict", "evidence", "evidenceRefs"],
246
+ required: ["verdict", "evidence", "evidenceRefs", "severity"],
245
247
  properties: {
246
248
  verdict: { type: "string", enum: ["CONFIRMED", "PLAUSIBLE", "REFUTED"] },
247
249
  evidence: { type: "string", minLength: 1 },
248
250
  evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
249
- severity: { type: "string", enum: ["P0", "P1", "P2", "P3"] }
251
+ severity: { type: ["string", "null"], enum: ["P0", "P1", "P2", "P3", null] }
250
252
  }
251
253
  };
252
254
  const synthesisSchema = {
@@ -260,12 +262,12 @@ const synthesisSchema = {
260
262
  items: {
261
263
  type: "object",
262
264
  additionalProperties: false,
263
- required: ["index", "action", "reasonCategory", "reason"],
265
+ required: ["index", "action", "merge", "severity", "reasonCategory", "reason"],
264
266
  properties: {
265
267
  index: { type: "integer", minimum: 0 },
266
268
  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
+ merge: { type: ["array", "null"], minItems: 1, items: { type: "integer", minimum: 0 } },
270
+ severity: { type: ["string", "null"], enum: ["P0", "P1", "P2", "P3", null] },
269
271
  reasonCategory: { type: "string", enum: ["material", "duplicate", "not_material", "report_cap", "unsupported_evidence", "superseded"] },
270
272
  reason: { type: "string", minLength: 1 }
271
273
  }
@@ -1085,14 +1087,14 @@ export class WorkflowTaskRegistry {
1085
1087
  agentStallRetryLimit;
1086
1088
  constructor(options) {
1087
1089
  this.options = options;
1088
- this.stateDir = options.stateDir ?? join(options.cwd ?? process.cwd(), '.ultracode-for-codex');
1090
+ this.stateDir = options.stateDir ?? defaultWorkflowStateDir(options.cwd ?? process.cwd());
1089
1091
  this.agentStallRetryLimit = normalizeAgentStallRetryLimit(options.agentStallRetryLimit);
1090
1092
  this.agentStallTimeoutMs = normalizeAgentStallTimeoutMs(options.agentStallTimeoutMs, options.requestTimeoutMs);
1091
1093
  }
1092
1094
  async launch(input) {
1093
1095
  if (this.closed)
1094
1096
  throw workflowInputError('Workflow runtime is closed.');
1095
- const resumePlan = this.prepareResumePlan(input);
1097
+ const resumePlan = await this.prepareResumePlan(input);
1096
1098
  let resolved = await this.resolveLaunchInput(resumePlan.launchInput);
1097
1099
  const parsed = parseInlineWorkflowScript(resolved.script);
1098
1100
  const scriptHash = workflowScriptHash(resolved.script);
@@ -1189,29 +1191,31 @@ export class WorkflowTaskRegistry {
1189
1191
  scriptHash,
1190
1192
  };
1191
1193
  }
1192
- 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) {
1193
1204
  if (!Object.prototype.hasOwnProperty.call(input, 'resumeFromRunId')) {
1194
1205
  return { launchInput: input };
1195
1206
  }
1196
1207
  const resumeFromRunId = normalizeResumeFromRunId(input.resumeFromRunId);
1197
- const sourceTask = this.workflowTaskByRunId(resumeFromRunId);
1208
+ const sourceTask = await this.workflowTaskByRunId(resumeFromRunId);
1198
1209
  if (!sourceTask)
1199
1210
  throw workflowInputError(`Unknown workflow run for resume: ${resumeFromRunId}`);
1200
1211
  if (sourceTask.status === 'running')
1201
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
+ }
1202
1216
  const inheritedArgs = !Object.prototype.hasOwnProperty.call(input, 'args') && sourceTask.retryInput.args !== undefined
1203
1217
  ? { args: sourceTask.retryInput.args }
1204
1218
  : {};
1205
- if (workflowLaunchHasSourceSelector(input)) {
1206
- return {
1207
- sourceTask,
1208
- launchInput: {
1209
- ...inheritedArgs,
1210
- ...input,
1211
- resumeFromRunId,
1212
- },
1213
- };
1214
- }
1215
1219
  return {
1216
1220
  sourceTask,
1217
1221
  launchInput: {
@@ -1223,21 +1227,66 @@ export class WorkflowTaskRegistry {
1223
1227
  },
1224
1228
  };
1225
1229
  }
1226
- workflowTaskByRunId(runId) {
1230
+ async workflowTaskByRunId(runId) {
1227
1231
  for (const task of this.tasks.values()) {
1228
1232
  if (task.runId === runId)
1229
1233
  return task;
1230
1234
  }
1231
- return undefined;
1235
+ return await this.durableWorkflowResumeSource(runId);
1232
1236
  }
1233
- async createResumeCache(sourceTask) {
1234
- let entries;
1237
+ async durableWorkflowResumeSource(runId) {
1238
+ const resultPath = join(this.stateDir, 'workflows', `${runId}.result.json`);
1239
+ let record;
1235
1240
  try {
1236
- entries = (await readWorkflowJournal(workflowJournalPath(sourceTask.transcriptDir))).entries;
1241
+ record = durableWorkflowResultRecordFromUnknown(JSON.parse(await readFile(resultPath, 'utf8')));
1237
1242
  }
1238
1243
  catch {
1239
- throw workflowInputError(`Workflow run cannot be used as a resume source: ${sourceTask.runId}`);
1244
+ return undefined;
1240
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 ?? '');
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);
1241
1290
  const completedByCallKey = new Map();
1242
1291
  for (const entry of entries) {
1243
1292
  if (entry.kind === 'workflow.agent.completed')
@@ -1263,6 +1312,30 @@ export class WorkflowTaskRegistry {
1263
1312
  prefixOpen: true,
1264
1313
  };
1265
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
+ }
1266
1339
  async resolveLaunchInput(input) {
1267
1340
  const normalized = normalizeLaunchInput(input);
1268
1341
  if (normalized.scriptPath) {
@@ -1732,6 +1805,7 @@ export class WorkflowTaskRegistry {
1732
1805
  workflowSource: task.workflowSource,
1733
1806
  ...(task.workflowSourcePath ? { workflowSourcePath: task.workflowSourcePath } : {}),
1734
1807
  scriptHash: task.scriptHash,
1808
+ retryInput: durableWorkflowRetryInput(task.retryInput),
1735
1809
  result: journalResult,
1736
1810
  }, null, 2)}\n`);
1737
1811
  const completedSnapshot = await this.completeTask(ctx, journalResult, {
@@ -2494,6 +2568,9 @@ function normalizeResumeFromRunId(value) {
2494
2568
  const runId = value.trim();
2495
2569
  if (!runId)
2496
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
+ }
2497
2574
  return runId;
2498
2575
  }
2499
2576
  function normalizeAgentIsolation(value) {
@@ -2537,13 +2614,16 @@ function takeResumeCacheHit(cache, agentCallKey) {
2537
2614
  return keyed;
2538
2615
  }
2539
2616
  async function gitOutput(cwd, args) {
2617
+ return (await gitOutputRaw(cwd, args)).trim();
2618
+ }
2619
+ async function gitOutputRaw(cwd, args) {
2540
2620
  try {
2541
2621
  const result = await execFileAsync('git', args, {
2542
2622
  cwd,
2543
2623
  encoding: 'utf8',
2544
2624
  maxBuffer: 1024 * 1024,
2545
2625
  });
2546
- return result.stdout.trim();
2626
+ return result.stdout;
2547
2627
  }
2548
2628
  catch (err) {
2549
2629
  const record = err;
@@ -2554,22 +2634,39 @@ async function gitOutput(cwd, args) {
2554
2634
  }
2555
2635
  async function buildWorkspaceContext(cwd, options) {
2556
2636
  const root = await workspaceContextRoot(cwd);
2557
- const gitStatus = await gitOutput(root, ['status', '--short', '--untracked-files=all']).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));
2558
2655
  const reviewEvidence = options.includeDiff
2559
- ? await buildReviewEvidenceContext(root, gitStatus, options)
2656
+ ? await buildReviewEvidenceContext(root, gitStatusPaths, options, statusUnavailableEvidence, runtimeStateExcludedPaths)
2560
2657
  : undefined;
2561
2658
  const explicitPaths = [
2562
2659
  ...options.files,
2563
2660
  ...extractMentionedWorkspacePaths(options.query ?? ''),
2564
2661
  ];
2565
- const changedPaths = pathsFromGitStatus(gitStatus);
2662
+ const changedPaths = gitStatusPathParse.paths.filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths));
2566
2663
  const listedPaths = await listWorkspaceContextCandidates(root);
2567
2664
  const candidates = uniqueStrings([
2568
2665
  ...explicitPaths,
2569
2666
  ...changedPaths,
2570
2667
  ...WORKSPACE_CONTEXT_PRIORITY_FILES,
2571
2668
  ...listedPaths,
2572
- ]).filter(shouldIncludeWorkspaceContextPath);
2669
+ ]).filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths) && !excludedWorkspacePaths.has(workspacePathKey(path)));
2573
2670
  const fileBlocks = [];
2574
2671
  let usedBytes = 0;
2575
2672
  for (const candidate of candidates) {
@@ -2606,30 +2703,36 @@ async function buildWorkspaceContext(cwd, options) {
2606
2703
  ] : []),
2607
2704
  '',
2608
2705
  '### Git Status',
2609
- gitStatus.trim() ? gitStatus : '(clean or unavailable)',
2706
+ gitStatus,
2610
2707
  '',
2611
2708
  '### Included Files',
2612
2709
  fileBlocks.length ? fileBlocks.join('\n\n') : '(no readable text files selected)',
2613
2710
  ].join('\n');
2614
2711
  }
2615
- async function buildReviewEvidenceContext(root, gitStatus, options) {
2616
- const unavailableEvidence = [];
2617
- const changedPaths = pathsFromGitStatus(gitStatus);
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);
2618
2721
  const head = await gitOutput(root, ['rev-parse', '--verify', 'HEAD']).catch((err) => {
2619
- unavailableEvidence.push(`unavailable:git-head:${workflowErrorMessage(err)}`);
2722
+ unavailableEvidence.push(unavailableGitEvidence('git-head', err));
2620
2723
  return 'unavailable';
2621
2724
  });
2622
- const unstaged = await boundedGitOutput(root, [
2725
+ const unstaged = filterWorkspaceContextDiff(await boundedGitOutput(root, [
2623
2726
  'diff',
2624
2727
  '--no-ext-diff',
2625
2728
  '--patch',
2626
2729
  '--find-renames',
2627
2730
  '--',
2628
2731
  ], options.maxDiffBytes).catch((err) => {
2629
- unavailableEvidence.push(`unavailable:diff-unstaged:${workflowErrorMessage(err)}`);
2732
+ unavailableEvidence.push(unavailableGitEvidence('diff-unstaged', err));
2630
2733
  return { text: '', truncated: false };
2631
- });
2632
- const staged = await boundedGitOutput(root, [
2734
+ }), excludedDiffPaths, runtimeStateExcludedPaths);
2735
+ const staged = filterWorkspaceContextDiff(await boundedGitOutput(root, [
2633
2736
  'diff',
2634
2737
  '--cached',
2635
2738
  '--no-ext-diff',
@@ -2637,19 +2740,19 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
2637
2740
  '--find-renames',
2638
2741
  '--',
2639
2742
  ], options.maxDiffBytes).catch((err) => {
2640
- unavailableEvidence.push(`unavailable:diff-staged:${workflowErrorMessage(err)}`);
2743
+ unavailableEvidence.push(unavailableGitEvidence('diff-staged', err));
2641
2744
  return { text: '', truncated: false };
2642
- });
2745
+ }), excludedDiffPaths, runtimeStateExcludedPaths);
2643
2746
  let committed = { text: '', truncated: false };
2644
2747
  let acceptedDiffBaseRef = '';
2645
2748
  if (options.diffBaseRef) {
2646
2749
  const baseCommit = await gitOutput(root, ['rev-parse', '--verify', `${options.diffBaseRef}^{commit}`]).catch((err) => {
2647
- unavailableEvidence.push(`unavailable:diff-base:${options.diffBaseRef}:${workflowErrorMessage(err)}`);
2750
+ unavailableEvidence.push(unavailableGitEvidence('diff-base', err, options.diffBaseRef));
2648
2751
  return '';
2649
2752
  });
2650
2753
  if (baseCommit) {
2651
2754
  acceptedDiffBaseRef = options.diffBaseRef;
2652
- committed = await boundedGitOutput(root, [
2755
+ committed = filterWorkspaceContextDiff(await boundedGitOutput(root, [
2653
2756
  'diff',
2654
2757
  '--no-ext-diff',
2655
2758
  '--patch',
@@ -2657,9 +2760,9 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
2657
2760
  `${baseCommit}..HEAD`,
2658
2761
  '--',
2659
2762
  ], options.maxDiffBytes).catch((err) => {
2660
- unavailableEvidence.push(`unavailable:diff-committed:${options.diffBaseRef}:${workflowErrorMessage(err)}`);
2763
+ unavailableEvidence.push(unavailableGitEvidence('diff-committed', err, options.diffBaseRef));
2661
2764
  return { text: '', truncated: false };
2662
- });
2765
+ }), excludedDiffPaths, runtimeStateExcludedPaths);
2663
2766
  }
2664
2767
  }
2665
2768
  const diffEvidence = [
@@ -2669,7 +2772,7 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
2669
2772
  ];
2670
2773
  const allowedEvidenceRefs = uniqueStrings([
2671
2774
  ...changedPaths.map((path) => `file:${path}`),
2672
- ...diffEvidence.flatMap((entry) => diffEvidenceRefs(entry.kind, entry.value.text)),
2775
+ ...diffEvidence.flatMap((entry) => diffEvidenceRefs(entry.kind, entry.value.text, runtimeStateExcludedPaths)),
2673
2776
  ]);
2674
2777
  const allowedEvidenceIndexDigest = fullHash(allowedEvidenceRefs.join('\n'));
2675
2778
  const sourceSnapshotId = `git:${head}:${fullHash([
@@ -2690,6 +2793,7 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
2690
2793
  acceptedDiffBaseRef,
2691
2794
  truncation,
2692
2795
  allowedEvidenceRefs,
2796
+ unavailableEvidence,
2693
2797
  }));
2694
2798
  const sections = [
2695
2799
  `sourceSnapshotId: ${sourceSnapshotId}`,
@@ -2727,26 +2831,98 @@ async function boundedGitOutput(root, args, maxBytes) {
2727
2831
  truncated: true,
2728
2832
  };
2729
2833
  }
2730
- function diffEvidenceRefs(kind, diff) {
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) {
2731
2872
  const refs = [];
2732
2873
  let currentPath = '';
2733
2874
  let hunkIndex = 0;
2734
2875
  for (const line of diff.split(/\r?\n/)) {
2735
- const header = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
2876
+ const header = parseGitDiffHeader(line);
2736
2877
  if (header) {
2737
- currentPath = header[2] ?? header[1] ?? '';
2878
+ currentPath = header.newPath || header.oldPath;
2738
2879
  hunkIndex = 0;
2739
- if (currentPath && currentPath !== '/dev/null')
2880
+ if (currentPath && currentPath !== '/dev/null' && shouldIncludeWorkspaceContextPath(currentPath, runtimeStateExcludedPaths))
2740
2881
  refs.push(`diff:${kind}:${currentPath}`);
2741
2882
  continue;
2742
2883
  }
2743
- if (currentPath && line.startsWith('@@')) {
2884
+ if (currentPath && shouldIncludeWorkspaceContextPath(currentPath, runtimeStateExcludedPaths) && line.startsWith('@@')) {
2744
2885
  hunkIndex += 1;
2745
2886
  refs.push(`hunk:${kind}:${currentPath}:${hunkIndex}`);
2746
2887
  }
2747
2888
  }
2748
2889
  return refs;
2749
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
+ }
2750
2926
  function normalizeWorkspaceContextOptions(value) {
2751
2927
  const options = asRecord(value) ?? {};
2752
2928
  const query = typeof options.query === 'string' ? options.query : undefined;
@@ -2780,6 +2956,14 @@ async function workspaceContextRoot(cwd) {
2780
2956
  return await realpath(cwd).catch(() => resolve(cwd));
2781
2957
  }
2782
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
+ }
2783
2967
  async function listWorkspaceContextCandidates(root) {
2784
2968
  try {
2785
2969
  return splitLines(await gitOutput(root, ['ls-files', '--cached', '--others', '--exclude-standard']));
@@ -2826,15 +3010,311 @@ function extractMentionedWorkspacePaths(query) {
2826
3010
  return [...out];
2827
3011
  }
2828
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);
2829
3018
  const paths = [];
3019
+ const excludedPaths = [];
3020
+ const unavailableEvidence = [];
3021
+ let entryIndex = 0;
2830
3022
  for (const line of status.split(/\r?\n/).filter(Boolean)) {
2831
- 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];
2832
3031
  if (!rawPath)
2833
3032
  continue;
2834
- const path = rawPath.includes(' -> ') ? rawPath.split(' -> ').at(-1) ?? rawPath : rawPath;
2835
- 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'}`);
3064
+ }
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;
2836
3263
  }
2837
- return paths;
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;
2838
3318
  }
2839
3319
  async function workspaceContextFileBlock(root, requestedPath, maxFileBytes) {
2840
3320
  const resolved = await resolveWorkspaceContextPath(root, requestedPath);
@@ -2866,10 +3346,12 @@ async function resolveWorkspaceContextPath(root, requestedPath) {
2866
3346
  relativePath: relative(root, canonical) || '.',
2867
3347
  };
2868
3348
  }
2869
- function shouldIncludeWorkspaceContextPath(path) {
3349
+ function shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
2870
3350
  const normalized = path.replaceAll('\\', '/').replace(/^\.\/+/, '');
2871
3351
  if (!normalized || normalized.startsWith('../') || normalized.includes('/../'))
2872
3352
  return false;
3353
+ if (workspacePathExcludedBySet(normalized, runtimeStateExcludedPaths))
3354
+ return false;
2873
3355
  const parts = normalized.split('/');
2874
3356
  if (parts.some((part) => WORKSPACE_CONTEXT_EXCLUDED_DIRS.has(part)))
2875
3357
  return false;
@@ -2881,6 +3363,29 @@ function shouldIncludeWorkspaceContextPath(path) {
2881
3363
  return false;
2882
3364
  return WORKSPACE_CONTEXT_ALLOWED_EXTENSIONS.has(name.slice(dot).toLowerCase());
2883
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
+ }
2884
3389
  function numberWorkspaceContextLines(text) {
2885
3390
  return text.split(/\r?\n/).map((line, index) => {
2886
3391
  return `${String(index + 1).padStart(4, ' ')} | ${line}`;
@@ -3532,6 +4037,60 @@ function workflowScriptMetadataFromUnknown(value) {
3532
4037
  ...(typeof record.permissionKey === 'string' ? { permissionKey: record.permissionKey } : {}),
3533
4038
  };
3534
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
+ }
3535
4094
  function workflowPermissionRecordFromUnknown(value) {
3536
4095
  const record = asRecord(value);
3537
4096
  if (!record)