ultracode-for-codex 0.2.6 → 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 CHANGED
@@ -26,7 +26,7 @@ npm run pack:ultracode-for-codex
26
26
  Install the tarball from a target project:
27
27
 
28
28
  ```bash
29
- npm install --save-dev /path/to/ultracode-for-codex-0.2.6.tgz
29
+ npm install --save-dev /path/to/ultracode-for-codex-<version>.tgz
30
30
  ```
31
31
 
32
32
  Run a workflow:
@@ -41,6 +41,17 @@ npm exec -- ultracode-for-codex run \
41
41
 
42
42
  By default this prints a background launch record to stdout. The record contains
43
43
  `jobId`, `pid`, `resultPath`, `progressPath`, `metadataPath`, and `pidPath`.
44
+ Use the job id to inspect or control the background run:
45
+
46
+ ```bash
47
+ npm exec -- ultracode-for-codex status <jobId> --cwd /path/to/target-repo
48
+ npm exec -- ultracode-for-codex wait <jobId> --cwd /path/to/target-repo
49
+ npm exec -- ultracode-for-codex logs <jobId> --cwd /path/to/target-repo --tail 40
50
+ npm exec -- ultracode-for-codex result <jobId> --cwd /path/to/target-repo
51
+ npm exec -- ultracode-for-codex cancel <jobId> --cwd /path/to/target-repo
52
+ npm exec -- ultracode-for-codex jobs --cwd /path/to/target-repo
53
+ npm exec -- ultracode-for-codex archive <jobId> --cwd /path/to/target-repo
54
+ ```
44
55
 
45
56
  Run attached to the current terminal:
46
57
 
@@ -102,12 +113,21 @@ The package default workflow timeout is `0`, meaning the workflow waits until it
102
113
  completes, is cancelled, or the Codex app-server exits. Set `--timeout-ms` to a
103
114
  positive value to opt into a deadline for one run.
104
115
  Use the default background execution for long Codex-launched work so Codex can
105
- continue other tasks and inspect `progressPath` or `resultPath` later. Use
116
+ continue other tasks and inspect the job later with `status`, `logs`, or
117
+ `result`. Use
106
118
  `--execution attached` only when the caller must block until the final result.
107
119
 
108
120
  ## CLI Controls
109
121
 
110
122
  - Use `--version` or `-v` to print the installed package version.
123
+ - Use `status`, `wait`, `logs`, `result`, and `cancel` with a background
124
+ `jobId` or `metadata.json` path to inspect, wait for, read, or cancel OS
125
+ background runs.
126
+ - Use `jobs` or `list` to enumerate local background runs.
127
+ - Use `archive` or `export` to write a sensitive local JSON bundle for one run
128
+ without deleting runtime state.
129
+ - Use `wait --result`, `cancel --wait`, `logs --event <event>`, and `--plain`
130
+ for shorter foreground checks.
111
131
  - Progress is printed to stderr as JSONL by default.
112
132
  - The final workflow result is printed as JSON to stdout.
113
133
  - JSONL records include `kind`, `version`, `event`, `status`, and `summary`;
@@ -120,6 +140,10 @@ continue other tasks and inspect `progressPath` or `resultPath` later. Use
120
140
  begins.
121
141
  - Each `workflow.agent.completed` record includes phase progress, total known
122
142
  agent progress, and elapsed time.
143
+ - After a completed run, `workflow.summary.ready` reports each phase with its
144
+ planned agent count and angle/focus list, then `workflow.review.recommended`
145
+ asks the current session LLM to critically re-check the final result before
146
+ acting on it.
123
147
  - Press `Ctrl-C` once to cancel the active workflow.
124
148
  - Use `--retry-limit <n>` to retry failed workflows inside the same process.
125
149
  - `--timeout-ms 0` waits for completion, cancellation, or app-server exit.
@@ -196,6 +220,12 @@ For supported CI/CD environments, provenance is available as an explicit opt-in:
196
220
  npm run publish:npm:provenance
197
221
  ```
198
222
 
223
+ Optional live smoke against the local Codex CLI:
224
+
225
+ ```bash
226
+ ULTRACODE_LIVE_SMOKE=1 npm run smoke:live
227
+ ```
228
+
199
229
  Useful local run:
200
230
 
201
231
  ```bash
@@ -13,6 +13,15 @@ workflow runs to OS background execution with result and progress files under
13
13
  Production surface:
14
14
 
15
15
  - `ultracode-for-codex run`
