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.
package/README.md CHANGED
@@ -162,6 +162,16 @@ npm exec -- ultracode-for-codex run \
162
162
  --args '{"prompt":"check the release plan"}'
163
163
  ```
164
164
 
165
+ Resume a completed local workflow from preserved runtime state:
166
+
167
+ ```bash
168
+ npm exec -- ultracode-for-codex run \
169
+ --accept-llm-guide=v1 \
170
+ --execution attached \
171
+ --cwd /path/to/project \
172
+ --resume-from-run-id run_...
173
+ ```
174
+
165
175
  ## What Gets Installed
166
176
 
167
177
  The package includes:
@@ -174,11 +184,14 @@ The package includes:
174
184
 
175
185
  ## Local State
176
186
 
177
- CLI background runs write local workflow state under `.ultracode-for-codex/` in
178
- the target project. Treat that folder as local runtime data. It may contain
179
- progress, metadata, transcripts, and results for the run.
187
+ CLI runs write workflow state under `${ULTRACODE_FOR_CODEX_HOME:-~/.ultracode-for-codex}`.
188
+ The runtime keeps background metadata, journals, transcripts, generated scripts,
189
+ and results outside the target project so review evidence stays focused on the
190
+ workspace itself.
180
191
 
181
- Add it to `.gitignore` if your project does not already ignore it:
192
+ Project workflow sources may still live in `.codex/workflows/`. If an older
193
+ workspace already has `.ultracode-for-codex/`, keep it ignored and treat it as
194
+ legacy sensitive local data:
182
195
 
183
196
  ```gitignore
184
197
  .ultracode-for-codex/
@@ -22,7 +22,7 @@ Skill commands:
22
22
 
23
23
  The packaged `settings.json` defaults CLI workflow runs to OS background
24
24
  execution with result and progress files under
25
- `.ultracode-for-codex/background/{jobId}`.
25
+ `${ULTRACODE_FOR_CODEX_HOME:-~/.ultracode-for-codex}/background/{jobId}`.
26
26
 
27
27
  Production surface:
28
28
 
@@ -124,7 +124,7 @@ Settings defaults:
124
124
  "retryLimit": 0,
125
125
  "timeoutMs": 0,
126
126
  "background": {
127
- "runDir": ".ultracode-for-codex/background/{jobId}",
127
+ "runDir": "{stateRoot}/background/{jobId}",
128
128
  "resultFile": "result.json",
129
129
  "progressFile": "progress.jsonl",
130
130
  "metadataFile": "metadata.json",
@@ -166,6 +166,9 @@ Useful controls:
166
166
  acting on it.
167
167
  - Press `Ctrl-C` once to cancel the running workflow.
168
168
  - Use `--retry-limit <n>` to retry failed runs in the same process.
169
+ - Use `--resume-from-run-id <runId>` to resume a completed local workflow from
170
+ preserved runtime state. Resume always uses the original persisted workflow
171
+ source; without `--args`, it also reuses the original args.
169
172
  - `--timeout-ms 0` waits for completion, cancellation, or app-server exit.
170
173
  Positive values opt into a workflow deadline and per-agent silence budget;
171
174
  that budget is not divided by the retry budget.
@@ -196,12 +199,18 @@ Useful controls:
196
199
  - Keep `journalPath`, `journal.jsonl`, and journal contents out of CLI output.
197
200
  Local runtime state may still contain runtime-owned
198
201
  `transcriptDir`, `scriptPath`, and result files.
199
- - `resumeFromRunId` remains a runtime-internal same-session capability; the
200
- CLI uses retry or explicit reruns for user-facing recovery.
202
+ - `--resume-from-run-id` reads the preserved runtime script, result record, and
203
+ completed journal from the workflow state directory under
204
+ `${ULTRACODE_FOR_CODEX_HOME:-~/.ultracode-for-codex}`; completed agent
205
+ results are reused only when their runtime-owned call keys still match. The
206
+ script path, script source identity, and inherited args must match the
207
+ completed journal.
201
208
  - Use `isolation: "worktree"` only in git repositories with at least one commit.
202
209
  Isolated worktrees are intentionally preserved for review, including clean
203
210
  worktrees.
204
- - Treat `.ultracode-for-codex` workflow state as sensitive local data.
211
+ - Treat workflow state under `${ULTRACODE_FOR_CODEX_HOME:-~/.ultracode-for-codex}`
212
+ as sensitive local data. Project-local `.ultracode-for-codex/` directories are
213
+ legacy state and should stay ignored.
205
214
 
206
215
  ## First Checks After Install
207
216
 
@@ -224,5 +233,5 @@ workflow.
224
233
  progress and completion summary examples for native orchestration.
225
234
  - `skills/ultracode-for-codex-cli/SKILL.md`: explicit CLI runtime command.
226
235
  - `docs/ultracode-p3a-journal-design.md`: implemented journal contract.
227
- - `docs/ultracode-p3b-resume-cache.md`: runtime-internal resume/cache contract.
236
+ - `docs/ultracode-p3b-resume-cache.md`: local resume/cache contract.
228
237
  - `docs/ultracode-p3c-worktree-isolation.md`: worktree isolation contract.
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.');
849
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.');
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.');
@@ -1426,7 +1470,8 @@ Options:
1426
1470
  --script-file <path> Workflow script file. A positional file path is also accepted.
1427
1471
  --script-path <path> Runtime-owned persisted workflow script path.
1428
1472
  --name <name> Named workflow from .codex/workflows or built-ins.
1429
- --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.
1430
1475
  --args-file <path> Read workflow args JSON from a file.
1431
1476
  --permission <ask|allow|deny> Permission review behavior. Default: settings.json (${workflowDefaultPermissionPolicy()}).
1432
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
+ }
@@ -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;