ultracode-for-codex 0.2.0
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 +182 -0
- package/ULTRACODE_INSTALL.md +133 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +616 -0
- package/dist/codex/env.d.ts +2 -0
- package/dist/codex/env.js +45 -0
- package/dist/codex/subagent-backend.d.ts +72 -0
- package/dist/codex/subagent-backend.js +685 -0
- package/dist/runtime/async-queue.d.ts +10 -0
- package/dist/runtime/async-queue.js +55 -0
- package/dist/runtime/types.d.ts +61 -0
- package/dist/runtime/types.js +18 -0
- package/dist/runtime/workflow-journal.d.ts +135 -0
- package/dist/runtime/workflow-journal.js +681 -0
- package/dist/runtime/workflow-runtime.d.ts +266 -0
- package/dist/runtime/workflow-runtime.js +3280 -0
- package/dist/settings.d.ts +38 -0
- package/dist/settings.js +153 -0
- package/dist/ultracode-install-guide.d.ts +4 -0
- package/dist/ultracode-install-guide.js +22 -0
- package/docs/ultracode-p3a-journal-design.md +78 -0
- package/docs/ultracode-p3b-resume-cache.md +43 -0
- package/docs/ultracode-p3c-worktree-isolation.md +60 -0
- package/package.json +77 -0
- package/postinstall.mjs +23 -0
- package/settings.json +20 -0
- package/skills/ultracode-for-codex/SKILL.md +102 -0
- package/skills/ultracode-for-codex/agents/openai.yaml +4 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { mkdir, open, readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { createInterface } from 'node:readline/promises';
|
|
9
|
+
import { CodexSubagentBackend } from './codex/subagent-backend.js';
|
|
10
|
+
import { WorkflowTaskRegistry } from './runtime/workflow-runtime.js';
|
|
11
|
+
import { UltracodeRequestError } from './runtime/types.js';
|
|
12
|
+
import { renderUltracodeInstallGuideNotice } from './ultracode-install-guide.js';
|
|
13
|
+
import { codexDefaultReasoningEffort, codexDefaultVerbosity, isReasoningEffort, isVerbosity, isWorkflowExecutionMode, isWorkflowPermissionPolicy, isWorkflowProgressMode, workflowBackgroundDefaults, workflowDefaultExecutionMode, workflowDefaultPermissionPolicy, workflowDefaultProgressMode, workflowDefaultRetryLimit, workflowDefaultTimeoutMs, } from './settings.js';
|
|
14
|
+
const ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION = 'v1';
|
|
15
|
+
const PROGRESS_KIND = 'ultracode.workflow.progress';
|
|
16
|
+
async function main(argv) {
|
|
17
|
+
const [command = 'help', ...args] = argv;
|
|
18
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
19
|
+
process.stdout.write(helpText());
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
if (command === '--llm-guide' || command === 'llm-guide') {
|
|
23
|
+
process.stdout.write(renderUltracodeInstallGuideNotice());
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
if (command === 'run')
|
|
27
|
+
return runWorkflow(args);
|
|
28
|
+
process.stderr.write(`Unknown command: ${command}\n\n${helpText()}`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
async function runWorkflow(args) {
|
|
32
|
+
const options = parseOptions(args);
|
|
33
|
+
if (options.acceptLlmGuide !== ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION) {
|
|
34
|
+
process.stdout.write(renderUltracodeInstallGuideNotice());
|
|
35
|
+
process.stderr.write(`Refusing to run Ultracode for Codex until the install guide is acknowledged. Re-run with --accept-llm-guide=${ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION}.\n`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const cwd = options.cwd ?? process.cwd();
|
|
39
|
+
const executionMode = parseExecutionMode(options.execution);
|
|
40
|
+
if (executionMode === 'background')
|
|
41
|
+
return launchBackgroundWorkflow(args, cwd);
|
|
42
|
+
const timeoutMs = parseIntOption(options.timeoutMs, workflowDefaultTimeoutMs());
|
|
43
|
+
const retryLimit = parseRetryLimit(options.retryLimit);
|
|
44
|
+
const permissionPolicy = parsePermissionPolicy(options.permission);
|
|
45
|
+
const progressMode = parseProgressMode(options.progress);
|
|
46
|
+
const input = await workflowLaunchInputFromOptions(options);
|
|
47
|
+
const backend = new CodexSubagentBackend({
|
|
48
|
+
command: options.command,
|
|
49
|
+
cwd,
|
|
50
|
+
model: options.model,
|
|
51
|
+
timeoutMs,
|
|
52
|
+
reasoningEffort: parseReasoningEffort(options.reasoningEffort),
|
|
53
|
+
verbosity: parseVerbosity(options.verbosity),
|
|
54
|
+
});
|
|
55
|
+
const runtime = new WorkflowTaskRegistry({
|
|
56
|
+
backend,
|
|
57
|
+
cwd,
|
|
58
|
+
requestTimeoutMs: timeoutMs,
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
let launch = await requireRunnableLaunch(runtime, await runtime.launch(input), permissionPolicy, progressMode);
|
|
62
|
+
let retries = 0;
|
|
63
|
+
while (true) {
|
|
64
|
+
const snapshot = await streamCommandWorkflow(runtime, launch, progressMode);
|
|
65
|
+
if (snapshot.status === 'completed') {
|
|
66
|
+
process.stdout.write(`${JSON.stringify(snapshot.result ?? null, null, 2)}\n`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
renderFailedSnapshot(snapshot, progressMode);
|
|
70
|
+
if (snapshot.failureReason === 'workflow_aborted' || retries >= retryLimit) {
|
|
71
|
+
return snapshot.failureReason === 'workflow_aborted' ? 130 : 1;
|
|
72
|
+
}
|
|
73
|
+
retries += 1;
|
|
74
|
+
renderControlProgress('workflow.retrying', progressMode, {
|
|
75
|
+
status: 'retrying',
|
|
76
|
+
summary: `Retrying workflow ${retries}/${retryLimit}`,
|
|
77
|
+
taskId: snapshot.taskId,
|
|
78
|
+
runId: snapshot.runId,
|
|
79
|
+
workflowName: snapshot.workflowName,
|
|
80
|
+
retryIndex: retries,
|
|
81
|
+
retryLimit,
|
|
82
|
+
}, `[workflow] retrying ${retries}/${retryLimit}\n`);
|
|
83
|
+
launch = await requireRunnableLaunch(runtime, await runtime.retry(snapshot.taskId), permissionPolicy, progressMode);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await runtime.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function parseOptions(args) {
|
|
91
|
+
const out = { _: [] };
|
|
92
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
93
|
+
const arg = args[i] ?? '';
|
|
94
|
+
if (!arg.startsWith('--')) {
|
|
95
|
+
out._.push(arg);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const eq = arg.indexOf('=');
|
|
99
|
+
const rawKey = arg.slice(2, eq === -1 ? undefined : eq);
|
|
100
|
+
const key = rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
101
|
+
const value = eq === -1 ? args[i + 1] : arg.slice(eq + 1);
|
|
102
|
+
if (eq === -1 && (value === undefined || value.startsWith('--'))) {
|
|
103
|
+
out[key] = 'true';
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (eq === -1)
|
|
107
|
+
i += 1;
|
|
108
|
+
out[key] = value ?? '';
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
async function launchBackgroundWorkflow(args, cwd) {
|
|
113
|
+
const settings = workflowBackgroundDefaults();
|
|
114
|
+
const jobId = `job_${randomUUID()}`;
|
|
115
|
+
const runDir = resolveBackgroundRunDir(cwd, settings.runDir, jobId);
|
|
116
|
+
const resultPath = join(runDir, settings.resultFile);
|
|
117
|
+
const progressPath = join(runDir, settings.progressFile);
|
|
118
|
+
const metadataPath = join(runDir, settings.metadataFile);
|
|
119
|
+
const pidPath = join(runDir, settings.pidFile);
|
|
120
|
+
assertDistinctBackgroundPaths([resultPath, progressPath, metadataPath, pidPath]);
|
|
121
|
+
await mkdir(runDir, { recursive: true });
|
|
122
|
+
const stdout = await open(resultPath, 'w');
|
|
123
|
+
const stderr = await open(progressPath, 'w');
|
|
124
|
+
let childPid = 0;
|
|
125
|
+
try {
|
|
126
|
+
const child = spawn(process.execPath, [
|
|
127
|
+
cliEntryPath(),
|
|
128
|
+
'run',
|
|
129
|
+
...args,
|
|
130
|
+
'--execution',
|
|
131
|
+
'attached',
|
|
132
|
+
], {
|
|
133
|
+
cwd: process.cwd(),
|
|
134
|
+
detached: true,
|
|
135
|
+
stdio: ['ignore', stdout.fd, stderr.fd],
|
|
136
|
+
});
|
|
137
|
+
childPid = child.pid ?? 0;
|
|
138
|
+
child.unref();
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await stdout.close();
|
|
142
|
+
await stderr.close();
|
|
143
|
+
}
|
|
144
|
+
const launchedAt = new Date().toISOString();
|
|
145
|
+
await writeFile(pidPath, `${childPid}\n`);
|
|
146
|
+
await writeFile(metadataPath, `${JSON.stringify({
|
|
147
|
+
kind: 'ultracode.workflow.background',
|
|
148
|
+
version: 1,
|
|
149
|
+
status: 'launched',
|
|
150
|
+
jobId,
|
|
151
|
+
pid: childPid,
|
|
152
|
+
launchedAt,
|
|
153
|
+
cwd,
|
|
154
|
+
resultPath,
|
|
155
|
+
progressPath,
|
|
156
|
+
metadataPath,
|
|
157
|
+
pidPath,
|
|
158
|
+
}, null, 2)}\n`);
|
|
159
|
+
process.stdout.write(`${JSON.stringify({
|
|
160
|
+
kind: 'ultracode.workflow.background',
|
|
161
|
+
version: 1,
|
|
162
|
+
status: 'launched',
|
|
163
|
+
jobId,
|
|
164
|
+
pid: childPid,
|
|
165
|
+
resultPath,
|
|
166
|
+
progressPath,
|
|
167
|
+
metadataPath,
|
|
168
|
+
pidPath,
|
|
169
|
+
}, null, 2)}\n`);
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
function resolveBackgroundRunDir(cwd, template, jobId) {
|
|
173
|
+
const expanded = template.replaceAll('{jobId}', jobId);
|
|
174
|
+
return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
|
|
175
|
+
}
|
|
176
|
+
function assertDistinctBackgroundPaths(paths) {
|
|
177
|
+
const normalized = new Set(paths);
|
|
178
|
+
if (normalized.size !== paths.length) {
|
|
179
|
+
throw new Error('Background result, progress, metadata, and pid paths must be distinct.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function cliEntryPath() {
|
|
183
|
+
const entry = process.argv[1];
|
|
184
|
+
if (!entry)
|
|
185
|
+
throw new Error('Unable to locate CLI entry path for background launch.');
|
|
186
|
+
return realpathSync(entry);
|
|
187
|
+
}
|
|
188
|
+
async function workflowLaunchInputFromOptions(options) {
|
|
189
|
+
const positionalScriptFile = options._[0];
|
|
190
|
+
if (options._.length > 1)
|
|
191
|
+
throw new Error('run accepts at most one positional workflow script file.');
|
|
192
|
+
const scriptFile = options.scriptFile ?? positionalScriptFile;
|
|
193
|
+
if (options.resumeFromRunId) {
|
|
194
|
+
throw new Error('CLI resume is not available yet; use --retry-limit for same-run retry or rerun the workflow command.');
|
|
195
|
+
}
|
|
196
|
+
const sourceSelectors = [
|
|
197
|
+
options.script !== undefined ? '--script' : '',
|
|
198
|
+
scriptFile ? '--script-file' : '',
|
|
199
|
+
options.scriptPath ? '--script-path' : '',
|
|
200
|
+
options.name ? '--name' : '',
|
|
201
|
+
].filter(Boolean);
|
|
202
|
+
if (sourceSelectors.length > 1) {
|
|
203
|
+
throw new Error(`Choose only one workflow source selector: ${sourceSelectors.join(', ')}.`);
|
|
204
|
+
}
|
|
205
|
+
if (sourceSelectors.length === 0) {
|
|
206
|
+
throw new Error('run requires --script, --script-file, --script-path, --name, or a positional script file.');
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
...(options.script !== undefined ? { script: options.script } : {}),
|
|
210
|
+
...(scriptFile ? { script: await readFile(scriptFile, 'utf8') } : {}),
|
|
211
|
+
...(options.scriptPath ? { scriptPath: options.scriptPath } : {}),
|
|
212
|
+
...(options.name ? { name: options.name } : {}),
|
|
213
|
+
args: await parseArgsPayload(options),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async function parseArgsPayload(options) {
|
|
217
|
+
if (options.args !== undefined && options.argsFile) {
|
|
218
|
+
throw new Error('Use either --args or --args-file, not both.');
|
|
219
|
+
}
|
|
220
|
+
const text = options.argsFile ? await readFile(options.argsFile, 'utf8') : options.args;
|
|
221
|
+
if (text === undefined || text.trim() === '')
|
|
222
|
+
return {};
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(text);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
throw new Error('Workflow args must be valid JSON.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function requireRunnableLaunch(runtime, launched, permissionPolicy, progressMode) {
|
|
231
|
+
let current = launched;
|
|
232
|
+
while (current.status === 'permission_required') {
|
|
233
|
+
const allow = await resolvePermissionReview(current.review, permissionPolicy, progressMode);
|
|
234
|
+
if (!allow) {
|
|
235
|
+
const denied = await runtime.denyPermissionRequest(current.permissionRequestId);
|
|
236
|
+
throw new Error(`Workflow permission denied: ${denied.workflowName}`);
|
|
237
|
+
}
|
|
238
|
+
current = await runtime.approvePermissionRequest(current.permissionRequestId);
|
|
239
|
+
}
|
|
240
|
+
if (current.status !== 'async_launched') {
|
|
241
|
+
throw new Error('CLI supports only local workflow launches.');
|
|
242
|
+
}
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
async function resolvePermissionReview(review, permissionPolicy, progressMode) {
|
|
246
|
+
renderPermissionReview(review, progressMode);
|
|
247
|
+
if (permissionPolicy === 'allow')
|
|
248
|
+
return true;
|
|
249
|
+
if (permissionPolicy === 'deny')
|
|
250
|
+
return false;
|
|
251
|
+
if (!process.stdin.isTTY) {
|
|
252
|
+
throw new Error('Workflow permission review requires --permission allow or --permission deny in non-interactive terminals.');
|
|
253
|
+
}
|
|
254
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
255
|
+
try {
|
|
256
|
+
const answer = (await rl.question('Allow this workflow? [y/N] ')).trim().toLowerCase();
|
|
257
|
+
return answer === 'y' || answer === 'yes';
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
rl.close();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function renderPermissionReview(review, progressMode) {
|
|
264
|
+
if (progressMode === 'jsonl') {
|
|
265
|
+
writeJsonlProgress({
|
|
266
|
+
event: 'workflow.permission.required',
|
|
267
|
+
status: 'waiting_for_permission',
|
|
268
|
+
summary: `Permission review required for ${review.workflowName}`,
|
|
269
|
+
permissionRequestId: review.permissionRequestId,
|
|
270
|
+
workflowName: review.workflowName,
|
|
271
|
+
workflowSource: review.workflowSource,
|
|
272
|
+
workflowSourcePath: review.workflowSourcePath,
|
|
273
|
+
scriptHash: review.scriptHash,
|
|
274
|
+
riskSummary: review.riskSummary,
|
|
275
|
+
phases: review.phases,
|
|
276
|
+
requestedIsolationModes: review.requestedIsolationModes,
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
process.stderr.write([
|
|
281
|
+
'[permission] workflow review required',
|
|
282
|
+
` name: ${review.workflowName}`,
|
|
283
|
+
` source: ${review.workflowSource}${review.workflowSourcePath ? ` (${review.workflowSourcePath})` : ''}`,
|
|
284
|
+
` scriptHash: ${review.scriptHash}`,
|
|
285
|
+
` risk: ${review.riskSummary}`,
|
|
286
|
+
review.phases.length > 0 ? ` phases: ${review.phases.join(', ')}` : '',
|
|
287
|
+
review.requestedIsolationModes.length > 0 ? ` isolation: ${review.requestedIsolationModes.join(', ')}` : '',
|
|
288
|
+
].filter(Boolean).join('\n'));
|
|
289
|
+
process.stderr.write('\n');
|
|
290
|
+
}
|
|
291
|
+
async function streamCommandWorkflow(runtime, launch, progressMode) {
|
|
292
|
+
let cancelling = false;
|
|
293
|
+
const cancel = (signal) => {
|
|
294
|
+
if (cancelling) {
|
|
295
|
+
renderControlProgress('workflow.cancel.already_requested', progressMode, {
|
|
296
|
+
status: 'cancelling',
|
|
297
|
+
summary: `${signal} received while cancellation is already in progress`,
|
|
298
|
+
taskId: launch.taskId,
|
|
299
|
+
runId: launch.runId,
|
|
300
|
+
workflowName: launch.workflowName,
|
|
301
|
+
signal,
|
|
302
|
+
}, `[workflow] ${signal} received while cancellation is already in progress\n`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
cancelling = true;
|
|
306
|
+
renderControlProgress('workflow.cancel.requested', progressMode, {
|
|
307
|
+
status: 'cancelling',
|
|
308
|
+
summary: `${signal} received; cancelling workflow`,
|
|
309
|
+
taskId: launch.taskId,
|
|
310
|
+
runId: launch.runId,
|
|
311
|
+
workflowName: launch.workflowName,
|
|
312
|
+
signal,
|
|
313
|
+
}, `[workflow] ${signal} received; cancelling ${launch.taskId}\n`);
|
|
314
|
+
void runtime.cancel(launch.taskId).catch((err) => {
|
|
315
|
+
renderControlProgress('workflow.cancel.failed', progressMode, {
|
|
316
|
+
status: 'failed',
|
|
317
|
+
summary: 'Workflow cancellation failed',
|
|
318
|
+
taskId: launch.taskId,
|
|
319
|
+
runId: launch.runId,
|
|
320
|
+
workflowName: launch.workflowName,
|
|
321
|
+
error: errorMessage(err),
|
|
322
|
+
}, `[workflow] cancellation failed: ${errorMessage(err)}\n`);
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
process.once('SIGINT', cancel);
|
|
326
|
+
process.once('SIGTERM', cancel);
|
|
327
|
+
try {
|
|
328
|
+
for await (const event of runtime.streamEvents(launch.taskId)) {
|
|
329
|
+
renderWorkflowEvent(event, progressMode);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
process.off('SIGINT', cancel);
|
|
334
|
+
process.off('SIGTERM', cancel);
|
|
335
|
+
}
|
|
336
|
+
const snapshot = runtime.get(launch.taskId);
|
|
337
|
+
if (!snapshot)
|
|
338
|
+
throw new Error(`Workflow task disappeared: ${launch.taskId}`);
|
|
339
|
+
return snapshot;
|
|
340
|
+
}
|
|
341
|
+
function renderWorkflowEvent(event, progressMode) {
|
|
342
|
+
if (progressMode === 'jsonl') {
|
|
343
|
+
writeJsonlProgress(progressPayloadForEvent(event));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
switch (event.type) {
|
|
347
|
+
case 'workflow.started':
|
|
348
|
+
process.stderr.write(`[workflow] started ${event.workflowName} task=${event.taskId} run=${event.runId}\n`);
|
|
349
|
+
return;
|
|
350
|
+
case 'workflow.phase.started':
|
|
351
|
+
process.stderr.write(`[phase] ${event.title}${event.detail ? ` - ${event.detail}` : ''}\n`);
|
|
352
|
+
return;
|
|
353
|
+
case 'workflow.log':
|
|
354
|
+
process.stderr.write(`[log] ${event.message}\n`);
|
|
355
|
+
return;
|
|
356
|
+
case 'workflow.agent.started':
|
|
357
|
+
process.stderr.write(`[agent:${event.agentIndex + 1}] started ${event.label}\n`);
|
|
358
|
+
return;
|
|
359
|
+
case 'workflow.agent.completed':
|
|
360
|
+
process.stderr.write(`[agent:${event.agentIndex + 1}] completed ${event.label} tokens=${event.tokens} preview=${formatPreview(event.resultPreview)}${event.cached ? ' cached=true' : ''}\n`);
|
|
361
|
+
return;
|
|
362
|
+
case 'workflow.agent.failed':
|
|
363
|
+
process.stderr.write(`[agent:${event.agentIndex + 1}] failed ${event.label} ${event.error}\n`);
|
|
364
|
+
return;
|
|
365
|
+
case 'workflow.completed':
|
|
366
|
+
process.stderr.write(`[workflow] completed agents=${event.agentCount} tokens=${event.tokens} result=${event.resultPath}\n`);
|
|
367
|
+
return;
|
|
368
|
+
case 'workflow.failed':
|
|
369
|
+
process.stderr.write(`[workflow] failed ${event.recovery?.reason ?? ''} ${event.error}\n`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function renderFailedSnapshot(snapshot, progressMode) {
|
|
374
|
+
if (progressMode === 'jsonl') {
|
|
375
|
+
writeJsonlProgress({
|
|
376
|
+
event: 'workflow.terminal_failure',
|
|
377
|
+
status: 'failed',
|
|
378
|
+
summary: `Workflow terminal failure: ${snapshot.failureReason ?? 'unknown'}`,
|
|
379
|
+
taskId: snapshot.taskId,
|
|
380
|
+
runId: snapshot.runId,
|
|
381
|
+
workflowName: snapshot.workflowName,
|
|
382
|
+
reason: snapshot.failureReason ?? 'unknown',
|
|
383
|
+
error: snapshot.error ?? 'unknown',
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
process.stderr.write(`[workflow] terminal failure task=${snapshot.taskId} reason=${snapshot.failureReason ?? 'unknown'} error=${snapshot.error ?? 'unknown'}\n`);
|
|
388
|
+
}
|
|
389
|
+
function renderControlProgress(event, progressMode, payload, plainText) {
|
|
390
|
+
if (progressMode === 'jsonl') {
|
|
391
|
+
writeJsonlProgress({ event, ...payload });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
process.stderr.write(plainText);
|
|
395
|
+
}
|
|
396
|
+
function writeJsonlProgress(payload) {
|
|
397
|
+
process.stderr.write(`${JSON.stringify({
|
|
398
|
+
kind: PROGRESS_KIND,
|
|
399
|
+
version: 1,
|
|
400
|
+
...payload,
|
|
401
|
+
})}\n`);
|
|
402
|
+
}
|
|
403
|
+
function progressPayloadForEvent(event) {
|
|
404
|
+
switch (event.type) {
|
|
405
|
+
case 'workflow.started':
|
|
406
|
+
return {
|
|
407
|
+
event: event.type,
|
|
408
|
+
status: 'running',
|
|
409
|
+
summary: `Workflow ${event.workflowName} started`,
|
|
410
|
+
taskId: event.taskId,
|
|
411
|
+
runId: event.runId,
|
|
412
|
+
workflowName: event.workflowName,
|
|
413
|
+
workflowSource: event.workflowSource,
|
|
414
|
+
workflowSourcePath: event.workflowSourcePath,
|
|
415
|
+
scriptHash: event.scriptHash,
|
|
416
|
+
};
|
|
417
|
+
case 'workflow.phase.started':
|
|
418
|
+
return {
|
|
419
|
+
event: event.type,
|
|
420
|
+
status: 'running',
|
|
421
|
+
summary: event.detail ? `Phase ${event.title}: ${event.detail}` : `Phase ${event.title}`,
|
|
422
|
+
taskId: event.taskId,
|
|
423
|
+
runId: event.runId,
|
|
424
|
+
phaseIndex: event.phaseIndex,
|
|
425
|
+
title: event.title,
|
|
426
|
+
detail: event.detail,
|
|
427
|
+
};
|
|
428
|
+
case 'workflow.log':
|
|
429
|
+
return {
|
|
430
|
+
event: event.type,
|
|
431
|
+
status: 'running',
|
|
432
|
+
summary: event.message,
|
|
433
|
+
taskId: event.taskId,
|
|
434
|
+
runId: event.runId,
|
|
435
|
+
message: event.message,
|
|
436
|
+
};
|
|
437
|
+
case 'workflow.agent.started':
|
|
438
|
+
return {
|
|
439
|
+
event: event.type,
|
|
440
|
+
status: 'running',
|
|
441
|
+
summary: `Agent ${event.agentIndex + 1} started: ${event.label}`,
|
|
442
|
+
taskId: event.taskId,
|
|
443
|
+
runId: event.runId,
|
|
444
|
+
agentIndex: event.agentIndex,
|
|
445
|
+
agentId: event.agentId,
|
|
446
|
+
label: event.label,
|
|
447
|
+
phase: event.phase,
|
|
448
|
+
promptPreview: event.promptPreview,
|
|
449
|
+
};
|
|
450
|
+
case 'workflow.agent.completed':
|
|
451
|
+
return {
|
|
452
|
+
event: event.type,
|
|
453
|
+
status: 'completed',
|
|
454
|
+
summary: `Agent ${event.agentIndex + 1} completed`,
|
|
455
|
+
taskId: event.taskId,
|
|
456
|
+
runId: event.runId,
|
|
457
|
+
agentIndex: event.agentIndex,
|
|
458
|
+
agentId: event.agentId,
|
|
459
|
+
label: event.label,
|
|
460
|
+
phase: event.phase,
|
|
461
|
+
tokens: event.tokens,
|
|
462
|
+
toolCalls: event.toolCalls,
|
|
463
|
+
resultPreview: event.resultPreview,
|
|
464
|
+
cached: event.cached,
|
|
465
|
+
worktreePreserved: event.worktreePreserved,
|
|
466
|
+
preservedWorktrees: event.preservedWorktrees,
|
|
467
|
+
};
|
|
468
|
+
case 'workflow.agent.failed':
|
|
469
|
+
return {
|
|
470
|
+
event: event.type,
|
|
471
|
+
status: event.skipped ? 'skipped' : 'failed',
|
|
472
|
+
summary: `Agent ${event.agentIndex + 1} ${event.skipped ? 'skipped' : 'failed'}: ${event.error}`,
|
|
473
|
+
taskId: event.taskId,
|
|
474
|
+
runId: event.runId,
|
|
475
|
+
agentIndex: event.agentIndex,
|
|
476
|
+
agentId: event.agentId,
|
|
477
|
+
label: event.label,
|
|
478
|
+
phase: event.phase,
|
|
479
|
+
error: event.error,
|
|
480
|
+
skipped: event.skipped,
|
|
481
|
+
worktreePreserved: event.worktreePreserved,
|
|
482
|
+
preservedWorktrees: event.preservedWorktrees,
|
|
483
|
+
};
|
|
484
|
+
case 'workflow.completed':
|
|
485
|
+
return {
|
|
486
|
+
event: event.type,
|
|
487
|
+
status: 'completed',
|
|
488
|
+
summary: `Workflow completed with ${event.agentCount} agent${event.agentCount === 1 ? '' : 's'}`,
|
|
489
|
+
taskId: event.taskId,
|
|
490
|
+
runId: event.runId,
|
|
491
|
+
resultPath: event.resultPath,
|
|
492
|
+
agentCount: event.agentCount,
|
|
493
|
+
tokens: event.tokens,
|
|
494
|
+
toolCalls: event.toolCalls,
|
|
495
|
+
durationMs: event.durationMs,
|
|
496
|
+
};
|
|
497
|
+
case 'workflow.failed':
|
|
498
|
+
return {
|
|
499
|
+
event: event.type,
|
|
500
|
+
status: 'failed',
|
|
501
|
+
summary: `Workflow failed: ${event.recovery?.reason ?? event.error}`,
|
|
502
|
+
taskId: event.taskId,
|
|
503
|
+
runId: event.runId,
|
|
504
|
+
error: event.error,
|
|
505
|
+
reason: event.recovery?.reason,
|
|
506
|
+
retryable: event.recovery?.retryable,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function formatPreview(value) {
|
|
511
|
+
if (value === undefined || value === null)
|
|
512
|
+
return '';
|
|
513
|
+
return String(value).replace(/\s+/g, ' ').slice(0, 160);
|
|
514
|
+
}
|
|
515
|
+
function parseIntOption(value, fallback) {
|
|
516
|
+
if (value === undefined)
|
|
517
|
+
return fallback;
|
|
518
|
+
const parsed = Number.parseInt(value, 10);
|
|
519
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
520
|
+
return fallback;
|
|
521
|
+
return parsed;
|
|
522
|
+
}
|
|
523
|
+
function parseRetryLimit(value) {
|
|
524
|
+
if (value === undefined)
|
|
525
|
+
return workflowDefaultRetryLimit();
|
|
526
|
+
const parsed = Number.parseInt(value, 10);
|
|
527
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
528
|
+
throw new Error('retry-limit must be a non-negative integer.');
|
|
529
|
+
return parsed;
|
|
530
|
+
}
|
|
531
|
+
function parsePermissionPolicy(value) {
|
|
532
|
+
if (value === undefined)
|
|
533
|
+
return workflowDefaultPermissionPolicy();
|
|
534
|
+
if (isWorkflowPermissionPolicy(value))
|
|
535
|
+
return value;
|
|
536
|
+
throw new Error('permission must be one of ask, allow, or deny.');
|
|
537
|
+
}
|
|
538
|
+
function parseProgressMode(value) {
|
|
539
|
+
if (value === undefined)
|
|
540
|
+
return workflowDefaultProgressMode();
|
|
541
|
+
if (isWorkflowProgressMode(value))
|
|
542
|
+
return value;
|
|
543
|
+
throw new Error('progress must be one of jsonl or plain.');
|
|
544
|
+
}
|
|
545
|
+
function parseExecutionMode(value) {
|
|
546
|
+
if (value === undefined)
|
|
547
|
+
return workflowDefaultExecutionMode();
|
|
548
|
+
if (isWorkflowExecutionMode(value))
|
|
549
|
+
return value;
|
|
550
|
+
throw new Error('execution must be one of background or attached.');
|
|
551
|
+
}
|
|
552
|
+
function parseReasoningEffort(value) {
|
|
553
|
+
if (value === undefined)
|
|
554
|
+
return codexDefaultReasoningEffort();
|
|
555
|
+
if (isReasoningEffort(value))
|
|
556
|
+
return value;
|
|
557
|
+
throw new Error('reasoning effort must be one of none, minimal, low, medium, high, or xhigh.');
|
|
558
|
+
}
|
|
559
|
+
function parseVerbosity(value) {
|
|
560
|
+
if (value === undefined)
|
|
561
|
+
return codexDefaultVerbosity();
|
|
562
|
+
if (isVerbosity(value))
|
|
563
|
+
return value;
|
|
564
|
+
throw new Error('verbosity must be one of low, medium, or high.');
|
|
565
|
+
}
|
|
566
|
+
function errorMessage(err) {
|
|
567
|
+
if (err instanceof UltracodeRequestError && err.code)
|
|
568
|
+
return `${err.code}: ${err.message}`;
|
|
569
|
+
return err instanceof Error ? err.message : String(err);
|
|
570
|
+
}
|
|
571
|
+
function helpText() {
|
|
572
|
+
return `ultracode-for-codex
|
|
573
|
+
|
|
574
|
+
Commands:
|
|
575
|
+
run Run a workflow as a local CLI command.
|
|
576
|
+
|
|
577
|
+
Options:
|
|
578
|
+
--llm-guide Print the Ultracode install and usage guide.
|
|
579
|
+
--accept-llm-guide <version> Required for run. Current version: ${ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION}.
|
|
580
|
+
--script <js> Inline workflow script.
|
|
581
|
+
--script-file <path> Workflow script file. A positional file path is also accepted.
|
|
582
|
+
--script-path <path> Runtime-owned persisted workflow script path.
|
|
583
|
+
--name <name> Named workflow from .codex/workflows or built-ins.
|
|
584
|
+
--args <json> Workflow args JSON. Default: {}.
|
|
585
|
+
--args-file <path> Read workflow args JSON from a file.
|
|
586
|
+
--permission <ask|allow|deny> Permission review behavior. Default: settings.json (${workflowDefaultPermissionPolicy()}).
|
|
587
|
+
--retry-limit <number> Retry failed workflows in the same process. Default: settings.json (${workflowDefaultRetryLimit()}).
|
|
588
|
+
--progress <jsonl|plain> Progress format on stderr. Default: settings.json (${workflowDefaultProgressMode()}).
|
|
589
|
+
--execution <background|attached> Execution mode. Default: settings.json (${workflowDefaultExecutionMode()}).
|
|
590
|
+
--command <path> Override Codex CLI binary path.
|
|
591
|
+
--model <model> Pass a model to Codex app-server.
|
|
592
|
+
--timeout-ms <number> Runtime timeout. Default: settings.json (${workflowDefaultTimeoutMs()}).
|
|
593
|
+
--cwd <dir> Working directory for workflow execution. Default: current cwd.
|
|
594
|
+
--reasoning-effort <effort> Codex reasoning effort. Default: settings.json (${codexDefaultReasoningEffort()}).
|
|
595
|
+
--verbosity <verbosity> Codex verbosity. Default: settings.json (${codexDefaultVerbosity()}).
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
function isMainModule() {
|
|
599
|
+
const entry = process.argv[1];
|
|
600
|
+
if (!entry)
|
|
601
|
+
return false;
|
|
602
|
+
try {
|
|
603
|
+
return import.meta.url === pathToFileURL(realpathSync(entry)).href;
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (isMainModule()) {
|
|
610
|
+
main(process.argv.slice(2)).then((code) => {
|
|
611
|
+
process.exitCode = code;
|
|
612
|
+
}).catch((err) => {
|
|
613
|
+
process.stderr.write(`${errorMessage(err)}\n`);
|
|
614
|
+
process.exitCode = 1;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const DIRECT_PROVIDER_ENV_PREFIXES = [
|
|
2
|
+
'ANTHROPIC',
|
|
3
|
+
'AZURE_OPENAI',
|
|
4
|
+
'COHERE',
|
|
5
|
+
'DEEPSEEK',
|
|
6
|
+
'GEMINI',
|
|
7
|
+
'GOOGLE',
|
|
8
|
+
'GROQ',
|
|
9
|
+
'MISTRAL',
|
|
10
|
+
'OPENAI',
|
|
11
|
+
'OPENROUTER',
|
|
12
|
+
'PERPLEXITY',
|
|
13
|
+
'TOGETHER',
|
|
14
|
+
'XAI',
|
|
15
|
+
];
|
|
16
|
+
const DIRECT_PROVIDER_ENV_SUFFIXES = [
|
|
17
|
+
'ACCESS_TOKEN',
|
|
18
|
+
'API_BASE',
|
|
19
|
+
'API_KEY',
|
|
20
|
+
'AUTH_TOKEN',
|
|
21
|
+
'BASE_URL',
|
|
22
|
+
'ENDPOINT',
|
|
23
|
+
'ORG_ID',
|
|
24
|
+
'ORGANIZATION',
|
|
25
|
+
'PROJECT',
|
|
26
|
+
];
|
|
27
|
+
export function codexChildProcessEnv(overrides = {}) {
|
|
28
|
+
const env = {};
|
|
29
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
30
|
+
if (value === undefined || isDirectProviderEnvName(key))
|
|
31
|
+
continue;
|
|
32
|
+
env[key] = value;
|
|
33
|
+
}
|
|
34
|
+
env.TERM = process.env.TERM && process.env.TERM !== 'dumb'
|
|
35
|
+
? process.env.TERM
|
|
36
|
+
: 'xterm-256color';
|
|
37
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
38
|
+
if (value !== undefined)
|
|
39
|
+
env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
return env;
|
|
42
|
+
}
|
|
43
|
+
export function isDirectProviderEnvName(name) {
|
|
44
|
+
return DIRECT_PROVIDER_ENV_PREFIXES.some((prefix) => (DIRECT_PROVIDER_ENV_SUFFIXES.some((suffix) => name === `${prefix}_${suffix}`)));
|
|
45
|
+
}
|