16
+ - `ultracode-for-codex status`
17
+ - `ultracode-for-codex wait`
18
+ - `ultracode-for-codex logs`
19
+ - `ultracode-for-codex result`
20
+ - `ultracode-for-codex cancel`
21
+ - `ultracode-for-codex jobs`
22
+ - `ultracode-for-codex list`
23
+ - `ultracode-for-codex archive`
24
+ - `ultracode-for-codex export`
16
25
 
17
26
  Progress, cancellation, permission review, retry, and final result projection
18
27
  are handled inside the CLI process. Progress is JSONL on stderr by
@@ -31,7 +40,7 @@ npm exec -- ultracode-for-codex --llm-guide
31
40
  For source-checkout validation, install the generated tarball instead:
32
41
 
33
42
  ```bash
34
- npm install --save-dev ./ultracode-for-codex-0.2.6.tgz
43
+ npm install --save-dev ./ultracode-for-codex-<version>.tgz
35
44
  ```
36
45
 
37
46
  Optional Codex companion skill:
@@ -57,8 +66,20 @@ npm exec -- ultracode-for-codex run \
57
66
 
58
67
  The default run prints a background launch record to stdout. Prefer that
59
68
  background path for long Codex-launched work so Codex can continue other tasks
60
- and inspect `progressPath` or `resultPath` later. Use `--execution attached`
61
- only when the caller must block until completion.
69
+ and inspect the job later with `status`, `logs`, or `result`. Use
70
+ `--execution attached` only when the caller must block until completion.
71
+
72
+ Use the background `jobId` from the launch record to inspect or control the run:
73
+
74
+ ```bash
75
+ npm exec -- ultracode-for-codex status <jobId> --cwd /path/to/project
76
+ npm exec -- ultracode-for-codex wait <jobId> --cwd /path/to/project
77
+ npm exec -- ultracode-for-codex logs <jobId> --cwd /path/to/project --tail 40
78
+ npm exec -- ultracode-for-codex result <jobId> --cwd /path/to/project
79
+ npm exec -- ultracode-for-codex cancel <jobId> --cwd /path/to/project
80
+ npm exec -- ultracode-for-codex jobs --cwd /path/to/project
81
+ npm exec -- ultracode-for-codex archive <jobId> --cwd /path/to/project
82
+ ```
62
83
 
63
84
  Use built-in `task` for general work and `code-review` for review-specific work.
64
85
  Both start with an LLM planner, execute phase by phase, run multiple focused
@@ -93,6 +114,13 @@ Settings defaults:
93
114
  Useful controls:
94
115
 
95
116
  - `--version` or `-v` prints the installed package version.
117
+ - `status`, `wait`, `logs`, `result`, and `cancel` accept a background `jobId`
118
+ or `metadata.json` path.
119
+ - `jobs` and `list` enumerate local background runs.
120
+ - `archive` and `export` write a sensitive local JSON bundle for one run without
121
+ deleting runtime state.
122
+ - `wait --result`, `cancel --wait`, `logs --event <event>`, and `--plain` are
123
+ available for shorter foreground checks.
96
124
  - Progress events are printed to stderr as JSONL by default.
97
125
  - The final workflow result is printed as JSON to stdout.
98
126
  - The package default workflow timeout is `0`, meaning the workflow waits until
@@ -107,6 +135,10 @@ Useful controls:
107
135
  begins.
108
136
  - Each `workflow.agent.completed` record includes phase progress, total known
109
137
  agent progress, and elapsed time.
138
+ - After a completed run, `workflow.summary.ready` reports each phase with its
139
+ planned agent count and angle/focus list, then `workflow.review.recommended`
140
+ asks the current session LLM to critically re-check the final result before
141
+ acting on it.
110
142
  - Press `Ctrl-C` once to cancel the running workflow.
111
143
  - Use `--retry-limit <n>` to retry failed runs in the same process.
112
144
  - `--timeout-ms 0` waits for completion, cancellation, or app-server exit.
package/dist/cli.d.ts CHANGED
@@ -20,5 +20,20 @@ interface ParsedOptions {
20
20
  readonly resumeFromRunId?: string;
21
21
  readonly permission?: string;
22
22
  readonly retryLimit?: string;
23
+ readonly jobId?: string;
24
+ readonly metadataPath?: string;
25
+ readonly resultPath?: string;
26
+ readonly progressPath?: string;
27
+ readonly pidPath?: string;
28
+ readonly intervalMs?: string;
29
+ readonly tail?: string;
30
+ readonly signal?: string;
31
+ readonly plain?: string;
32
+ readonly format?: string;
33
+ readonly event?: string;
34
+ readonly result?: string;
35
+ readonly wait?: string;
36
+ readonly outDir?: string;
37
+ readonly outputPath?: string;
23
38
  }
