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 +17 -4
- package/ULTRACODE_INSTALL.md +15 -6
- package/dist/cli.js +56 -11
- package/dist/runtime/state-root.d.ts +3 -0
- package/dist/runtime/state-root.js +29 -0
- package/dist/runtime/workflow-runtime.d.ts +3 -0
- package/dist/runtime/workflow-runtime.js +622 -63
- package/docs/provenance-audit.md +3 -3
- package/docs/ultracode-p3b-resume-cache.md +27 -8
- package/package.json +1 -1
- package/settings.json +1 -1
- package/skills/ultracode-for-codex-cli/SKILL.md +10 -3
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
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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/
|
package/ULTRACODE_INSTALL.md
CHANGED
|
@@ -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
|
-
|
|
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": "
|
|
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
|
-
- `
|
|
200
|
-
|
|
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
|
|
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`:
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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 (
|
|
848
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
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,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;
|