ultracode-for-codex 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -10,10 +10,12 @@ import { CodexSubagentBackend } from './codex/subagent-backend.js';
10
10
  import { WorkflowTaskRegistry } from './runtime/workflow-runtime.js';
11
11
  import { UltracodeRequestError } from './runtime/types.js';
12
12
  import { ultracodePackageVersion } from './runtime/package-info.js';
13
+ import { defaultUltracodeStateRoot, resolveUltracodeStatePath } from './runtime/state-root.js';
13
14
  import { renderUltracodeInstallGuideNotice } from './ultracode-install-guide.js';
14
15
  import { codexDefaultReasoningEffort, codexDefaultVerbosity, isReasoningEffort, isVerbosity, isWorkflowExecutionMode, isWorkflowPermissionPolicy, isWorkflowProgressMode, workflowBackgroundDefaults, workflowDefaultExecutionMode, workflowDefaultPermissionPolicy, workflowDefaultProgressMode, workflowDefaultRetryLimit, workflowDefaultTimeoutMs, } from './settings.js';
15
16
  const ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION = 'v1';
16
17
  const PROGRESS_KIND = 'ultracode.workflow.progress';
18
+ const WORKFLOW_RUN_ID_RE = /^run_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
19
  async function main(argv) {
18
20
  const [command = 'help', ...args] = argv;
19
21
  if (command === 'help' || command === '--help' || command === '-h') {
@@ -56,13 +58,18 @@ async function runWorkflow(args) {
56
58
  }
57
59
  const cwd = options.cwd ?? process.cwd();
58
60
  const executionMode = parseExecutionMode(options.execution);
59
- if (executionMode === 'background')
61
+ const inputPromise = workflowLaunchInputFromOptions(options);
62
+ if (executionMode === 'background') {
63
+ const input = await inputPromise;
64
+ if (input.resumeFromRunId)
65
+ await assertBackgroundResumeSource(cwd, input.resumeFromRunId);
60
66
  return launchBackgroundWorkflow(args, cwd);
67
+ }
61
68
  const timeoutMs = parseIntOption(options.timeoutMs, workflowDefaultTimeoutMs());
62
69
  const retryLimit = parseRetryLimit(options.retryLimit);
63
70
  const permissionPolicy = parsePermissionPolicy(options.permission);
64
71
  const progressMode = parseProgressMode(options.progress);
65
- const input = await workflowLaunchInputFromOptions(options);
72
+ const input = await inputPromise;
66
73
  const backend = new CodexSubagentBackend({
67
74
  command: options.command,
68
75
  cwd,
@@ -107,6 +114,27 @@ async function runWorkflow(args) {
107
114
  await runtime.close();
108
115
  }
109
116
  }
117
+ async function assertBackgroundResumeSource(cwd, runId) {
118
+ const runtime = new WorkflowTaskRegistry({
119
+ backend: RESUME_PREFLIGHT_BACKEND,
120
+ cwd,
121
+ requestTimeoutMs: 0,
122
+ });
123
+ try {
124
+ await runtime.validateResumeSource(runId);
125
+ }
126
+ finally {
127
+ await runtime.close();
128
+ }
129
+ }
130
+ const RESUME_PREFLIGHT_BACKEND = {
131
+ name: 'resume-preflight',
132
+ model: 'resume-preflight',
133
+ async generate() {
134
+ throw new Error('Resume preflight must not run subagents.');
135
+ },
136
+ async close() { },
137
+ };
110
138
  export function parseOptions(args) {
111
139
  const out = { _: [] };
112
140
  for (let i = 0; i < args.length; i += 1) {
@@ -619,10 +647,9 @@ function backgroundProcessIdentityMatches(metadata, commandLine) {
619
647
  async function backgroundArchivePath(options, jobId) {
620
648
  if (options.outputPath)
621
649
  return resolve(options.outputPath);
622
- const cwd = options.cwd ?? process.cwd();
623
650
  const archiveDir = options.outDir
624
651
  ? resolve(options.outDir)
625
- : resolve(cwd, '.ultracode-for-codex', 'archive');
652
+ : join(defaultUltracodeStateRoot(), 'archive');
626
653
  return join(archiveDir, `${jobId}.json`);
627
654
  }
628
655
  async function readBackgroundProgress(progressPath) {
@@ -813,6 +840,9 @@ function delay(ms) {
813
840
  }
814
841
  function resolveBackgroundRunDir(cwd, template, jobId) {
815
842
  const expanded = template.replaceAll('{jobId}', jobId);
843
+ if (expanded.includes('{stateRoot}') || expanded === '~' || expanded.startsWith('~/')) {
844
+ return resolveUltracodeStatePath(expanded);
845
+ }
816
846
  return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
817
847
  }
818
848
  function assertDistinctBackgroundPaths(paths) {
@@ -832,9 +862,7 @@ async function workflowLaunchInputFromOptions(options) {
832
862
  if (options._.length > 1)
833
863
  throw new Error('run accepts at most one positional workflow script file.');
834
864
  const scriptFile = options.scriptFile ?? positionalScriptFile;
835
- if (options.resumeFromRunId) {
836
- throw new Error('CLI resume is not available yet; use --retry-limit for same-run retry or rerun the workflow command.');
837
- }
865
+ const hasResumeFromRunId = options.resumeFromRunId !== undefined;
838
866
  const sourceSelectors = [
839
867
  options.script !== undefined ? '--script' : '',
840
868
  scriptFile ? '--script-file' : '',
@@ -844,17 +872,33 @@ async function workflowLaunchInputFromOptions(options) {
844
872
  if (sourceSelectors.length > 1) {
845
873
  throw new Error(`Choose only one workflow source selector: ${sourceSelectors.join(', ')}.`);
846
874
  }
847
- if (sourceSelectors.length === 0) {
848
- throw new Error('run requires --script, --script-file, --script-path, --name, or a positional script file.');
875
+ if (hasResumeFromRunId)
876
+ validateCliResumeFromRunId(options.resumeFromRunId);
877
+ if (hasResumeFromRunId && sourceSelectors.length > 0) {
878
+ throw new Error('--resume-from-run-id cannot be combined with --script, --script-file, --script-path, --name, or a positional script file.');
879
+ }
880
+ if (sourceSelectors.length === 0 && !hasResumeFromRunId) {
881
+ throw new Error('run requires --script, --script-file, --script-path, --name, --resume-from-run-id, or a positional script file.');
849
882
  }
883
+ const parsedArgs = await parseArgsPayload(options);
884
+ const shouldIncludeArgs = options.args !== undefined || options.argsFile !== undefined || !hasResumeFromRunId;
850
885
  return {
851
886
  ...(options.script !== undefined ? { script: options.script } : {}),
852
887
  ...(scriptFile ? { script: await readFile(scriptFile, 'utf8') } : {}),
853
888
  ...(options.scriptPath ? { scriptPath: options.scriptPath } : {}),
854
889
  ...(options.name ? { name: options.name } : {}),
855
- args: await parseArgsPayload(options),
890
+ ...(hasResumeFromRunId ? { resumeFromRunId: options.resumeFromRunId } : {}),
891
+ ...(shouldIncludeArgs ? { args: parsedArgs } : {}),
856
892
  };
857
893
  }
894
+ function validateCliResumeFromRunId(value) {
895
+ if (typeof value !== 'string' || value.trim() === '') {
896
+ throw new Error('resumeFromRunId must be a non-empty workflow runId string.');
897
+ }
898
+ if (!WORKFLOW_RUN_ID_RE.test(value.trim())) {
899
+ throw new Error('resumeFromRunId must be a workflow runId in run_<uuid> format.');
900
+ }
901
+ }
858
902
  async function parseArgsPayload(options) {
859
903
  if (options.args !== undefined && options.argsFile) {
860
904
  throw new Error('Use either --args or --args-file, not both.');
@@ -1116,31 +1160,27 @@ function workflowPhaseExecutionSummary(events) {
1116
1160
  for (const event of events) {
1117
1161
  if (event.type !== 'workflow.agent.started' || !event.phase)
1118
1162
  continue;
1163
+ const startedAgent = {
1164
+ title: event.label,
1165
+ label: event.label,
1166
+ angle: event.promptPreview,
1167
+ };
1119
1168
  const existing = phases.get(event.phase);
1120
1169
  if (!existing) {
1121
1170
  phases.set(event.phase, {
1122
1171
  title: event.phase,
1123
1172
  agentCount: 1,
1124
- agents: [{
1125
- title: event.label,
1126
- label: event.label,
1127
- angle: event.promptPreview,
1128
- }],
1173
+ agents: [startedAgent],
1129
1174
  });
1130
1175
  continue;
1131
1176
  }
1132
- if (phaseTitlesWithPlannedAgents.has(event.phase))
1177
+ if (phaseTitlesWithPlannedAgents.has(event.phase)
1178
+ && existing.agents.length > 0
1179
+ && !phaseSummaryAllowsDynamicStartedAgents(existing))
1133
1180
  continue;
1134
1181
  if (existing.agents.some((agent) => agent.label === event.label || agent.title === event.label))
1135
1182
  continue;
1136
- const agents = [
1137
- ...existing.agents,
1138
- {
1139
- title: event.label,
1140
- label: event.label,
1141
- angle: event.promptPreview,
1142
- },
1143
- ];
1183
+ const agents = [...existing.agents, startedAgent];
1144
1184
  phases.set(event.phase, {
1145
1185
  ...existing,
1146
1186
  agentCount: agents.length,
@@ -1149,6 +1189,14 @@ function workflowPhaseExecutionSummary(events) {
1149
1189
  }
1150
1190
  return [...phases.values()];
1151
1191
  }
1192
+ function phaseSummaryAllowsDynamicStartedAgents(phase) {
1193
+ return phase.agents.some((agent) => {
1194
+ const label = agent.label ?? '';
1195
+ const title = agent.title ?? '';
1196
+ const angle = agent.angle ?? '';
1197
+ return /\bdynamic\b/i.test(`${label} ${title} ${angle}`);
1198
+ });
1199
+ }
1152
1200
  function criticalReviewRecommendation() {
1153
1201
  return 'Session LLM should critically re-check the final result before acting: verify whether the conclusion is justified, internally consistent, supported by the observed workflow evidence, and missing material counterarguments.';
1154
1202
  }
@@ -1422,7 +1470,8 @@ Options:
1422
1470
  --script-file <path> Workflow script file. A positional file path is also accepted.
1423
1471
  --script-path <path> Runtime-owned persisted workflow script path.
1424
1472
  --name <name> Named workflow from .codex/workflows or built-ins.
1425
- --args <json> Workflow args JSON. Default: {}.
1473
+ --resume-from-run-id <runId> Resume a completed local workflow run from preserved runtime state.
1474
+ --args <json> Workflow args JSON. Default: {}; resume runs inherit prior args when omitted.
1426
1475
  --args-file <path> Read workflow args JSON from a file.
1427
1476
  --permission <ask|allow|deny> Permission review behavior. Default: settings.json (${workflowDefaultPermissionPolicy()}).
1428
1477
  --retry-limit <number> Retry failed workflows in the same process. Default: settings.json (${workflowDefaultRetryLimit()}).
@@ -0,0 +1,3 @@
1
+ export declare function defaultUltracodeStateRoot(): string;
2
+ export declare function defaultWorkflowStateDir(cwd?: string): string;
3
+ export declare function resolveUltracodeStatePath(value: string): string;
@@ -0,0 +1,29 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { homedir } from 'node:os';
3
+ import { basename, join, resolve } from 'node:path';
4
+ const ULTRACODE_HOME_ENV = 'ULTRACODE_FOR_CODEX_HOME';
5
+ export function defaultUltracodeStateRoot() {
6
+ const configured = process.env[ULTRACODE_HOME_ENV]?.trim();
7
+ if (configured)
8
+ return resolveHomePath(configured);
9
+ return join(homedir(), '.ultracode-for-codex');
10
+ }
11
+ export function defaultWorkflowStateDir(cwd = process.cwd()) {
12
+ const root = resolve(cwd);
13
+ const digest = createHash('sha256').update(root).digest('hex').slice(0, 16);
14
+ const label = safeWorkspaceLabel(basename(root) || 'workspace');
15
+ return join(defaultUltracodeStateRoot(), 'workspaces', `${label}-${digest}`);
16
+ }
17
+ export function resolveUltracodeStatePath(value) {
18
+ return resolveHomePath(value.replaceAll('{stateRoot}', defaultUltracodeStateRoot()));
19
+ }
20
+ function resolveHomePath(value) {
21
+ if (value === '~')
22
+ return homedir();
23
+ if (value.startsWith('~/'))
24
+ return join(homedir(), value.slice(2));
25
+ return resolve(value);
26
+ }
27
+ function safeWorkspaceLabel(value) {
28
+ return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'workspace';
29
+ }
@@ -12,6 +12,7 @@ export interface WorkflowAgentSemanticOpts {
12
12
  readonly effort?: string;
13
13
  readonly isolation?: string;
14
14
  readonly agentType?: string;
15
+ readonly logicalKey?: string;
15
16
  }
16
17
  export type WorkflowJournalEntry = WorkflowRunStartedEntry | WorkflowAgentStartedEntry | WorkflowAgentCompletedEntry | WorkflowAgentFailedEntry | WorkflowRunCompletedEntry | WorkflowRunFailedEntry;
17
18
  interface WorkflowJournalEntryEnvelope {
@@ -219,6 +219,9 @@ export function computeWorkflowAgentCallKey(input) {
219
219
  if (!HASH_RE.test(input.previousAgentCallKey)) {
220
220
  throw new WorkflowJournalValidationError('previousAgentCallKey must be a 64-character sha256 hex digest.');
221
221
  }
222
+ if (input.semanticOpts.logicalKey) {
223
+ return sha256(`logical\0${input.semanticOpts.logicalKey}\0${input.prompt}\0${stableJson(input.semanticOpts)}`);
224
+ }
222
225
  return sha256(`${input.previousAgentCallKey}\0${input.prompt}\0${stableJson(input.semanticOpts)}`);
223
226
  }
224
227
  export function workflowJournalHash(entryWithoutEntryHash) {
@@ -473,10 +476,10 @@ function assertWorkflowAgentSemanticOpts(value) {
473
476
  const opts = asRecord(value);
474
477
  if (!opts)
475
478
  throw new WorkflowJournalValidationError('semanticOpts must be an object.');
476
- rejectUnknownKeys(opts, ['schema', 'model', 'effort', 'isolation', 'agentType'], 'semanticOpts');
479
+ rejectUnknownKeys(opts, ['schema', 'model', 'effort', 'isolation', 'agentType', 'logicalKey'], 'semanticOpts');
477
480
  if (typeof opts.model !== 'string' || !opts.model)
478
481
  throw new WorkflowJournalValidationError('semanticOpts.model must be a string.');
479
- for (const key of ['effort', 'isolation', 'agentType']) {
482
+ for (const key of ['effort', 'isolation', 'agentType', 'logicalKey']) {
480
483
  if (opts[key] !== undefined && typeof opts[key] !== 'string') {
481
484
  throw new WorkflowJournalValidationError(`semanticOpts.${key} must be a string.`);
482
485
  }
@@ -241,9 +241,12 @@ export declare class WorkflowTaskRegistry implements WorkflowRuntime {
241
241
  private readonly agentStallRetryLimit;
242
242
  constructor(options: WorkflowTaskRegistryOptions);
243
243
  launch(input: WorkflowLaunchInput): Promise<WorkflowLaunchResult>;
244
+ validateResumeSource(resumeFromRunId: string): Promise<void>;
244
245
  private prepareResumePlan;
245
246
  private workflowTaskByRunId;
247
+ private durableWorkflowResumeSource;
246
248
  private createResumeCache;
249
+ private readCompletedResumeJournal;
247
250
  private resolveLaunchInput;
248
251
  private workflowPermissionRequired;
249
252
  private resolveTrustedScriptPathMetadata;