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 +32 -2
- package/ULTRACODE_INSTALL.md +35 -3
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +771 -4
- package/docs/provenance-audit.md +3 -2
- package/package.json +2 -1
- package/skills/ultracode-for-codex/SKILL.md +12 -3
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
|
|
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
|
|
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
|
package/ULTRACODE_INSTALL.md
CHANGED
|
@@ -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
|
|
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 `
|
|
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
|
-
|
|
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() {
|
package/docs/provenance-audit.md
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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
|
|
19
|
-
|
|
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
|
|
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;
|