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/README.md +154 -189
- package/ULTRACODE_INSTALL.md +35 -17
- package/dist/cli.js +74 -25
- package/dist/runtime/state-root.d.ts +3 -0
- package/dist/runtime/state-root.js +29 -0
- package/dist/runtime/workflow-journal.d.ts +1 -0
- package/dist/runtime/workflow-journal.js +5 -2
- package/dist/runtime/workflow-runtime.d.ts +3 -0
- package/dist/runtime/workflow-runtime.js +1529 -46
- package/docs/provenance-audit.md +4 -4
- 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 +18 -5
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.');
|
|
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
|
-
|
|
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
|
-
--
|
|
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,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;
|