ultracode-for-codex 0.2.5 → 0.3.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 +54 -12
- package/ULTRACODE_INSTALL.md +58 -13
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +848 -11
- package/dist/codex/subagent-backend.d.ts +1 -0
- package/dist/codex/subagent-backend.js +22 -10
- package/dist/runtime/package-info.d.ts +1 -0
- package/dist/runtime/package-info.js +12 -0
- package/dist/runtime/workflow-journal.d.ts +0 -1
- package/dist/runtime/workflow-journal.js +1 -4
- package/dist/runtime/workflow-runtime.d.ts +19 -1
- package/dist/runtime/workflow-runtime.js +144 -80
- package/dist/settings.js +1 -6
- package/docs/provenance-audit.md +3 -2
- package/docs/ultracode-p3c-worktree-isolation.md +8 -8
- package/package.json +4 -1
- package/settings.json +1 -1
- package/skills/ultracode-for-codex/SKILL.md +32 -13
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
|
-
import { mkdir, open, readFile, writeFile } from 'node:fs/promises';
|
|
6
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { chmod, mkdir, open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { createInterface } from 'node:readline/promises';
|
|
9
9
|
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
|
+
import { ultracodePackageVersion } from './runtime/package-info.js';
|
|
12
13
|
import { renderUltracodeInstallGuideNotice } from './ultracode-install-guide.js';
|
|
13
14
|
import { codexDefaultReasoningEffort, codexDefaultVerbosity, isReasoningEffort, isVerbosity, isWorkflowExecutionMode, isWorkflowPermissionPolicy, isWorkflowProgressMode, workflowBackgroundDefaults, workflowDefaultExecutionMode, workflowDefaultPermissionPolicy, workflowDefaultProgressMode, workflowDefaultRetryLimit, workflowDefaultTimeoutMs, } from './settings.js';
|
|
14
15
|
const ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION = 'v1';
|
|
@@ -19,12 +20,30 @@ async function main(argv) {
|
|
|
19
20
|
process.stdout.write(helpText());
|
|
20
21
|
return 0;
|
|
21
22
|
}
|
|
23
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
24
|
+
process.stdout.write(`ultracode-for-codex ${ultracodePackageVersion()}\n`);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
22
27
|
if (command === '--llm-guide' || command === 'llm-guide') {
|
|
23
28
|
process.stdout.write(renderUltracodeInstallGuideNotice());
|
|
24
29
|
return 0;
|
|
25
30
|
}
|
|
26
31
|
if (command === 'run')
|
|
27
32
|
return runWorkflow(args);
|
|
33
|
+
if (command === 'status')
|
|
34
|
+
return showBackgroundStatus(args);
|
|
35
|
+
if (command === 'wait')
|
|
36
|
+
return waitForBackgroundJob(args);
|
|
37
|
+
if (command === 'logs')
|
|
38
|
+
return showBackgroundLogs(args);
|
|
39
|
+
if (command === 'result')
|
|
40
|
+
return showBackgroundResult(args);
|
|
41
|
+
if (command === 'cancel')
|
|
42
|
+
return cancelBackgroundJob(args);
|
|
43
|
+
if (command === 'jobs' || command === 'list')
|
|
44
|
+
return listBackgroundJobs(args);
|
|
45
|
+
if (command === 'archive' || command === 'export')
|
|
46
|
+
return archiveBackgroundJob(args);
|
|
28
47
|
process.stderr.write(`Unknown command: ${command}\n\n${helpText()}`);
|
|
29
48
|
return 1;
|
|
30
49
|
}
|
|
@@ -64,6 +83,7 @@ async function runWorkflow(args) {
|
|
|
64
83
|
const snapshot = await streamCommandWorkflow(runtime, launch, progressMode);
|
|
65
84
|
if (snapshot.status === 'completed') {
|
|
66
85
|
process.stdout.write(`${JSON.stringify(snapshot.result ?? null, null, 2)}\n`);
|
|
86
|
+
renderWorkflowCompletionGuidance(snapshot, progressMode);
|
|
67
87
|
return 0;
|
|
68
88
|
}
|
|
69
89
|
renderFailedSnapshot(snapshot, progressMode);
|
|
@@ -121,10 +141,11 @@ async function launchBackgroundWorkflow(args, cwd) {
|
|
|
121
141
|
await mkdir(runDir, { recursive: true });
|
|
122
142
|
const stdout = await open(resultPath, 'w');
|
|
123
143
|
const stderr = await open(progressPath, 'w');
|
|
144
|
+
const entryPath = cliEntryPath();
|
|
124
145
|
let childPid = 0;
|
|
125
146
|
try {
|
|
126
147
|
const child = spawn(process.execPath, [
|
|
127
|
-
|
|
148
|
+
entryPath,
|
|
128
149
|
'run',
|
|
129
150
|
...args,
|
|
130
151
|
'--execution',
|
|
@@ -155,6 +176,9 @@ async function launchBackgroundWorkflow(args, cwd) {
|
|
|
155
176
|
progressPath,
|
|
156
177
|
metadataPath,
|
|
157
178
|
pidPath,
|
|
179
|
+
nodePath: process.execPath,
|
|
180
|
+
cliEntryPath: entryPath,
|
|
181
|
+
commandLineHint: `${process.execPath} ${entryPath} run`,
|
|
158
182
|
}, null, 2)}\n`);
|
|
159
183
|
process.stdout.write(`${JSON.stringify({
|
|
160
184
|
kind: 'ultracode.workflow.background',
|
|
@@ -169,6 +193,624 @@ async function launchBackgroundWorkflow(args, cwd) {
|
|
|
169
193
|
}, null, 2)}\n`);
|
|
170
194
|
return 0;
|
|
171
195
|
}
|
|
196
|
+
async function showBackgroundStatus(args) {
|
|
197
|
+
const options = parseOptions(args);
|
|
198
|
+
const status = await inspectBackgroundJob(options);
|
|
199
|
+
if (wantsPlain(options)) {
|
|
200
|
+
process.stdout.write(renderBackgroundStatusPlain(status));
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
async function waitForBackgroundJob(args) {
|
|
207
|
+
const options = parseOptions(args);
|
|
208
|
+
const timeoutMs = parseNonNegativeIntOption(options.timeoutMs, 0, 'timeout-ms');
|
|
209
|
+
const intervalMs = parsePositiveIntOption(options.intervalMs, 1_000, 'interval-ms');
|
|
210
|
+
const waited = await waitForTerminalBackgroundJob(options, timeoutMs, intervalMs);
|
|
211
|
+
if (waited.timedOut) {
|
|
212
|
+
const payload = {
|
|
213
|
+
...waited.status,
|
|
214
|
+
waitTimedOut: true,
|
|
215
|
+
waitTimeoutMs: timeoutMs,
|
|
216
|
+
};
|
|
217
|
+
if (wantsPlain(options))
|
|
218
|
+
process.stdout.write(renderBackgroundStatusPlain(payload));
|
|
219
|
+
else
|
|
220
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
221
|
+
return 124;
|
|
222
|
+
}
|
|
223
|
+
if (wantsResult(options) && waited.status.status === 'completed') {
|
|
224
|
+
return await printBackgroundResult(await resolveBackgroundJobRef(options), waited.status);
|
|
225
|
+
}
|
|
226
|
+
if (wantsPlain(options))
|
|
227
|
+
process.stdout.write(renderBackgroundStatusPlain(waited.status));
|
|
228
|
+
else
|
|
229
|
+
process.stdout.write(`${JSON.stringify(waited.status, null, 2)}\n`);
|
|
230
|
+
return waited.status.status === 'completed' ? 0 : 1;
|
|
231
|
+
}
|
|
232
|
+
async function showBackgroundLogs(args) {
|
|
233
|
+
const options = parseOptions(args);
|
|
234
|
+
const ref = await resolveBackgroundJobRef(options);
|
|
235
|
+
const progress = await readBackgroundProgress(ref.progressPath);
|
|
236
|
+
if (!progress.exists) {
|
|
237
|
+
process.stderr.write(`Background progress file not found: ${ref.progressPath}\n`);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
const filtered = options.event
|
|
241
|
+
? progress.events.filter((event) => event.event === options.event)
|
|
242
|
+
: progress.events;
|
|
243
|
+
const tail = options.tail === undefined ? 0 : parseNonNegativeIntOption(options.tail, 0, 'tail');
|
|
244
|
+
const selected = tail > 0 ? filtered.slice(-tail) : filtered;
|
|
245
|
+
if (wantsPlain(options)) {
|
|
246
|
+
process.stdout.write(selected.map(renderProgressEventPlain).join(''));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
process.stdout.write(selected.map((event) => JSON.stringify(event)).join('\n'));
|
|
250
|
+
if (selected.length > 0)
|
|
251
|
+
process.stdout.write('\n');
|
|
252
|
+
}
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
async function showBackgroundResult(args) {
|
|
256
|
+
const options = parseOptions(args);
|
|
257
|
+
const ref = await resolveBackgroundJobRef(options);
|
|
258
|
+
const status = await inspectBackgroundJob(options);
|
|
259
|
+
return await printBackgroundResult(ref, status);
|
|
260
|
+
}
|
|
261
|
+
async function printBackgroundResult(ref, status) {
|
|
262
|
+
const text = await readTextFileIfPresent(ref.resultPath);
|
|
263
|
+
if (text !== null && text.trim()) {
|
|
264
|
+
process.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
process.stderr.write(`Background result is not ready: ${status.status}${status.reason ? ` (${status.reason})` : ''}\n`);
|
|
268
|
+
return status.status === 'failed' ? 1 : 2;
|
|
269
|
+
}
|
|
270
|
+
async function cancelBackgroundJob(args) {
|
|
271
|
+
const options = parseOptions(args);
|
|
272
|
+
const ref = await resolveBackgroundJobRef(options);
|
|
273
|
+
const metadata = await readBackgroundMetadata(ref.metadataPath);
|
|
274
|
+
const pid = metadata.pid || await readPid(ref.pidPath);
|
|
275
|
+
if (!pid || pid <= 0) {
|
|
276
|
+
process.stderr.write(`Background job pid not found: ${ref.pidPath}\n`);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
const signal = parseSignalOption(options.signal);
|
|
280
|
+
const alive = isProcessAlive(pid);
|
|
281
|
+
const commandLine = alive ? backgroundProcessCommandLine(pid) : undefined;
|
|
282
|
+
const identityVerified = !alive || backgroundProcessIdentityMatches(metadata, commandLine);
|
|
283
|
+
const cancelResult = {
|
|
284
|
+
kind: 'ultracode.workflow.background.cancel',
|
|
285
|
+
version: 1,
|
|
286
|
+
status: alive
|
|
287
|
+
? identityVerified ? 'signalled' : 'identity_mismatch'
|
|
288
|
+
: 'not_running',
|
|
289
|
+
jobId: metadata.jobId,
|
|
290
|
+
pid,
|
|
291
|
+
signal,
|
|
292
|
+
identityVerified,
|
|
293
|
+
...(commandLine ? { processCommandLine: commandLine } : {}),
|
|
294
|
+
metadataPath: ref.metadataPath,
|
|
295
|
+
resultPath: ref.resultPath,
|
|
296
|
+
progressPath: ref.progressPath,
|
|
297
|
+
pidPath: ref.pidPath,
|
|
298
|
+
};
|
|
299
|
+
if (alive && !identityVerified) {
|
|
300
|
+
if (wantsPlain(options))
|
|
301
|
+
process.stdout.write(renderBackgroundCancelPlain(cancelResult));
|
|
302
|
+
else
|
|
303
|
+
process.stdout.write(`${JSON.stringify(cancelResult, null, 2)}\n`);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
if (alive) {
|
|
307
|
+
process.kill(pid, signal);
|
|
308
|
+
}
|
|
309
|
+
if (wantsWait(options)) {
|
|
310
|
+
const timeoutMs = parseNonNegativeIntOption(options.timeoutMs, 0, 'timeout-ms');
|
|
311
|
+
const intervalMs = parsePositiveIntOption(options.intervalMs, 1_000, 'interval-ms');
|
|
312
|
+
const waited = await waitForTerminalBackgroundJob(options, timeoutMs, intervalMs);
|
|
313
|
+
const payload = {
|
|
314
|
+
kind: 'ultracode.workflow.background.cancel.wait',
|
|
315
|
+
version: 1,
|
|
316
|
+
cancel: cancelResult,
|
|
317
|
+
terminalStatus: waited.status,
|
|
318
|
+
waitTimedOut: waited.timedOut,
|
|
319
|
+
...(waited.timedOut ? { waitTimeoutMs: timeoutMs } : {}),
|
|
320
|
+
};
|
|
321
|
+
if (wantsPlain(options)) {
|
|
322
|
+
process.stdout.write(`${renderBackgroundCancelPlain(cancelResult)}${renderBackgroundStatusPlain(waited.status)}`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
326
|
+
}
|
|
327
|
+
return waited.timedOut ? 124 : 0;
|
|
328
|
+
}
|
|
329
|
+
if (wantsPlain(options))
|
|
330
|
+
process.stdout.write(renderBackgroundCancelPlain(cancelResult));
|
|
331
|
+
else
|
|
332
|
+
process.stdout.write(`${JSON.stringify(cancelResult, null, 2)}\n`);
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
async function listBackgroundJobs(args) {
|
|
336
|
+
const options = parseOptions(args);
|
|
337
|
+
const list = await backgroundJobsList(options);
|
|
338
|
+
if (wantsPlain(options)) {
|
|
339
|
+
process.stdout.write(renderBackgroundJobsPlain(list));
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
process.stdout.write(`${JSON.stringify(list, null, 2)}\n`);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
async function archiveBackgroundJob(args) {
|
|
346
|
+
const options = parseOptions(args);
|
|
347
|
+
const ref = await resolveBackgroundJobRef(options);
|
|
348
|
+
const metadata = await readBackgroundMetadata(ref.metadataPath);
|
|
349
|
+
const status = await inspectBackgroundJob(options);
|
|
350
|
+
const progress = await readBackgroundProgress(ref.progressPath);
|
|
351
|
+
const resultText = await readTextFileIfPresent(ref.resultPath);
|
|
352
|
+
const archivePath = await backgroundArchivePath(options, metadata.jobId);
|
|
353
|
+
const record = {
|
|
354
|
+
kind: 'ultracode.workflow.background.archive',
|
|
355
|
+
version: 1,
|
|
356
|
+
archivedAt: new Date().toISOString(),
|
|
357
|
+
archivePath,
|
|
358
|
+
status,
|
|
359
|
+
metadata,
|
|
360
|
+
progressEvents: progress.events,
|
|
361
|
+
malformedProgressLineCount: progress.malformedLineCount,
|
|
362
|
+
resultText,
|
|
363
|
+
};
|
|
364
|
+
await mkdir(dirname(archivePath), { recursive: true });
|
|
365
|
+
await writeFile(archivePath, `${JSON.stringify(record, null, 2)}\n`);
|
|
366
|
+
await chmod(archivePath, 0o600).catch(() => undefined);
|
|
367
|
+
const projection = {
|
|
368
|
+
kind: 'ultracode.workflow.background.archive.created',
|
|
369
|
+
version: 1,
|
|
370
|
+
jobId: metadata.jobId,
|
|
371
|
+
archivePath,
|
|
372
|
+
status: status.status,
|
|
373
|
+
progressEventCount: progress.events.length,
|
|
374
|
+
};
|
|
375
|
+
if (wantsPlain(options)) {
|
|
376
|
+
process.stdout.write(`[archive] ${metadata.jobId} ${status.status} -> ${archivePath}\n`);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
process.stdout.write(`${JSON.stringify(projection, null, 2)}\n`);
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
async function inspectBackgroundJob(options) {
|
|
384
|
+
const ref = await resolveBackgroundJobRef(options);
|
|
385
|
+
const metadata = await readBackgroundMetadata(ref.metadataPath);
|
|
386
|
+
const pid = metadata.pid || await readPid(ref.pidPath);
|
|
387
|
+
const alive = pid ? isProcessAlive(pid) : false;
|
|
388
|
+
const progress = await readBackgroundProgress(ref.progressPath);
|
|
389
|
+
const resultText = await readTextFileIfPresent(ref.resultPath);
|
|
390
|
+
const resultReady = Boolean(resultText?.trim());
|
|
391
|
+
const statusEvents = progress.events.filter((event) => !isPostCompletionGuidanceEvent(event));
|
|
392
|
+
const lastEvent = statusEvents.at(-1);
|
|
393
|
+
const terminalEvent = [...statusEvents].reverse().find((event) => (event.event === 'workflow.completed'
|
|
394
|
+
|| event.event === 'workflow.failed'
|
|
395
|
+
|| event.event === 'workflow.terminal_failure'));
|
|
396
|
+
const status = backgroundStatusFrom({ terminalEvent, resultReady, alive });
|
|
397
|
+
return {
|
|
398
|
+
kind: 'ultracode.workflow.background.status',
|
|
399
|
+
version: 1,
|
|
400
|
+
status,
|
|
401
|
+
jobId: metadata.jobId ?? ref.jobId,
|
|
402
|
+
pid,
|
|
403
|
+
alive,
|
|
404
|
+
launchedAt: metadata.launchedAt,
|
|
405
|
+
cwd: metadata.cwd ?? ref.cwd,
|
|
406
|
+
resultPath: ref.resultPath,
|
|
407
|
+
progressPath: ref.progressPath,
|
|
408
|
+
metadataPath: ref.metadataPath,
|
|
409
|
+
pidPath: ref.pidPath,
|
|
410
|
+
resultReady,
|
|
411
|
+
progressEventCount: progress.events.length,
|
|
412
|
+
malformedProgressLineCount: progress.malformedLineCount,
|
|
413
|
+
lastEvent: lastEvent?.event,
|
|
414
|
+
lastStatus: lastEvent?.status,
|
|
415
|
+
lastSummary: lastEvent?.summary,
|
|
416
|
+
reason: terminalEvent?.reason,
|
|
417
|
+
error: terminalEvent?.error,
|
|
418
|
+
completedAgentCount: lastNumericField(progress.events, 'completedAgentCount'),
|
|
419
|
+
knownAgentCount: lastNumericField(progress.events, 'knownAgentCount'),
|
|
420
|
+
phase: lastStringField(progress.events, 'phase'),
|
|
421
|
+
phaseCompletedAgentCount: lastNumericField(progress.events, 'phaseCompletedAgentCount'),
|
|
422
|
+
phaseKnownAgentCount: lastNumericField(progress.events, 'phaseKnownAgentCount'),
|
|
423
|
+
elapsedMs: lastNumericField(progress.events, 'elapsedMs'),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async function waitForTerminalBackgroundJob(options, timeoutMs, intervalMs) {
|
|
427
|
+
const startedAt = Date.now();
|
|
428
|
+
while (true) {
|
|
429
|
+
const status = await inspectBackgroundJob(options);
|
|
430
|
+
if (isTerminalBackgroundStatus(status.status))
|
|
431
|
+
return { status, timedOut: false };
|
|
432
|
+
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs)
|
|
433
|
+
return { status, timedOut: true };
|
|
434
|
+
await delay(intervalMs);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function backgroundJobsList(options) {
|
|
438
|
+
const cwd = options.cwd ?? process.cwd();
|
|
439
|
+
const settings = workflowBackgroundDefaults();
|
|
440
|
+
const backgroundRoot = resolveBackgroundJobsRoot(cwd, settings.runDir);
|
|
441
|
+
const jobs = [];
|
|
442
|
+
const invalidJobs = [];
|
|
443
|
+
let entries = [];
|
|
444
|
+
try {
|
|
445
|
+
entries = await readdir(backgroundRoot);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
if (!isNodeErrorCode(err, 'ENOENT'))
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
for (const entry of entries) {
|
|
452
|
+
const candidateDir = join(backgroundRoot, entry);
|
|
453
|
+
const candidateMetadataPath = join(candidateDir, settings.metadataFile);
|
|
454
|
+
const candidateStat = await stat(candidateMetadataPath).catch((err) => {
|
|
455
|
+
if (isNodeErrorCode(err, 'ENOENT') || isNodeErrorCode(err, 'ENOTDIR'))
|
|
456
|
+
return null;
|
|
457
|
+
throw err;
|
|
458
|
+
});
|
|
459
|
+
if (!candidateStat?.isFile())
|
|
460
|
+
continue;
|
|
461
|
+
try {
|
|
462
|
+
const metadata = await readBackgroundMetadata(candidateMetadataPath);
|
|
463
|
+
jobs.push(await inspectBackgroundJob({
|
|
464
|
+
...options,
|
|
465
|
+
_: [],
|
|
466
|
+
metadataPath: metadata.metadataPath || candidateMetadataPath,
|
|
467
|
+
cwd,
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
invalidJobs.push({ path: candidateMetadataPath, error: errorMessage(err) });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
jobs.sort((a, b) => String(b.launchedAt ?? '').localeCompare(String(a.launchedAt ?? '')));
|
|
475
|
+
return {
|
|
476
|
+
kind: 'ultracode.workflow.background.jobs',
|
|
477
|
+
version: 1,
|
|
478
|
+
cwd,
|
|
479
|
+
backgroundRoot,
|
|
480
|
+
count: jobs.length,
|
|
481
|
+
jobs,
|
|
482
|
+
invalidJobs,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async function resolveBackgroundJobRef(options) {
|
|
486
|
+
if (options._.length > 1) {
|
|
487
|
+
throw new Error('Background commands accept at most one positional job id or metadata path.');
|
|
488
|
+
}
|
|
489
|
+
const cwd = options.cwd ?? process.cwd();
|
|
490
|
+
const positional = options._[0];
|
|
491
|
+
const positionalLooksLikePath = positional
|
|
492
|
+
? positional.includes('/') || positional.includes('\\') || positional.endsWith('.json')
|
|
493
|
+
: false;
|
|
494
|
+
const jobId = options.jobId ?? (positional && !positionalLooksLikePath ? positional : undefined);
|
|
495
|
+
const metadataPathOption = options.metadataPath ?? (positional && positionalLooksLikePath ? positional : undefined);
|
|
496
|
+
let ref;
|
|
497
|
+
if (metadataPathOption) {
|
|
498
|
+
const metadataPath = resolve(metadataPathOption);
|
|
499
|
+
const metadata = await readBackgroundMetadata(metadataPath);
|
|
500
|
+
ref = {
|
|
501
|
+
jobId: metadata.jobId,
|
|
502
|
+
cwd: metadata.cwd,
|
|
503
|
+
metadataPath,
|
|
504
|
+
resultPath: options.resultPath ? resolve(options.resultPath) : requireMetadataPath(metadata.resultPath, 'resultPath'),
|
|
505
|
+
progressPath: options.progressPath ? resolve(options.progressPath) : requireMetadataPath(metadata.progressPath, 'progressPath'),
|
|
506
|
+
pidPath: options.pidPath ? resolve(options.pidPath) : requireMetadataPath(metadata.pidPath, 'pidPath'),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
else if (jobId) {
|
|
510
|
+
const settings = workflowBackgroundDefaults();
|
|
511
|
+
const runDir = resolveBackgroundRunDir(cwd, settings.runDir, jobId);
|
|
512
|
+
ref = {
|
|
513
|
+
jobId,
|
|
514
|
+
cwd,
|
|
515
|
+
metadataPath: options.metadataPath ? resolve(options.metadataPath) : join(runDir, settings.metadataFile),
|
|
516
|
+
resultPath: options.resultPath ? resolve(options.resultPath) : join(runDir, settings.resultFile),
|
|
517
|
+
progressPath: options.progressPath ? resolve(options.progressPath) : join(runDir, settings.progressFile),
|
|
518
|
+
pidPath: options.pidPath ? resolve(options.pidPath) : join(runDir, settings.pidFile),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
throw new Error('Background command requires a job id, metadata path, or --job-id.');
|
|
523
|
+
}
|
|
524
|
+
assertDistinctBackgroundPaths([ref.resultPath, ref.progressPath, ref.metadataPath, ref.pidPath]);
|
|
525
|
+
return ref;
|
|
526
|
+
}
|
|
527
|
+
async function readBackgroundMetadata(metadataPath) {
|
|
528
|
+
const text = await readTextFileIfPresent(metadataPath);
|
|
529
|
+
if (text === null)
|
|
530
|
+
throw new Error(`Background metadata file not found: ${metadataPath}`);
|
|
531
|
+
let parsed;
|
|
532
|
+
try {
|
|
533
|
+
parsed = JSON.parse(text);
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
throw new Error(`Background metadata file is not valid JSON: ${errorMessage(err)}`);
|
|
537
|
+
}
|
|
538
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
539
|
+
throw new Error('Background metadata must contain a JSON object.');
|
|
540
|
+
}
|
|
541
|
+
return validateBackgroundMetadata(parsed, metadataPath);
|
|
542
|
+
}
|
|
543
|
+
async function readPid(pidPath) {
|
|
544
|
+
const text = await readTextFileIfPresent(pidPath);
|
|
545
|
+
if (text === null)
|
|
546
|
+
return undefined;
|
|
547
|
+
const pid = Number.parseInt(text.trim(), 10);
|
|
548
|
+
return Number.isFinite(pid) ? pid : undefined;
|
|
549
|
+
}
|
|
550
|
+
function validateBackgroundMetadata(value, metadataPath) {
|
|
551
|
+
const kind = requiredString(value.kind, 'kind');
|
|
552
|
+
if (kind !== 'ultracode.workflow.background') {
|
|
553
|
+
throw new Error(`Background metadata kind must be ultracode.workflow.background: ${metadataPath}`);
|
|
554
|
+
}
|
|
555
|
+
const version = requiredInteger(value.version, 'version');
|
|
556
|
+
if (version !== 1)
|
|
557
|
+
throw new Error(`Background metadata version must be 1: ${metadataPath}`);
|
|
558
|
+
const status = requiredString(value.status, 'status');
|
|
559
|
+
if (status !== 'launched')
|
|
560
|
+
throw new Error(`Background metadata status must be launched: ${metadataPath}`);
|
|
561
|
+
const pid = requiredInteger(value.pid, 'pid');
|
|
562
|
+
if (pid < 0)
|
|
563
|
+
throw new Error(`Background metadata pid must be non-negative: ${metadataPath}`);
|
|
564
|
+
const metadata = {
|
|
565
|
+
kind: 'ultracode.workflow.background',
|
|
566
|
+
version: 1,
|
|
567
|
+
status: 'launched',
|
|
568
|
+
jobId: requiredString(value.jobId, 'jobId'),
|
|
569
|
+
pid,
|
|
570
|
+
launchedAt: requiredString(value.launchedAt, 'launchedAt'),
|
|
571
|
+
cwd: requiredString(value.cwd, 'cwd'),
|
|
572
|
+
resultPath: requiredString(value.resultPath, 'resultPath'),
|
|
573
|
+
progressPath: requiredString(value.progressPath, 'progressPath'),
|
|
574
|
+
metadataPath: requiredString(value.metadataPath, 'metadataPath'),
|
|
575
|
+
pidPath: requiredString(value.pidPath, 'pidPath'),
|
|
576
|
+
...(typeof value.nodePath === 'string' && value.nodePath ? { nodePath: value.nodePath } : {}),
|
|
577
|
+
...(typeof value.cliEntryPath === 'string' && value.cliEntryPath ? { cliEntryPath: value.cliEntryPath } : {}),
|
|
578
|
+
...(typeof value.commandLineHint === 'string' && value.commandLineHint ? { commandLineHint: value.commandLineHint } : {}),
|
|
579
|
+
};
|
|
580
|
+
for (const [key, path] of Object.entries({
|
|
581
|
+
cwd: metadata.cwd,
|
|
582
|
+
resultPath: metadata.resultPath,
|
|
583
|
+
progressPath: metadata.progressPath,
|
|
584
|
+
metadataPath: metadata.metadataPath,
|
|
585
|
+
pidPath: metadata.pidPath,
|
|
586
|
+
})) {
|
|
587
|
+
if (!isAbsolute(path))
|
|
588
|
+
throw new Error(`Background metadata ${key} must be an absolute path: ${metadataPath}`);
|
|
589
|
+
}
|
|
590
|
+
return metadata;
|
|
591
|
+
}
|
|
592
|
+
function requiredString(value, key) {
|
|
593
|
+
if (typeof value === 'string' && value.trim())
|
|
594
|
+
return value;
|
|
595
|
+
throw new Error(`Background metadata ${key} must be a non-empty string.`);
|
|
596
|
+
}
|
|
597
|
+
function requiredInteger(value, key) {
|
|
598
|
+
if (typeof value === 'number' && Number.isInteger(value))
|
|
599
|
+
return value;
|
|
600
|
+
throw new Error(`Background metadata ${key} must be an integer.`);
|
|
601
|
+
}
|
|
602
|
+
function backgroundProcessCommandLine(pid) {
|
|
603
|
+
try {
|
|
604
|
+
return execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
605
|
+
encoding: 'utf8',
|
|
606
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
607
|
+
}).trim() || undefined;
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function backgroundProcessIdentityMatches(metadata, commandLine) {
|
|
614
|
+
if (!metadata.cliEntryPath || !commandLine)
|
|
615
|
+
return true;
|
|
616
|
+
return commandLine.includes(metadata.cliEntryPath)
|
|
617
|
+
&& (!metadata.nodePath || commandLine.includes(metadata.nodePath) || commandLine.includes('node'));
|
|
618
|
+
}
|
|
619
|
+
async function backgroundArchivePath(options, jobId) {
|
|
620
|
+
if (options.outputPath)
|
|
621
|
+
return resolve(options.outputPath);
|
|
622
|
+
const cwd = options.cwd ?? process.cwd();
|
|
623
|
+
const archiveDir = options.outDir
|
|
624
|
+
? resolve(options.outDir)
|
|
625
|
+
: resolve(cwd, '.ultracode-for-codex', 'archive');
|
|
626
|
+
return join(archiveDir, `${jobId}.json`);
|
|
627
|
+
}
|
|
628
|
+
async function readBackgroundProgress(progressPath) {
|
|
629
|
+
const text = await readTextFileIfPresent(progressPath);
|
|
630
|
+
if (text === null)
|
|
631
|
+
return { exists: false, events: [], malformedLineCount: 0 };
|
|
632
|
+
if (!text.trim())
|
|
633
|
+
return { exists: true, events: [], malformedLineCount: 0 };
|
|
634
|
+
const lines = text.split(/\r?\n/);
|
|
635
|
+
if (lines.at(-1) === '')
|
|
636
|
+
lines.pop();
|
|
637
|
+
const events = [];
|
|
638
|
+
let malformedLineCount = 0;
|
|
639
|
+
for (const [index, line] of lines.entries()) {
|
|
640
|
+
if (!line.trim())
|
|
641
|
+
continue;
|
|
642
|
+
try {
|
|
643
|
+
const parsed = JSON.parse(line);
|
|
644
|
+
if (parsed
|
|
645
|
+
&& typeof parsed === 'object'
|
|
646
|
+
&& !Array.isArray(parsed)
|
|
647
|
+
&& parsed.kind === PROGRESS_KIND
|
|
648
|
+
&& parsed.version === 1
|
|
649
|
+
&& typeof parsed.event === 'string') {
|
|
650
|
+
events.push(parsed);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
malformedLineCount += 1;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
if (index !== lines.length - 1)
|
|
658
|
+
malformedLineCount += 1;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return { exists: true, events, malformedLineCount };
|
|
662
|
+
}
|
|
663
|
+
async function readTextFileIfPresent(path) {
|
|
664
|
+
try {
|
|
665
|
+
return await readFile(path, 'utf8');
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
if (isNodeErrorCode(err, 'ENOENT'))
|
|
669
|
+
return null;
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function requireMetadataPath(value, key) {
|
|
674
|
+
if (!value)
|
|
675
|
+
throw new Error(`Background metadata is missing ${key}.`);
|
|
676
|
+
return value;
|
|
677
|
+
}
|
|
678
|
+
function backgroundStatusFrom(input) {
|
|
679
|
+
if (input.terminalEvent?.event === 'workflow.completed')
|
|
680
|
+
return 'completed';
|
|
681
|
+
if (input.terminalEvent?.event === 'workflow.failed' || input.terminalEvent?.event === 'workflow.terminal_failure')
|
|
682
|
+
return 'failed';
|
|
683
|
+
if (input.resultReady)
|
|
684
|
+
return 'completed';
|
|
685
|
+
return input.alive ? 'running' : 'exited_unknown';
|
|
686
|
+
}
|
|
687
|
+
function isTerminalBackgroundStatus(status) {
|
|
688
|
+
return status === 'completed' || status === 'failed' || status === 'exited_unknown';
|
|
689
|
+
}
|
|
690
|
+
function resolveBackgroundJobsRoot(cwd, template) {
|
|
691
|
+
const marker = '__ultracode_job_marker__';
|
|
692
|
+
return dirname(resolveBackgroundRunDir(cwd, template, marker));
|
|
693
|
+
}
|
|
694
|
+
function wantsPlain(options) {
|
|
695
|
+
return options.plain === 'true' || options.format === 'plain' || options.progress === 'plain';
|
|
696
|
+
}
|
|
697
|
+
function wantsResult(options) {
|
|
698
|
+
return options.result === 'true';
|
|
699
|
+
}
|
|
700
|
+
function wantsWait(options) {
|
|
701
|
+
return options.wait === 'true';
|
|
702
|
+
}
|
|
703
|
+
function renderBackgroundStatusPlain(status) {
|
|
704
|
+
const parts = [
|
|
705
|
+
`[job] ${status.jobId ?? 'unknown'} ${status.status}`,
|
|
706
|
+
status.pid !== undefined ? `pid=${status.pid}` : '',
|
|
707
|
+
`alive=${status.alive}`,
|
|
708
|
+
status.resultReady ? 'result=ready' : 'result=pending',
|
|
709
|
+
status.waitTimedOut ? `wait=timeout(${status.waitTimeoutMs}ms)` : '',
|
|
710
|
+
].filter(Boolean);
|
|
711
|
+
const lines = [parts.join(' ')];
|
|
712
|
+
if (status.lastEvent || status.lastSummary) {
|
|
713
|
+
lines.push(`[job] last=${status.lastEvent ?? 'unknown'} ${status.lastSummary ?? ''}`.trimEnd());
|
|
714
|
+
}
|
|
715
|
+
if (status.completedAgentCount !== undefined && status.knownAgentCount !== undefined) {
|
|
716
|
+
lines.push(`[job] agents=${status.completedAgentCount}/${status.knownAgentCount}${status.phase ? ` phase=${status.phase}` : ''}`);
|
|
717
|
+
}
|
|
718
|
+
if (status.reason || status.error) {
|
|
719
|
+
lines.push(`[job] failure=${status.reason ?? 'unknown'} ${status.error ?? ''}`.trimEnd());
|
|
720
|
+
}
|
|
721
|
+
lines.push(`[job] resultPath=${status.resultPath}`);
|
|
722
|
+
lines.push(`[job] progressPath=${status.progressPath}`);
|
|
723
|
+
return `${lines.join('\n')}\n`;
|
|
724
|
+
}
|
|
725
|
+
function renderBackgroundJobsPlain(list) {
|
|
726
|
+
const lines = [`[jobs] ${list.count} jobs in ${list.backgroundRoot}`];
|
|
727
|
+
for (const job of list.jobs) {
|
|
728
|
+
lines.push(`[jobs] ${job.jobId ?? 'unknown'} ${job.status} pid=${job.pid ?? 'unknown'} alive=${job.alive} result=${job.resultReady ? 'ready' : 'pending'}`);
|
|
729
|
+
}
|
|
730
|
+
for (const invalid of list.invalidJobs) {
|
|
731
|
+
lines.push(`[jobs] invalid ${invalid.path}: ${invalid.error}`);
|
|
732
|
+
}
|
|
733
|
+
return `${lines.join('\n')}\n`;
|
|
734
|
+
}
|
|
735
|
+
function renderBackgroundCancelPlain(cancel) {
|
|
736
|
+
return `[cancel] ${cancel.jobId} ${cancel.status} pid=${cancel.pid} signal=${cancel.signal} identity=${cancel.identityVerified ? 'verified' : 'unverified'}\n`;
|
|
737
|
+
}
|
|
738
|
+
function renderProgressEventPlain(event) {
|
|
739
|
+
const label = [
|
|
740
|
+
`[${event.event}]`,
|
|
741
|
+
event.status ? `status=${event.status}` : '',
|
|
742
|
+
event.phase ? `phase=${event.phase}` : '',
|
|
743
|
+
event.label ? `agent=${event.label}` : '',
|
|
744
|
+
event.summary,
|
|
745
|
+
].filter(Boolean).join(' ');
|
|
746
|
+
return `${label}\n`;
|
|
747
|
+
}
|
|
748
|
+
function isPostCompletionGuidanceEvent(event) {
|
|
749
|
+
return event.event === 'workflow.summary.ready'
|
|
750
|
+
|| event.event === 'workflow.review.recommended';
|
|
751
|
+
}
|
|
752
|
+
function isProcessAlive(pid) {
|
|
753
|
+
try {
|
|
754
|
+
process.kill(pid, 0);
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
if (isNodeErrorCode(err, 'ESRCH'))
|
|
759
|
+
return false;
|
|
760
|
+
if (isNodeErrorCode(err, 'EPERM'))
|
|
761
|
+
return true;
|
|
762
|
+
throw err;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function parseSignalOption(value) {
|
|
766
|
+
if (value === undefined)
|
|
767
|
+
return 'SIGINT';
|
|
768
|
+
const normalized = value.startsWith('SIG') ? value : `SIG${value}`;
|
|
769
|
+
const allowed = new Set(['SIGINT', 'SIGTERM', 'SIGHUP']);
|
|
770
|
+
if (allowed.has(normalized))
|
|
771
|
+
return normalized;
|
|
772
|
+
throw new Error('signal must be one of SIGINT, SIGTERM, or SIGHUP.');
|
|
773
|
+
}
|
|
774
|
+
function parsePositiveIntOption(value, fallback, label) {
|
|
775
|
+
if (value === undefined)
|
|
776
|
+
return fallback;
|
|
777
|
+
const parsed = Number.parseInt(value, 10);
|
|
778
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
779
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
780
|
+
return parsed;
|
|
781
|
+
}
|
|
782
|
+
function parseNonNegativeIntOption(value, fallback, label) {
|
|
783
|
+
if (value === undefined)
|
|
784
|
+
return fallback;
|
|
785
|
+
const parsed = Number.parseInt(value, 10);
|
|
786
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
787
|
+
throw new Error(`${label} must be a non-negative integer.`);
|
|
788
|
+
return parsed;
|
|
789
|
+
}
|
|
790
|
+
function lastNumericField(events, key) {
|
|
791
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
792
|
+
const value = events[index]?.[key];
|
|
793
|
+
if (typeof value === 'number')
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
return undefined;
|
|
797
|
+
}
|
|
798
|
+
function lastStringField(events, key) {
|
|
799
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
800
|
+
const value = events[index]?.[key];
|
|
801
|
+
if (typeof value === 'string')
|
|
802
|
+
return value;
|
|
803
|
+
}
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
function isNodeErrorCode(err, code) {
|
|
807
|
+
return err instanceof Error && err.code === code;
|
|
808
|
+
}
|
|
809
|
+
function delay(ms) {
|
|
810
|
+
return new Promise((resolveDelay) => {
|
|
811
|
+
setTimeout(resolveDelay, ms);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
172
814
|
function resolveBackgroundRunDir(cwd, template, jobId) {
|
|
173
815
|
const expanded = template.replaceAll('{jobId}', jobId);
|
|
174
816
|
return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
|
|
@@ -347,8 +989,19 @@ function renderWorkflowEvent(event, progressMode) {
|
|
|
347
989
|
case 'workflow.started':
|
|
348
990
|
process.stderr.write(`[workflow] started ${event.workflowName} task=${event.taskId} run=${event.runId}\n`);
|
|
349
991
|
return;
|
|
992
|
+
case 'workflow.phase.planned':
|
|
993
|
+
process.stderr.write(`[phase-plan] ${event.title} (${event.plannedAgentCount} agents)${event.goal ? ` - ${event.goal}` : ''}\n`);
|
|
994
|
+
for (const agent of event.plannedAgents) {
|
|
995
|
+
process.stderr.write(`[phase-plan] - ${agent.title}${agent.label ? ` (${agent.label})` : ''}${agent.focus ? `: ${agent.focus}` : ''}\n`);
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
350
998
|
case 'workflow.phase.started':
|
|
351
|
-
process.stderr.write(`[phase] ${event.title}${event.detail ? ` - ${event.detail}` : ''}\n`);
|
|
999
|
+
process.stderr.write(`[phase] ${event.title}${event.plannedAgentCount ? ` (${event.plannedAgentCount} agents)` : ''}${event.detail ? ` - ${event.detail}` : ''}\n`);
|
|
1000
|
+
if (event.plannedAgents) {
|
|
1001
|
+
for (const agent of event.plannedAgents) {
|
|
1002
|
+
process.stderr.write(`[phase] - ${agent.title}${agent.label ? ` (${agent.label})` : ''}${agent.focus ? `: ${agent.focus}` : ''}\n`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
352
1005
|
return;
|
|
353
1006
|
case 'workflow.plan.ready':
|
|
354
1007
|
process.stderr.write(`[plan] mode=${event.mode} phases=${event.phases.length}${event.rationale ? ` - ${event.rationale}` : ''}\n`);
|
|
@@ -366,7 +1019,7 @@ function renderWorkflowEvent(event, progressMode) {
|
|
|
366
1019
|
process.stderr.write(`[agent:${event.agentIndex + 1}] started ${event.label}\n`);
|
|
367
1020
|
return;
|
|
368
1021
|
case 'workflow.agent.completed':
|
|
369
|
-
process.stderr.write(`[agent:${event.agentIndex + 1}] completed ${event.label} tokens=${event.tokens} preview=${formatPreview(event.resultPreview)}${event.cached ? ' cached=true' : ''}\n`);
|
|
1022
|
+
process.stderr.write(`[agent:${event.agentIndex + 1}] completed ${event.label} | ${agentCompletionProgressSummary(event)} | tokens=${event.tokens} preview=${formatPreview(event.resultPreview)}${event.cached ? ' cached=true' : ''}\n`);
|
|
370
1023
|
return;
|
|
371
1024
|
case 'workflow.agent.failed':
|
|
372
1025
|
process.stderr.write(`[agent:${event.agentIndex + 1}] failed ${event.label} ${event.error}\n`);
|
|
@@ -395,6 +1048,110 @@ function renderFailedSnapshot(snapshot, progressMode) {
|
|
|
395
1048
|
}
|
|
396
1049
|
process.stderr.write(`[workflow] terminal failure task=${snapshot.taskId} reason=${snapshot.failureReason ?? 'unknown'} error=${snapshot.error ?? 'unknown'}\n`);
|
|
397
1050
|
}
|
|
1051
|
+
function renderWorkflowCompletionGuidance(snapshot, progressMode) {
|
|
1052
|
+
const phasesSummary = workflowPhaseExecutionSummary(snapshot.events);
|
|
1053
|
+
const totalPlannedAgentCount = phasesSummary.reduce((sum, phase) => sum + phase.agentCount, 0);
|
|
1054
|
+
const reviewRecommendation = criticalReviewRecommendation();
|
|
1055
|
+
if (progressMode === 'jsonl') {
|
|
1056
|
+
writeJsonlProgress({
|
|
1057
|
+
event: 'workflow.summary.ready',
|
|
1058
|
+
status: 'completed',
|
|
1059
|
+
summary: `Workflow completed with ${phasesSummary.length} phase${phasesSummary.length === 1 ? '' : 's'} and ${totalPlannedAgentCount} planned phase agent${totalPlannedAgentCount === 1 ? '' : 's'}`,
|
|
1060
|
+
taskId: snapshot.taskId,
|
|
1061
|
+
runId: snapshot.runId,
|
|
1062
|
+
workflowName: snapshot.workflowName,
|
|
1063
|
+
phasesSummary,
|
|
1064
|
+
totalPhaseCount: phasesSummary.length,
|
|
1065
|
+
totalPlannedAgentCount,
|
|
1066
|
+
});
|
|
1067
|
+
writeJsonlProgress({
|
|
1068
|
+
event: 'workflow.review.recommended',
|
|
1069
|
+
status: 'review_recommended',
|
|
1070
|
+
summary: reviewRecommendation,
|
|
1071
|
+
taskId: snapshot.taskId,
|
|
1072
|
+
runId: snapshot.runId,
|
|
1073
|
+
workflowName: snapshot.workflowName,
|
|
1074
|
+
recommendation: reviewRecommendation,
|
|
1075
|
+
});
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (phasesSummary.length > 0) {
|
|
1079
|
+
process.stderr.write('[workflow-summary] phase/agent angles\n');
|
|
1080
|
+
for (const phase of phasesSummary) {
|
|
1081
|
+
process.stderr.write(`[workflow-summary] Phase ${phase.title}: ${phase.agentCount} agent${phase.agentCount === 1 ? '' : 's'}${phase.goal ? ` - ${phase.goal}` : ''}\n`);
|
|
1082
|
+
for (const agent of phase.agents) {
|
|
1083
|
+
process.stderr.write(`[workflow-summary] - ${agent.title}${agent.label ? ` (${agent.label})` : ''}${agent.angle ? `: ${agent.angle}` : ''}\n`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
process.stderr.write('[workflow-summary] no phase-level agent plan was recorded\n');
|
|
1089
|
+
}
|
|
1090
|
+
process.stderr.write(`[review-recommendation] ${reviewRecommendation}\n`);
|
|
1091
|
+
}
|
|
1092
|
+
function workflowPhaseExecutionSummary(events) {
|
|
1093
|
+
const phases = new Map();
|
|
1094
|
+
const phaseTitlesWithPlannedAgents = new Set();
|
|
1095
|
+
for (const event of events) {
|
|
1096
|
+
if (event.type !== 'workflow.phase.planned' && event.type !== 'workflow.phase.started')
|
|
1097
|
+
continue;
|
|
1098
|
+
const plannedAgents = event.plannedAgents ?? [];
|
|
1099
|
+
if (plannedAgents.length > 0)
|
|
1100
|
+
phaseTitlesWithPlannedAgents.add(event.title);
|
|
1101
|
+
const existing = phases.get(event.title);
|
|
1102
|
+
const agents = plannedAgents.length > 0
|
|
1103
|
+
? plannedAgents.map((agent) => ({
|
|
1104
|
+
title: agent.title,
|
|
1105
|
+
...(agent.label ? { label: agent.label } : {}),
|
|
1106
|
+
...(agent.focus ? { angle: agent.focus } : {}),
|
|
1107
|
+
}))
|
|
1108
|
+
: existing?.agents ?? [];
|
|
1109
|
+
phases.set(event.title, {
|
|
1110
|
+
title: event.title,
|
|
1111
|
+
...(event.goal ?? existing?.goal ? { goal: event.goal ?? existing?.goal } : {}),
|
|
1112
|
+
agentCount: agents.length || existing?.agentCount || 0,
|
|
1113
|
+
agents,
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
for (const event of events) {
|
|
1117
|
+
if (event.type !== 'workflow.agent.started' || !event.phase)
|
|
1118
|
+
continue;
|
|
1119
|
+
const existing = phases.get(event.phase);
|
|
1120
|
+
if (!existing) {
|
|
1121
|
+
phases.set(event.phase, {
|
|
1122
|
+
title: event.phase,
|
|
1123
|
+
agentCount: 1,
|
|
1124
|
+
agents: [{
|
|
1125
|
+
title: event.label,
|
|
1126
|
+
label: event.label,
|
|
1127
|
+
angle: event.promptPreview,
|
|
1128
|
+
}],
|
|
1129
|
+
});
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if (phaseTitlesWithPlannedAgents.has(event.phase))
|
|
1133
|
+
continue;
|
|
1134
|
+
if (existing.agents.some((agent) => agent.label === event.label || agent.title === event.label))
|
|
1135
|
+
continue;
|
|
1136
|
+
const agents = [
|
|
1137
|
+
...existing.agents,
|
|
1138
|
+
{
|
|
1139
|
+
title: event.label,
|
|
1140
|
+
label: event.label,
|
|
1141
|
+
angle: event.promptPreview,
|
|
1142
|
+
},
|
|
1143
|
+
];
|
|
1144
|
+
phases.set(event.phase, {
|
|
1145
|
+
...existing,
|
|
1146
|
+
agentCount: agents.length,
|
|
1147
|
+
agents,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
return [...phases.values()];
|
|
1151
|
+
}
|
|
1152
|
+
function criticalReviewRecommendation() {
|
|
1153
|
+
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
|
+
}
|
|
398
1155
|
function renderControlProgress(event, progressMode, payload, plainText) {
|
|
399
1156
|
if (progressMode === 'jsonl') {
|
|
400
1157
|
writeJsonlProgress({ event, ...payload });
|
|
@@ -409,6 +1166,38 @@ function writeJsonlProgress(payload) {
|
|
|
409
1166
|
...payload,
|
|
410
1167
|
})}\n`);
|
|
411
1168
|
}
|
|
1169
|
+
function phaseStartedSummary(event) {
|
|
1170
|
+
const agentText = event.plannedAgentCount
|
|
1171
|
+
? `${event.plannedAgentCount} planned agent${event.plannedAgentCount === 1 ? '' : 's'}`
|
|
1172
|
+
: '';
|
|
1173
|
+
const suffix = [agentText, event.detail ?? event.goal].filter(Boolean).join(': ');
|
|
1174
|
+
return suffix ? `Phase ${event.title}: ${suffix}` : `Phase ${event.title}`;
|
|
1175
|
+
}
|
|
1176
|
+
function phasePlannedSummary(event) {
|
|
1177
|
+
return `Phase ${event.title} planned: ${event.plannedAgentCount} planned agent${event.plannedAgentCount === 1 ? '' : 's'}`;
|
|
1178
|
+
}
|
|
1179
|
+
function agentCompletionProgressSummary(event) {
|
|
1180
|
+
const parts = [];
|
|
1181
|
+
if (event.phase
|
|
1182
|
+
&& event.phaseCompletedAgentCount !== undefined
|
|
1183
|
+
&& event.phaseKnownAgentCount !== undefined) {
|
|
1184
|
+
parts.push(`Phase ${event.phase} (${event.phaseCompletedAgentCount}/${event.phaseKnownAgentCount})`);
|
|
1185
|
+
}
|
|
1186
|
+
parts.push(`${event.completedAgentCount} out of ${event.knownAgentCount} agents have completed the task`);
|
|
1187
|
+
parts.push(`${formatElapsedDuration(event.elapsedMs)} elapsed`);
|
|
1188
|
+
return parts.join(', ');
|
|
1189
|
+
}
|
|
1190
|
+
function formatElapsedDuration(ms) {
|
|
1191
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
1192
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1193
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1194
|
+
const seconds = totalSeconds % 60;
|
|
1195
|
+
if (hours > 0)
|
|
1196
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
1197
|
+
if (minutes > 0)
|
|
1198
|
+
return `${minutes}m ${seconds}s`;
|
|
1199
|
+
return `${seconds}s`;
|
|
1200
|
+
}
|
|
412
1201
|
function progressPayloadForEvent(event) {
|
|
413
1202
|
switch (event.type) {
|
|
414
1203
|
case 'workflow.started':
|
|
@@ -423,22 +1212,38 @@ function progressPayloadForEvent(event) {
|
|
|
423
1212
|
workflowSourcePath: event.workflowSourcePath,
|
|
424
1213
|
scriptHash: event.scriptHash,
|
|
425
1214
|
};
|
|
1215
|
+
case 'workflow.phase.planned':
|
|
1216
|
+
return {
|
|
1217
|
+
event: event.type,
|
|
1218
|
+
status: 'planned',
|
|
1219
|
+
summary: phasePlannedSummary(event),
|
|
1220
|
+
taskId: event.taskId,
|
|
1221
|
+
runId: event.runId,
|
|
1222
|
+
phaseIndex: event.phaseIndex,
|
|
1223
|
+
title: event.title,
|
|
1224
|
+
goal: event.goal,
|
|
1225
|
+
plannedAgentCount: event.plannedAgentCount,
|
|
1226
|
+
plannedAgents: event.plannedAgents,
|
|
1227
|
+
};
|
|
426
1228
|
case 'workflow.phase.started':
|
|
427
1229
|
return {
|
|
428
1230
|
event: event.type,
|
|
429
1231
|
status: 'running',
|
|
430
|
-
summary: event
|
|
1232
|
+
summary: phaseStartedSummary(event),
|
|
431
1233
|
taskId: event.taskId,
|
|
432
1234
|
runId: event.runId,
|
|
433
1235
|
phaseIndex: event.phaseIndex,
|
|
434
1236
|
title: event.title,
|
|
435
1237
|
detail: event.detail,
|
|
1238
|
+
goal: event.goal,
|
|
1239
|
+
plannedAgentCount: event.plannedAgentCount,
|
|
1240
|
+
plannedAgents: event.plannedAgents,
|
|
436
1241
|
};
|
|
437
1242
|
case 'workflow.plan.ready':
|
|
438
1243
|
return {
|
|
439
1244
|
event: event.type,
|
|
440
1245
|
status: 'planned',
|
|
441
|
-
summary: `Workflow
|
|
1246
|
+
summary: `Workflow planning snapshot: ${event.phases.length} known phase${event.phases.length === 1 ? '' : 's'}, mode=${event.mode}`,
|
|
442
1247
|
taskId: event.taskId,
|
|
443
1248
|
runId: event.runId,
|
|
444
1249
|
mode: event.mode,
|
|
@@ -472,7 +1277,7 @@ function progressPayloadForEvent(event) {
|
|
|
472
1277
|
return {
|
|
473
1278
|
event: event.type,
|
|
474
1279
|
status: 'completed',
|
|
475
|
-
summary: `Agent ${event.agentIndex + 1} completed`,
|
|
1280
|
+
summary: `Agent ${event.agentIndex + 1} completed: ${event.label}. ${agentCompletionProgressSummary(event)}`,
|
|
476
1281
|
taskId: event.taskId,
|
|
477
1282
|
runId: event.runId,
|
|
478
1283
|
agentIndex: event.agentIndex,
|
|
@@ -483,6 +1288,11 @@ function progressPayloadForEvent(event) {
|
|
|
483
1288
|
toolCalls: event.toolCalls,
|
|
484
1289
|
resultPreview: event.resultPreview,
|
|
485
1290
|
cached: event.cached,
|
|
1291
|
+
elapsedMs: event.elapsedMs,
|
|
1292
|
+
completedAgentCount: event.completedAgentCount,
|
|
1293
|
+
knownAgentCount: event.knownAgentCount,
|
|
1294
|
+
phaseCompletedAgentCount: event.phaseCompletedAgentCount,
|
|
1295
|
+
phaseKnownAgentCount: event.phaseKnownAgentCount,
|
|
486
1296
|
worktreePreserved: event.worktreePreserved,
|
|
487
1297
|
preservedWorktrees: event.preservedWorktrees,
|
|
488
1298
|
};
|
|
@@ -537,7 +1347,7 @@ function parseIntOption(value, fallback) {
|
|
|
537
1347
|
if (value === undefined)
|
|
538
1348
|
return fallback;
|
|
539
1349
|
const parsed = Number.parseInt(value, 10);
|
|
540
|
-
if (!Number.isFinite(parsed) || parsed
|
|
1350
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
541
1351
|
return fallback;
|
|
542
1352
|
return parsed;
|
|
543
1353
|
}
|
|
@@ -594,8 +1404,18 @@ function helpText() {
|
|
|
594
1404
|
|
|
595
1405
|
Commands:
|
|
596
1406
|
run Run a workflow as a local CLI command.
|
|
1407
|
+
status Show a background workflow status record.
|
|
1408
|
+
wait Wait for a background workflow to reach a terminal state.
|
|
1409
|
+
logs Print a background workflow progress JSONL file.
|
|
1410
|
+
result Print a completed background workflow result JSON.
|
|
1411
|
+
cancel Send SIGINT to a background workflow command.
|
|
1412
|
+
jobs List background workflow jobs.
|
|
1413
|
+
list Alias for jobs.
|
|
1414
|
+
archive Export one background workflow job state to an archive JSON file.
|
|
1415
|
+
export Alias for archive.
|
|
597
1416
|
|
|
598
1417
|
Options:
|
|
1418
|
+
--version, -v Print the package version.
|
|
599
1419
|
--llm-guide Print the Ultracode install and usage guide.
|
|
600
1420
|
--accept-llm-guide <version> Required for run. Current version: ${ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION}.
|
|
601
1421
|
--script <js> Inline workflow script.
|
|
@@ -610,10 +1430,27 @@ Options:
|
|
|
610
1430
|
--execution <background|attached> Execution mode. Default: settings.json (${workflowDefaultExecutionMode()}).
|
|
611
1431
|
--command <path> Override Codex CLI binary path.
|
|
612
1432
|
--model <model> Pass a model to Codex app-server.
|
|
613
|
-
--timeout-ms <number> Runtime timeout. Default: settings.json (${workflowDefaultTimeoutMs()}).
|
|
1433
|
+
--timeout-ms <number> Runtime timeout; 0 waits for completion/cancel. Default: settings.json (${workflowDefaultTimeoutMs()}).
|
|
614
1434
|
--cwd <dir> Working directory for workflow execution. Default: current cwd.
|
|
615
1435
|
--reasoning-effort <effort> Codex reasoning effort. Default: settings.json (${codexDefaultReasoningEffort()}).
|
|
616
1436
|
--verbosity <verbosity> Codex verbosity. Default: settings.json (${codexDefaultVerbosity()}).
|
|
1437
|
+
|
|
1438
|
+
Background command options:
|
|
1439
|
+
<jobId|metadataPath> Background job id or metadata.json path.
|
|
1440
|
+
--job-id <id> Background job id.
|
|
1441
|
+
--metadata-path <path> Path to metadata.json from a launch record.
|
|
1442
|
+
--result-path <path> Override result.json path.
|
|
1443
|
+
--progress-path <path> Override progress.jsonl path.
|
|
1444
|
+
--pid-path <path> Override pid path.
|
|
1445
|
+
--interval-ms <number> wait polling interval. Default: 1000.
|
|
1446
|
+
--tail <number> logs line count. Default: all lines.
|
|
1447
|
+
--event <event> logs event filter, such as workflow.agent.completed.
|
|
1448
|
+
--signal <SIGINT|SIGTERM|SIGHUP> cancel signal. Default: SIGINT.
|
|
1449
|
+
--plain Print a human-readable background summary.
|
|
1450
|
+
--result wait prints the workflow result on completion.
|
|
1451
|
+
--wait cancel waits for terminal workflow status.
|
|
1452
|
+
--out-dir <dir> archive output directory. Default: .ultracode-for-codex/archive.
|
|
1453
|
+
--output-path <path> archive output file path.
|
|
617
1454
|
`;
|
|
618
1455
|
}
|
|
619
1456
|
function isMainModule() {
|