ultracode-for-codex 0.2.6 → 0.3.1

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/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
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';
@@ -30,6 +30,20 @@ async function main(argv) {
30
30
  }
31
31
  if (command === 'run')
32
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);
33
47
  process.stderr.write(`Unknown command: ${command}\n\n${helpText()}`);
34
48
  return 1;
35
49
  }
@@ -69,6 +83,7 @@ async function runWorkflow(args) {
69
83
  const snapshot = await streamCommandWorkflow(runtime, launch, progressMode);
70
84
  if (snapshot.status === 'completed') {
71
85
  process.stdout.write(`${JSON.stringify(snapshot.result ?? null, null, 2)}\n`);
86
+ renderWorkflowCompletionGuidance(snapshot, progressMode);
72
87
  return 0;
73
88
  }
74
89
  renderFailedSnapshot(snapshot, progressMode);
@@ -126,10 +141,11 @@ async function launchBackgroundWorkflow(args, cwd) {
126
141
  await mkdir(runDir, { recursive: true });
127
142
  const stdout = await open(resultPath, 'w');
128
143
  const stderr = await open(progressPath, 'w');
144
+ const entryPath = cliEntryPath();
129
145
  let childPid = 0;
130
146
  try {
131
147
  const child = spawn(process.execPath, [
132
- cliEntryPath(),
148
+ entryPath,
133
149
  'run',
134
150
  ...args,
135
151
  '--execution',
@@ -160,6 +176,9 @@ async function launchBackgroundWorkflow(args, cwd) {
160
176
  progressPath,
161
177
  metadataPath,
162
178
  pidPath,
179
+ nodePath: process.execPath,
180
+ cliEntryPath: entryPath,
181
+ commandLineHint: `${process.execPath} ${entryPath} run`,
163
182
  }, null, 2)}\n`);
164
183
  process.stdout.write(`${JSON.stringify({
165
184
  kind: 'ultracode.workflow.background',
@@ -174,6 +193,624 @@ async function launchBackgroundWorkflow(args, cwd) {
174
193
  }, null, 2)}\n`);
175
194
  return 0;
176
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
+ }
177
814
  function resolveBackgroundRunDir(cwd, template, jobId) {
178
815
  const expanded = template.replaceAll('{jobId}', jobId);
179
816
  return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
@@ -411,6 +1048,110 @@ function renderFailedSnapshot(snapshot, progressMode) {
411
1048
  }
412
1049
  process.stderr.write(`[workflow] terminal failure task=${snapshot.taskId} reason=${snapshot.failureReason ?? 'unknown'} error=${snapshot.error ?? 'unknown'}\n`);
413
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
+ }
414
1155
  function renderControlProgress(event, progressMode, payload, plainText) {
415
1156
  if (progressMode === 'jsonl') {
416
1157
  writeJsonlProgress({ event, ...payload });
@@ -663,6 +1404,15 @@ function helpText() {
663
1404
 
664
1405
  Commands:
665
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.
666
1416
 
667
1417
  Options:
668
1418
  --version, -v Print the package version.
@@ -684,6 +1434,23 @@ Options:
684
1434
  --cwd <dir> Working directory for workflow execution. Default: current cwd.
685
1435
  --reasoning-effort <effort> Codex reasoning effort. Default: settings.json (${codexDefaultReasoningEffort()}).
686
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.
687
1454
  `;
688
1455
  }
689
1456
  function isMainModule() {