24
39
  export declare function parseOptions(args: readonly string[]): ParsedOptions;
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() {
@@ -7,7 +7,7 @@ Date: 2026-06-22
7
7
  This audit checked:
8
8
 
9
9
  - tracked repository files;
10
- - generated npm package contents for `ultracode-for-codex@0.2.6`;
10
+ - generated npm package contents for `ultracode-for-codex@0.3.0`;
11
11
  - the locally installed companion Codex skill.
12
12
 
13
13
  Generated build output and package tarballs were checked as projections of the
@@ -23,7 +23,8 @@ License transition completed:
23
23
 
24
24
  - Apache-2.0 `LICENSE` file is present;
25
25
  - `package.json` and `package-lock.json` declare `Apache-2.0`;
26
- - package version is prepared as `0.2.6`.
26
+ - release-candidate package version is `0.3.0`;
27
+ - npm latest before this release remains `0.2.6`.
27
28
 
28
29
  ## Evidence
29
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultracode-for-codex",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Run local Codex-backed workflows from a command-owned CLI runtime.",
5
5
  "keywords": [
6
6
  "codex",
@@ -58,6 +58,7 @@
58
58
  "publish:dry-run": "npm publish --dry-run --access public",
59
59
  "publish:npm": "npm publish --access public",
60
60
  "publish:npm:provenance": "npm publish --access public --provenance",
61
+ "smoke:live": "node scripts/live-smoke-ultracode-for-codex.mjs",
61
62
  "test:e2e:ultracode-for-codex": "node scripts/e2e-installed-ultracode-for-codex.mjs",
62
63
  "test:all": "npm test && npm run test:e2e:ultracode-for-codex",
63
64
  "test": "npm run build && npm run verify:runtime-boundary && node --test --test-concurrency=2 test/*.test.mjs",
@@ -15,8 +15,8 @@ Workflow execution runs through the local CLI command. Progress,
15
15
  cancellation, permission review, retry, and result projection stay in that
16
16
  command process. `settings.json` defaults runs to OS background execution; use
17
17
  that path for long Codex-launched work so Codex can keep doing other tasks and
18
- inspect `progressPath` or `resultPath` later. Attached runs stream stderr JSONL
19
- for Codex-readable status, while stdout remains the final workflow result JSON.
18
+ inspect the background job later. Attached runs stream stderr JSONL for
19
+ Codex-readable status, while stdout remains the final workflow result JSON.
20
20
 
21
21
  The default Ultracode work shape is phase-wise parallel execution: built-in
22
22
  `task` and `code-review` first call a planner agent, then execute each planned
@@ -45,7 +45,7 @@ For source-checkout validation before publish:
45
45
 
46
46
  ```bash
47
47
  npm run pack:ultracode-for-codex
48
- npm install --save-dev ./artifacts/ultracode-for-codex-0.2.6.tgz
48
+ npm install --save-dev ./artifacts/ultracode-for-codex-<version>.tgz
49
49
  ```
50
50
 
51
51
  CLI behavior:
@@ -53,6 +53,12 @@ CLI behavior:
53
53
  - `--version` or `-v` prints the installed package version;
54
54
  - default execution is `background`; stdout contains a launch record with
55
55
  `jobId`, `pid`, `resultPath`, `progressPath`, `metadataPath`, and `pidPath`;
56
+ - background jobs can be inspected with `status`, waited with `wait`, read with
57
+ `logs` and `result`, and cancelled with `cancel`;
58
+ - background jobs can be enumerated with `jobs` or `list`, and exported without
59
+ deletion with `archive` or `export`;
60
+ - `wait --result`, `cancel --wait`, `logs --event <event>`, and `--plain`
61
+ provide focused foreground checks;
56
62
  - attached execution is available with `--execution attached` when the caller
57
63
  should stay connected until completion;
58
64
  - attached progress prints to stderr as JSONL by default;
@@ -67,6 +73,9 @@ CLI behavior:
67
73
  phase begins;
68
74
  - each `workflow.agent.completed` record includes phase progress, total known
69
75
  agent progress, and elapsed time;
76
+ - after a completed run, `workflow.summary.ready` reports phase-level agent
77
+ counts and angles, then `workflow.review.recommended` asks the current
78
+ session LLM to critically re-check the final result before acting on it;
70
79
  - `Ctrl-C` cancels the active attached workflow;
71
80
  - `--retry-limit <n>` retries failed workflows inside the same process;
72
81
  - `--timeout-ms 0` waits for completion, cancellation, or app-server exit;