ultracode-for-codex 0.2.5 → 0.2.6
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 +24 -12
- package/ULTRACODE_INSTALL.md +26 -13
- package/dist/cli.js +77 -7
- package/dist/codex/subagent-backend.d.ts +1 -0
- package/dist/codex/subagent-backend.js +22 -10
- package/dist/runtime/package-info.d.ts +1 -0
- package/dist/runtime/package-info.js +12 -0
- package/dist/runtime/workflow-journal.d.ts +0 -1
- package/dist/runtime/workflow-journal.js +1 -4
- package/dist/runtime/workflow-runtime.d.ts +19 -1
- package/dist/runtime/workflow-runtime.js +144 -80
- package/dist/settings.js +1 -6
- package/docs/provenance-audit.md +2 -2
- package/docs/ultracode-p3c-worktree-isolation.md +8 -8
- package/package.json +3 -1
- package/settings.json +1 -1
- package/skills/ultracode-for-codex/SKILL.md +23 -13
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.
|
|
29
|
+
npm install --save-dev /path/to/ultracode-for-codex-0.2.6.tgz
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Run a workflow:
|
|
@@ -84,7 +84,7 @@ Package defaults live in `settings.json`:
|
|
|
84
84
|
"progress": "jsonl",
|
|
85
85
|
"permission": "ask",
|
|
86
86
|
"retryLimit": 0,
|
|
87
|
-
"timeoutMs":
|
|
87
|
+
"timeoutMs": 0,
|
|
88
88
|
"background": {
|
|
89
89
|
"runDir": ".ultracode-for-codex/background/{jobId}",
|
|
90
90
|
"resultFile": "result.json",
|
|
@@ -98,25 +98,38 @@ Package defaults live in `settings.json`:
|
|
|
98
98
|
|
|
99
99
|
Use `--execution attached`, `--progress`, `--permission`, `--retry-limit`, and
|
|
100
100
|
`--timeout-ms` to override settings for one run.
|
|
101
|
-
The package default workflow timeout is `
|
|
101
|
+
The package default workflow timeout is `0`, meaning the workflow waits until it
|
|
102
|
+
completes, is cancelled, or the Codex app-server exits. Set `--timeout-ms` to a
|
|
103
|
+
positive value to opt into a deadline for one run.
|
|
104
|
+
Use the default background execution for long Codex-launched work so Codex can
|
|
105
|
+
continue other tasks and inspect `progressPath` or `resultPath` later. Use
|
|
106
|
+
`--execution attached` only when the caller must block until the final result.
|
|
102
107
|
|
|
103
108
|
## CLI Controls
|
|
104
109
|
|
|
110
|
+
- Use `--version` or `-v` to print the installed package version.
|
|
105
111
|
- Progress is printed to stderr as JSONL by default.
|
|
106
112
|
- The final workflow result is printed as JSON to stdout.
|
|
107
113
|
- JSONL records include `kind`, `version`, `event`, `status`, and `summary`;
|
|
108
114
|
agent records also include stable agent identity and label fields.
|
|
109
|
-
- Built-in `task` and `code-review` emit `workflow.plan.ready`
|
|
110
|
-
|
|
115
|
+
- Built-in `task` and `code-review` emit `workflow.plan.ready` as a planning
|
|
116
|
+
snapshot, not a promise that every later phase is already known.
|
|
117
|
+
- `workflow.phase.planned` is emitted immediately before each phase starts and
|
|
118
|
+
carries that phase's current planned agent role labels. Each
|
|
119
|
+
`workflow.phase.started` record repeats the same role labels when the phase
|
|
120
|
+
begins.
|
|
121
|
+
- Each `workflow.agent.completed` record includes phase progress, total known
|
|
122
|
+
agent progress, and elapsed time.
|
|
111
123
|
- Press `Ctrl-C` once to cancel the active workflow.
|
|
112
124
|
- Use `--retry-limit <n>` to retry failed workflows inside the same process.
|
|
113
|
-
- `--timeout-ms`
|
|
114
|
-
|
|
125
|
+
- `--timeout-ms 0` waits for completion, cancellation, or app-server exit.
|
|
126
|
+
Positive values opt into a workflow deadline and per-agent silence budget;
|
|
127
|
+
that budget is not divided by the retry budget.
|
|
115
128
|
- Use `--permission ask|allow|deny` for project/user/plugin/scriptPath workflow
|
|
116
129
|
permission reviews.
|
|
117
130
|
- Use `--progress plain` for human-readable log lines.
|
|
118
131
|
- Use `--execution background` for OS background runs and `--execution attached`
|
|
119
|
-
when
|
|
132
|
+
only when the caller should stay connected until completion.
|
|
120
133
|
|
|
121
134
|
## Codex Companion Skill
|
|
122
135
|
|
|
@@ -138,8 +151,8 @@ want Codex to auto-load the package boundaries and verification routine.
|
|
|
138
151
|
planner-selected phase-wise parallel subagents, then synthesize each phase and
|
|
139
152
|
the final result.
|
|
140
153
|
- Workflow execution is local and command-owned; settings default to OS
|
|
141
|
-
background execution
|
|
142
|
-
|
|
154
|
+
background execution so long runs can keep waiting while Codex does other
|
|
155
|
+
work.
|
|
143
156
|
- `.ultracode-for-codex` workflow state is sensitive local data.
|
|
144
157
|
- `journalPath`, `journal.jsonl`, and journal contents stay out of CLI output.
|
|
145
158
|
Local runtime state may still contain runtime-owned
|
|
@@ -147,8 +160,7 @@ want Codex to auto-load the package boundaries and verification routine.
|
|
|
147
160
|
- `resumeFromRunId` remains runtime-internal and same-session; users retry the
|
|
148
161
|
active run or rerun the workflow command.
|
|
149
162
|
- `agent(..., { isolation: "worktree" })` runs the agent in a detached git
|
|
150
|
-
worktree
|
|
151
|
-
worktrees for review.
|
|
163
|
+
worktree and preserves the worktree for review, including clean worktrees.
|
|
152
164
|
|
|
153
165
|
## Development
|
|
154
166
|
|
package/ULTRACODE_INSTALL.md
CHANGED
|
@@ -31,7 +31,7 @@ npm exec -- ultracode-for-codex --llm-guide
|
|
|
31
31
|
For source-checkout validation, install the generated tarball instead:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npm install --save-dev ./ultracode-for-codex-0.2.
|
|
34
|
+
npm install --save-dev ./ultracode-for-codex-0.2.6.tgz
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Optional Codex companion skill:
|
|
@@ -55,8 +55,10 @@ npm exec -- ultracode-for-codex run \
|
|
|
55
55
|
--args '{"prompt":"review the current change"}'
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
The default run prints a background launch record to stdout.
|
|
59
|
-
|
|
58
|
+
The default run prints a background launch record to stdout. Prefer that
|
|
59
|
+
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.
|
|
60
62
|
|
|
61
63
|
Use built-in `task` for general work and `code-review` for review-specific work.
|
|
62
64
|
Both start with an LLM planner, execute phase by phase, run multiple focused
|
|
@@ -76,7 +78,7 @@ Settings defaults:
|
|
|
76
78
|
"progress": "jsonl",
|
|
77
79
|
"permission": "ask",
|
|
78
80
|
"retryLimit": 0,
|
|
79
|
-
"timeoutMs":
|
|
81
|
+
"timeoutMs": 0,
|
|
80
82
|
"background": {
|
|
81
83
|
"runDir": ".ultracode-for-codex/background/{jobId}",
|
|
82
84
|
"resultFile": "result.json",
|
|
@@ -90,29 +92,38 @@ Settings defaults:
|
|
|
90
92
|
|
|
91
93
|
Useful controls:
|
|
92
94
|
|
|
95
|
+
- `--version` or `-v` prints the installed package version.
|
|
93
96
|
- Progress events are printed to stderr as JSONL by default.
|
|
94
97
|
- The final workflow result is printed as JSON to stdout.
|
|
95
|
-
- The package default workflow timeout is `
|
|
98
|
+
- The package default workflow timeout is `0`, meaning the workflow waits until
|
|
99
|
+
it completes, is cancelled, or the Codex app-server exits.
|
|
96
100
|
- JSONL records include `kind`, `version`, `event`, `status`, and `summary`;
|
|
97
101
|
agent records also include stable agent identity and label fields.
|
|
98
|
-
- Built-in `task` and `code-review` emit `workflow.plan.ready`
|
|
99
|
-
|
|
102
|
+
- Built-in `task` and `code-review` emit `workflow.plan.ready` as a planning
|
|
103
|
+
snapshot, not a promise that every later phase is already known.
|
|
104
|
+
- `workflow.phase.planned` is emitted immediately before each phase starts and
|
|
105
|
+
carries that phase's current planned agent role labels. Each
|
|
106
|
+
`workflow.phase.started` record repeats the same role labels when the phase
|
|
107
|
+
begins.
|
|
108
|
+
- Each `workflow.agent.completed` record includes phase progress, total known
|
|
109
|
+
agent progress, and elapsed time.
|
|
100
110
|
- Press `Ctrl-C` once to cancel the running workflow.
|
|
101
111
|
- Use `--retry-limit <n>` to retry failed runs in the same process.
|
|
102
|
-
- `--timeout-ms`
|
|
103
|
-
|
|
112
|
+
- `--timeout-ms 0` waits for completion, cancellation, or app-server exit.
|
|
113
|
+
Positive values opt into a workflow deadline and per-agent silence budget;
|
|
114
|
+
that budget is not divided by the retry budget.
|
|
104
115
|
- Use `--permission ask|allow|deny` for project/user/plugin/scriptPath
|
|
105
116
|
workflow permission reviews.
|
|
106
117
|
- Use `--progress plain` for human-readable log lines.
|
|
107
118
|
- Use `--execution background` for OS background runs and `--execution attached`
|
|
108
|
-
|
|
119
|
+
only when the caller should stay connected until completion.
|
|
109
120
|
|
|
110
121
|
## Runtime Contract
|
|
111
122
|
|
|
112
123
|
- Use Codex app-server over stdio as the production backend.
|
|
113
124
|
- Keep workflow execution local and command-owned; settings default to OS
|
|
114
|
-
background execution
|
|
115
|
-
|
|
125
|
+
background execution so long runs can keep waiting while Codex does other
|
|
126
|
+
work.
|
|
116
127
|
- Route progress, cancellation, permission review, retry, and result projection
|
|
117
128
|
through the CLI command.
|
|
118
129
|
- Keep stdout reserved for the final JSON result; stream progress records to
|
|
@@ -130,13 +141,15 @@ Useful controls:
|
|
|
130
141
|
- `resumeFromRunId` remains a runtime-internal same-session capability; the
|
|
131
142
|
CLI uses retry or explicit reruns for user-facing recovery.
|
|
132
143
|
- Use `isolation: "worktree"` only in git repositories with at least one commit.
|
|
133
|
-
|
|
144
|
+
Isolated worktrees are intentionally preserved for review, including clean
|
|
145
|
+
worktrees.
|
|
134
146
|
- Treat `.ultracode-for-codex` workflow state as sensitive local data.
|
|
135
147
|
|
|
136
148
|
## First Checks After Install
|
|
137
149
|
|
|
138
150
|
```bash
|
|
139
151
|
npm exec -- ultracode-for-codex --help
|
|
152
|
+
npm exec -- ultracode-for-codex --version
|
|
140
153
|
npm exec -- ultracode-for-codex --llm-guide
|
|
141
154
|
```
|
|
142
155
|
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
9
9
|
import { CodexSubagentBackend } from './codex/subagent-backend.js';
|
|
10
10
|
import { WorkflowTaskRegistry } from './runtime/workflow-runtime.js';
|
|
11
11
|
import { UltracodeRequestError } from './runtime/types.js';
|
|
12
|
+
import { ultracodePackageVersion } from './runtime/package-info.js';
|
|
12
13
|
import { renderUltracodeInstallGuideNotice } from './ultracode-install-guide.js';
|
|
13
14
|
import { codexDefaultReasoningEffort, codexDefaultVerbosity, isReasoningEffort, isVerbosity, isWorkflowExecutionMode, isWorkflowPermissionPolicy, isWorkflowProgressMode, workflowBackgroundDefaults, workflowDefaultExecutionMode, workflowDefaultPermissionPolicy, workflowDefaultProgressMode, workflowDefaultRetryLimit, workflowDefaultTimeoutMs, } from './settings.js';
|
|
14
15
|
const ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION = 'v1';
|
|
@@ -19,6 +20,10 @@ async function main(argv) {
|
|
|
19
20
|
process.stdout.write(helpText());
|
|
20
21
|
return 0;
|
|
21
22
|
}
|
|
23
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
24
|
+
process.stdout.write(`ultracode-for-codex ${ultracodePackageVersion()}\n`);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
22
27
|
if (command === '--llm-guide' || command === 'llm-guide') {
|
|
23
28
|
process.stdout.write(renderUltracodeInstallGuideNotice());
|
|
24
29
|
return 0;
|
|
@@ -347,8 +352,19 @@ function renderWorkflowEvent(event, progressMode) {
|
|
|
347
352
|
case 'workflow.started':
|
|
348
353
|
process.stderr.write(`[workflow] started ${event.workflowName} task=${event.taskId} run=${event.runId}\n`);
|
|
349
354
|
return;
|
|
355
|
+
case 'workflow.phase.planned':
|
|
356
|
+
process.stderr.write(`[phase-plan] ${event.title} (${event.plannedAgentCount} agents)${event.goal ? ` - ${event.goal}` : ''}\n`);
|
|
357
|
+
for (const agent of event.plannedAgents) {
|
|
358
|
+
process.stderr.write(`[phase-plan] - ${agent.title}${agent.label ? ` (${agent.label})` : ''}${agent.focus ? `: ${agent.focus}` : ''}\n`);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
350
361
|
case 'workflow.phase.started':
|
|
351
|
-
process.stderr.write(`[phase] ${event.title}${event.detail ? ` - ${event.detail}` : ''}\n`);
|
|
362
|
+
process.stderr.write(`[phase] ${event.title}${event.plannedAgentCount ? ` (${event.plannedAgentCount} agents)` : ''}${event.detail ? ` - ${event.detail}` : ''}\n`);
|
|
363
|
+
if (event.plannedAgents) {
|
|
364
|
+
for (const agent of event.plannedAgents) {
|
|
365
|
+
process.stderr.write(`[phase] - ${agent.title}${agent.label ? ` (${agent.label})` : ''}${agent.focus ? `: ${agent.focus}` : ''}\n`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
352
368
|
return;
|
|
353
369
|
case 'workflow.plan.ready':
|
|
354
370
|
process.stderr.write(`[plan] mode=${event.mode} phases=${event.phases.length}${event.rationale ? ` - ${event.rationale}` : ''}\n`);
|
|
@@ -366,7 +382,7 @@ function renderWorkflowEvent(event, progressMode) {
|
|
|
366
382
|
process.stderr.write(`[agent:${event.agentIndex + 1}] started ${event.label}\n`);
|
|
367
383
|
return;
|
|
368
384
|
case 'workflow.agent.completed':
|
|
369
|
-
process.stderr.write(`[agent:${event.agentIndex + 1}] completed ${event.label} tokens=${event.tokens} preview=${formatPreview(event.resultPreview)}${event.cached ? ' cached=true' : ''}\n`);
|
|
385
|
+
process.stderr.write(`[agent:${event.agentIndex + 1}] completed ${event.label} | ${agentCompletionProgressSummary(event)} | tokens=${event.tokens} preview=${formatPreview(event.resultPreview)}${event.cached ? ' cached=true' : ''}\n`);
|
|
370
386
|
return;
|
|
371
387
|
case 'workflow.agent.failed':
|
|
372
388
|
process.stderr.write(`[agent:${event.agentIndex + 1}] failed ${event.label} ${event.error}\n`);
|
|
@@ -409,6 +425,38 @@ function writeJsonlProgress(payload) {
|
|
|
409
425
|
...payload,
|
|
410
426
|
})}\n`);
|
|
411
427
|
}
|
|
428
|
+
function phaseStartedSummary(event) {
|
|
429
|
+
const agentText = event.plannedAgentCount
|
|
430
|
+
? `${event.plannedAgentCount} planned agent${event.plannedAgentCount === 1 ? '' : 's'}`
|
|
431
|
+
: '';
|
|
432
|
+
const suffix = [agentText, event.detail ?? event.goal].filter(Boolean).join(': ');
|
|
433
|
+
return suffix ? `Phase ${event.title}: ${suffix}` : `Phase ${event.title}`;
|
|
434
|
+
}
|
|
435
|
+
function phasePlannedSummary(event) {
|
|
436
|
+
return `Phase ${event.title} planned: ${event.plannedAgentCount} planned agent${event.plannedAgentCount === 1 ? '' : 's'}`;
|
|
437
|
+
}
|
|
438
|
+
function agentCompletionProgressSummary(event) {
|
|
439
|
+
const parts = [];
|
|
440
|
+
if (event.phase
|
|
441
|
+
&& event.phaseCompletedAgentCount !== undefined
|
|
442
|
+
&& event.phaseKnownAgentCount !== undefined) {
|
|
443
|
+
parts.push(`Phase ${event.phase} (${event.phaseCompletedAgentCount}/${event.phaseKnownAgentCount})`);
|
|
444
|
+
}
|
|
445
|
+
parts.push(`${event.completedAgentCount} out of ${event.knownAgentCount} agents have completed the task`);
|
|
446
|
+
parts.push(`${formatElapsedDuration(event.elapsedMs)} elapsed`);
|
|
447
|
+
return parts.join(', ');
|
|
448
|
+
}
|
|
449
|
+
function formatElapsedDuration(ms) {
|
|
450
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
451
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
452
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
453
|
+
const seconds = totalSeconds % 60;
|
|
454
|
+
if (hours > 0)
|
|
455
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
456
|
+
if (minutes > 0)
|
|
457
|
+
return `${minutes}m ${seconds}s`;
|
|
458
|
+
return `${seconds}s`;
|
|
459
|
+
}
|
|
412
460
|
function progressPayloadForEvent(event) {
|
|
413
461
|
switch (event.type) {
|
|
414
462
|
case 'workflow.started':
|
|
@@ -423,22 +471,38 @@ function progressPayloadForEvent(event) {
|
|
|
423
471
|
workflowSourcePath: event.workflowSourcePath,
|
|
424
472
|
scriptHash: event.scriptHash,
|
|
425
473
|
};
|
|
474
|
+
case 'workflow.phase.planned':
|
|
475
|
+
return {
|
|
476
|
+
event: event.type,
|
|
477
|
+
status: 'planned',
|
|
478
|
+
summary: phasePlannedSummary(event),
|
|
479
|
+
taskId: event.taskId,
|
|
480
|
+
runId: event.runId,
|
|
481
|
+
phaseIndex: event.phaseIndex,
|
|
482
|
+
title: event.title,
|
|
483
|
+
goal: event.goal,
|
|
484
|
+
plannedAgentCount: event.plannedAgentCount,
|
|
485
|
+
plannedAgents: event.plannedAgents,
|
|
486
|
+
};
|
|
426
487
|
case 'workflow.phase.started':
|
|
427
488
|
return {
|
|
428
489
|
event: event.type,
|
|
429
490
|
status: 'running',
|
|
430
|
-
summary: event
|
|
491
|
+
summary: phaseStartedSummary(event),
|
|
431
492
|
taskId: event.taskId,
|
|
432
493
|
runId: event.runId,
|
|
433
494
|
phaseIndex: event.phaseIndex,
|
|
434
495
|
title: event.title,
|
|
435
496
|
detail: event.detail,
|
|
497
|
+
goal: event.goal,
|
|
498
|
+
plannedAgentCount: event.plannedAgentCount,
|
|
499
|
+
plannedAgents: event.plannedAgents,
|
|
436
500
|
};
|
|
437
501
|
case 'workflow.plan.ready':
|
|
438
502
|
return {
|
|
439
503
|
event: event.type,
|
|
440
504
|
status: 'planned',
|
|
441
|
-
summary: `Workflow
|
|
505
|
+
summary: `Workflow planning snapshot: ${event.phases.length} known phase${event.phases.length === 1 ? '' : 's'}, mode=${event.mode}`,
|
|
442
506
|
taskId: event.taskId,
|
|
443
507
|
runId: event.runId,
|
|
444
508
|
mode: event.mode,
|
|
@@ -472,7 +536,7 @@ function progressPayloadForEvent(event) {
|
|
|
472
536
|
return {
|
|
473
537
|
event: event.type,
|
|
474
538
|
status: 'completed',
|
|
475
|
-
summary: `Agent ${event.agentIndex + 1} completed`,
|
|
539
|
+
summary: `Agent ${event.agentIndex + 1} completed: ${event.label}. ${agentCompletionProgressSummary(event)}`,
|
|
476
540
|
taskId: event.taskId,
|
|
477
541
|
runId: event.runId,
|
|
478
542
|
agentIndex: event.agentIndex,
|
|
@@ -483,6 +547,11 @@ function progressPayloadForEvent(event) {
|
|
|
483
547
|
toolCalls: event.toolCalls,
|
|
484
548
|
resultPreview: event.resultPreview,
|
|
485
549
|
cached: event.cached,
|
|
550
|
+
elapsedMs: event.elapsedMs,
|
|
551
|
+
completedAgentCount: event.completedAgentCount,
|
|
552
|
+
knownAgentCount: event.knownAgentCount,
|
|
553
|
+
phaseCompletedAgentCount: event.phaseCompletedAgentCount,
|
|
554
|
+
phaseKnownAgentCount: event.phaseKnownAgentCount,
|
|
486
555
|
worktreePreserved: event.worktreePreserved,
|
|
487
556
|
preservedWorktrees: event.preservedWorktrees,
|
|
488
557
|
};
|
|
@@ -537,7 +606,7 @@ function parseIntOption(value, fallback) {
|
|
|
537
606
|
if (value === undefined)
|
|
538
607
|
return fallback;
|
|
539
608
|
const parsed = Number.parseInt(value, 10);
|
|
540
|
-
if (!Number.isFinite(parsed) || parsed
|
|
609
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
541
610
|
return fallback;
|
|
542
611
|
return parsed;
|
|
543
612
|
}
|
|
@@ -596,6 +665,7 @@ Commands:
|
|
|
596
665
|
run Run a workflow as a local CLI command.
|
|
597
666
|
|
|
598
667
|
Options:
|
|
668
|
+
--version, -v Print the package version.
|
|
599
669
|
--llm-guide Print the Ultracode install and usage guide.
|
|
600
670
|
--accept-llm-guide <version> Required for run. Current version: ${ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION}.
|
|
601
671
|
--script <js> Inline workflow script.
|
|
@@ -610,7 +680,7 @@ Options:
|
|
|
610
680
|
--execution <background|attached> Execution mode. Default: settings.json (${workflowDefaultExecutionMode()}).
|
|
611
681
|
--command <path> Override Codex CLI binary path.
|
|
612
682
|
--model <model> Pass a model to Codex app-server.
|
|
613
|
-
--timeout-ms <number> Runtime timeout. Default: settings.json (${workflowDefaultTimeoutMs()}).
|
|
683
|
+
--timeout-ms <number> Runtime timeout; 0 waits for completion/cancel. Default: settings.json (${workflowDefaultTimeoutMs()}).
|
|
614
684
|
--cwd <dir> Working directory for workflow execution. Default: current cwd.
|
|
615
685
|
--reasoning-effort <effort> Codex reasoning effort. Default: settings.json (${codexDefaultReasoningEffort()}).
|
|
616
686
|
--verbosity <verbosity> Codex verbosity. Default: settings.json (${codexDefaultVerbosity()}).
|
|
@@ -20,6 +20,7 @@ export declare class CodexSubagentBackend implements SubagentBackend {
|
|
|
20
20
|
private readonly cwd;
|
|
21
21
|
private readonly configuredModel?;
|
|
22
22
|
private readonly timeoutMs;
|
|
23
|
+
private readonly rpcTimeoutMs;
|
|
23
24
|
private readonly reasoningEffort;
|
|
24
25
|
private readonly verbosity;
|
|
25
26
|
private child;
|
|
@@ -6,9 +6,11 @@ import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
|
6
6
|
import readline from 'node:readline';
|
|
7
7
|
import { codexDefaultReasoningEffort, codexDefaultVerbosity, } from '../settings.js';
|
|
8
8
|
import { estimateTokens } from '../runtime/types.js';
|
|
9
|
+
import { ultracodePackageVersion } from '../runtime/package-info.js';
|
|
9
10
|
import { codexChildProcessEnv } from './env.js';
|
|
10
11
|
const USAGE_NOTIFICATION_GRACE_MS = 100;
|
|
11
12
|
const BUFFERED_TURN_STATE_TTL_MS = 30_000;
|
|
13
|
+
const DEFAULT_CODEX_RPC_TIMEOUT_MS = 30_000;
|
|
12
14
|
const FALLBACK_CODEX_MODEL = 'gpt-5.5';
|
|
13
15
|
const WORKSPACE_DYNAMIC_TOOL_NAMESPACE = 'workspace';
|
|
14
16
|
const MAX_WORKSPACE_TOOL_READ_BYTES = 200_000;
|
|
@@ -72,6 +74,7 @@ export class CodexSubagentBackend {
|
|
|
72
74
|
cwd;
|
|
73
75
|
configuredModel;
|
|
74
76
|
timeoutMs;
|
|
77
|
+
rpcTimeoutMs;
|
|
75
78
|
reasoningEffort;
|
|
76
79
|
verbosity;
|
|
77
80
|
child = null;
|
|
@@ -89,7 +92,8 @@ export class CodexSubagentBackend {
|
|
|
89
92
|
this.cwd = options.cwd;
|
|
90
93
|
this.model = options.model ?? 'codex-subagent';
|
|
91
94
|
this.configuredModel = options.model;
|
|
92
|
-
this.timeoutMs = options.timeoutMs;
|
|
95
|
+
this.timeoutMs = normalizeOptionalTimeoutMs(options.timeoutMs);
|
|
96
|
+
this.rpcTimeoutMs = this.timeoutMs > 0 ? this.timeoutMs : DEFAULT_CODEX_RPC_TIMEOUT_MS;
|
|
93
97
|
this.reasoningEffort = options.reasoningEffort ?? codexDefaultReasoningEffort();
|
|
94
98
|
this.verbosity = options.verbosity ?? codexDefaultVerbosity();
|
|
95
99
|
}
|
|
@@ -220,7 +224,7 @@ export class CodexSubagentBackend {
|
|
|
220
224
|
clientInfo: {
|
|
221
225
|
name: 'ultracode_for_codex',
|
|
222
226
|
title: 'Ultracode for Codex',
|
|
223
|
-
version:
|
|
227
|
+
version: ultracodePackageVersion(),
|
|
224
228
|
},
|
|
225
229
|
capabilities: {
|
|
226
230
|
experimentalApi: true,
|
|
@@ -284,8 +288,8 @@ export class CodexSubagentBackend {
|
|
|
284
288
|
return new Promise((resolve, reject) => {
|
|
285
289
|
const timer = setTimeout(() => {
|
|
286
290
|
this.pending.delete(id);
|
|
287
|
-
reject(new Error(`${method} timed out after ${this.
|
|
288
|
-
}, this.
|
|
291
|
+
reject(new Error(`${method} timed out after ${this.rpcTimeoutMs}ms`));
|
|
292
|
+
}, this.rpcTimeoutMs);
|
|
289
293
|
this.pending.set(id, { method, resolve, reject, timer });
|
|
290
294
|
});
|
|
291
295
|
}
|
|
@@ -498,13 +502,16 @@ export class CodexSubagentBackend {
|
|
|
498
502
|
const key = `${threadId}:${turnId}`;
|
|
499
503
|
return new Promise((resolve, reject) => {
|
|
500
504
|
let waiter;
|
|
501
|
-
const timer =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
505
|
+
const timer = this.timeoutMs > 0
|
|
506
|
+
? setTimeout(() => {
|
|
507
|
+
this.turnWaiters.delete(key);
|
|
508
|
+
cleanup();
|
|
509
|
+
reject(new Error(`turn timed out after ${this.timeoutMs}ms`));
|
|
510
|
+
}, this.timeoutMs)
|
|
511
|
+
: null;
|
|
506
512
|
const cleanup = () => {
|
|
507
|
-
|
|
513
|
+
if (timer)
|
|
514
|
+
clearTimeout(timer);
|
|
508
515
|
if (waiter?.usageGraceTimer) {
|
|
509
516
|
clearTimeout(waiter.usageGraceTimer);
|
|
510
517
|
waiter.usageGraceTimer = undefined;
|
|
@@ -660,6 +667,11 @@ function estimatedUsage(prompt, text) {
|
|
|
660
667
|
source: 'estimated',
|
|
661
668
|
};
|
|
662
669
|
}
|
|
670
|
+
function normalizeOptionalTimeoutMs(value) {
|
|
671
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
672
|
+
return 0;
|
|
673
|
+
return Math.floor(value);
|
|
674
|
+
}
|
|
663
675
|
function turnStateKey(threadId, turnId) {
|
|
664
676
|
return typeof threadId === 'string' && typeof turnId === 'string'
|
|
665
677
|
? `${threadId}:${turnId}`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ultracodePackageVersion(): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
let cachedPackageVersion;
|
|
3
|
+
export function ultracodePackageVersion() {
|
|
4
|
+
if (cachedPackageVersion)
|
|
5
|
+
return cachedPackageVersion;
|
|
6
|
+
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
if (typeof packageJson.version !== 'string' || !packageJson.version.trim()) {
|
|
8
|
+
throw new Error('Package version is missing from package.json.');
|
|
9
|
+
}
|
|
10
|
+
cachedPackageVersion = packageJson.version;
|
|
11
|
+
return cachedPackageVersion;
|
|
12
|
+
}
|
|
@@ -131,5 +131,4 @@ export declare function computeWorkflowAgentCallKey(input: {
|
|
|
131
131
|
}): string;
|
|
132
132
|
export declare function workflowJournalHash(entryWithoutEntryHash: unknown): string;
|
|
133
133
|
export declare function readWorkflowJournal(journalPath: string): Promise<WorkflowJournalReadResult>;
|
|
134
|
-
export declare function cleanupWorkflowJournalTranscriptDir(transcriptDir: string): Promise<void>;
|
|
135
134
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { constants as fsConstants } from 'node:fs';
|
|
3
|
-
import { chmod, lstat, mkdir, open, readFile,
|
|
3
|
+
import { chmod, lstat, mkdir, open, readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
export class WorkflowJournalError extends Error {
|
|
6
6
|
cause;
|
|
@@ -250,9 +250,6 @@ export async function readWorkflowJournal(journalPath) {
|
|
|
250
250
|
validateWorkflowJournal(entries);
|
|
251
251
|
return { entries, truncatedTail };
|
|
252
252
|
}
|
|
253
|
-
export async function cleanupWorkflowJournalTranscriptDir(transcriptDir) {
|
|
254
|
-
await rm(transcriptDir, { recursive: true, force: true });
|
|
255
|
-
}
|
|
256
253
|
function parseJournalLine(line, lineNumber) {
|
|
257
254
|
if (Buffer.byteLength(line, 'utf8') > MAX_LINE_BYTES) {
|
|
258
255
|
throw new WorkflowJournalValidationError(`journal line ${lineNumber} exceeds ${MAX_LINE_BYTES} bytes.`);
|
|
@@ -32,6 +32,9 @@ export type WorkflowEvent = {
|
|
|
32
32
|
readonly phaseIndex: number;
|
|
33
33
|
readonly title: string;
|
|
34
34
|
readonly detail?: string;
|
|
35
|
+
readonly goal?: string;
|
|
36
|
+
readonly plannedAgentCount?: number;
|
|
37
|
+
readonly plannedAgents?: readonly WorkflowPlanAgent[];
|
|
35
38
|
} | {
|
|
36
39
|
readonly type: 'workflow.plan.ready';
|
|
37
40
|
readonly taskId: string;
|
|
@@ -39,6 +42,15 @@ export type WorkflowEvent = {
|
|
|
39
42
|
readonly mode: string;
|
|
40
43
|
readonly rationale?: string;
|
|
41
44
|
readonly phases: readonly WorkflowPlanPhase[];
|
|
45
|
+
} | {
|
|
46
|
+
readonly type: 'workflow.phase.planned';
|
|
47
|
+
readonly taskId: string;
|
|
48
|
+
readonly runId: string;
|
|
49
|
+
readonly phaseIndex: number;
|
|
50
|
+
readonly title: string;
|
|
51
|
+
readonly goal?: string;
|
|
52
|
+
readonly plannedAgentCount: number;
|
|
53
|
+
readonly plannedAgents: readonly WorkflowPlanAgent[];
|
|
42
54
|
} | {
|
|
43
55
|
readonly type: 'workflow.log';
|
|
44
56
|
readonly taskId: string;
|
|
@@ -65,6 +77,11 @@ export type WorkflowEvent = {
|
|
|
65
77
|
readonly toolCalls: number;
|
|
66
78
|
readonly resultPreview?: string;
|
|
67
79
|
readonly cached?: boolean;
|
|
80
|
+
readonly elapsedMs: number;
|
|
81
|
+
readonly completedAgentCount: number;
|
|
82
|
+
readonly knownAgentCount: number;
|
|
83
|
+
readonly phaseCompletedAgentCount?: number;
|
|
84
|
+
readonly phaseKnownAgentCount?: number;
|
|
68
85
|
readonly worktreePath?: string;
|
|
69
86
|
readonly worktreePreserved?: boolean;
|
|
70
87
|
readonly preservedWorktrees?: readonly WorkflowAgentPreservedWorktree[];
|
|
@@ -211,7 +228,7 @@ interface BuiltinWorkflow {
|
|
|
211
228
|
export interface WorkflowAgentPreservedWorktree {
|
|
212
229
|
readonly path: string;
|
|
213
230
|
readonly attemptIndex: number;
|
|
214
|
-
readonly reason: '
|
|
231
|
+
readonly reason: 'clean' | 'changed' | 'stalled' | 'aborted' | 'status_unavailable';
|
|
215
232
|
}
|
|
216
233
|
export declare class WorkflowTaskRegistry implements WorkflowRuntime {
|
|
217
234
|
private readonly options;
|
|
@@ -270,6 +287,7 @@ export declare class WorkflowTaskRegistry implements WorkflowRuntime {
|
|
|
270
287
|
private parallel;
|
|
271
288
|
private pipeline;
|
|
272
289
|
private announcePlan;
|
|
290
|
+
private announcePhasePlan;
|
|
273
291
|
private phase;
|
|
274
292
|
private completeTask;
|
|
275
293
|
private failTask;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { chmod, mkdir, readdir, readFile, realpath,
|
|
3
|
+
import { chmod, mkdir, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { createContext, runInContext } from 'node:vm';
|
|
8
8
|
import { UltracodeRequestError, estimateTokens } from './types.js';
|
|
9
|
-
import { WORKFLOW_JOURNAL_GENESIS_AGENT_CALL_KEY, WORKFLOW_JOURNAL_WRITE_FAILED_REASON, WorkflowJournalError, WorkflowJournalWriter,
|
|
9
|
+
import { WORKFLOW_JOURNAL_GENESIS_AGENT_CALL_KEY, WORKFLOW_JOURNAL_WRITE_FAILED_REASON, WorkflowJournalError, WorkflowJournalWriter, computeWorkflowAgentCallKey, isWorkflowJournalError, normalizeJournalJsonValue, readWorkflowJournal, workflowJournalPath, } from './workflow-journal.js';
|
|
10
10
|
const MAX_SCRIPT_BYTES = 64 * 1024;
|
|
11
11
|
const MAX_AGENT_CALLS = 1000;
|
|
12
12
|
const MAX_PARALLELISM = 16;
|
|
@@ -141,6 +141,15 @@ const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
|
141
141
|
};
|
|
142
142
|
const input = args && typeof args === "object" ? args : {};
|
|
143
143
|
const prompts = Array.isArray(input.prompts) ? input.prompts : [];
|
|
144
|
+
if (prompts.length > 0) {
|
|
145
|
+
announcePhasePlan({
|
|
146
|
+
title: "Batch",
|
|
147
|
+
agents: prompts.map((_, index) => ({
|
|
148
|
+
title: "Batch " + (index + 1),
|
|
149
|
+
label: "batch-" + (index + 1)
|
|
150
|
+
}))
|
|
151
|
+
});
|
|
152
|
+
}
|
|
144
153
|
phase("Batch");
|
|
145
154
|
return await parallel(prompts.map((prompt, index) => () => agent(
|
|
146
155
|
prompt == null ? "" : "" + prompt,
|
|
@@ -214,10 +223,8 @@ const plan = await agent([
|
|
|
214
223
|
}
|
|
215
224
|
});
|
|
216
225
|
const selectedPhases = plan.mode === "single" ? [plan.phases[0]] : plan.phases;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
rationale: plan.rationale,
|
|
220
|
-
phases: selectedPhases.map((phasePlan) => ({
|
|
226
|
+
function plannedPhaseFor(phasePlan) {
|
|
227
|
+
return {
|
|
221
228
|
id: phasePlan.id,
|
|
222
229
|
title: phasePlan.title,
|
|
223
230
|
goal: phasePlan.goal,
|
|
@@ -229,11 +236,18 @@ announcePlan({
|
|
|
229
236
|
? ${JSON.stringify(`${input.name}-single`)}
|
|
230
237
|
: ${JSON.stringify(`${input.name}-`)} + phasePlan.id + "-" + phaseAgent.id
|
|
231
238
|
}))
|
|
232
|
-
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const firstPhasePlan = plannedPhaseFor(selectedPhases[0]);
|
|
242
|
+
announcePlan({
|
|
243
|
+
mode: plan.mode,
|
|
244
|
+
rationale: plan.rationale,
|
|
245
|
+
phases: [firstPhasePlan]
|
|
233
246
|
});
|
|
234
247
|
if (plan.mode === "single") {
|
|
235
|
-
const singlePhase =
|
|
248
|
+
const singlePhase = firstPhasePlan;
|
|
236
249
|
const singleAgent = singlePhase.agents[0];
|
|
250
|
+
announcePhasePlan(singlePhase);
|
|
237
251
|
phase(singlePhase.title);
|
|
238
252
|
return await agent([
|
|
239
253
|
"Single-agent execution selected by the LLM planner.",
|
|
@@ -253,7 +267,9 @@ if (plan.mode === "single") {
|
|
|
253
267
|
}
|
|
254
268
|
const phaseOutputs = [];
|
|
255
269
|
const priorSummaries = [];
|
|
256
|
-
for (const
|
|
270
|
+
for (const rawPhasePlan of selectedPhases) {
|
|
271
|
+
const phasePlan = plannedPhaseFor(rawPhasePlan);
|
|
272
|
+
announcePhasePlan(phasePlan);
|
|
257
273
|
phase(phasePlan.title);
|
|
258
274
|
const agents = phasePlan.agents;
|
|
259
275
|
const agentOutputs = agents.length < 2
|
|
@@ -398,11 +414,6 @@ export class WorkflowTaskRegistry {
|
|
|
398
414
|
}
|
|
399
415
|
}
|
|
400
416
|
catch (err) {
|
|
401
|
-
await cleanupWorkflowJournalTranscriptDir(transcriptDir).catch(() => undefined);
|
|
402
|
-
if (!resolved.scriptPath) {
|
|
403
|
-
await rm(scriptPath, { force: true }).catch(() => undefined);
|
|
404
|
-
await rm(workflowScriptMetadataPath(scriptPath), { force: true }).catch(() => undefined);
|
|
405
|
-
}
|
|
406
417
|
throw workflowJournalRequestError(err);
|
|
407
418
|
}
|
|
408
419
|
const task = {
|
|
@@ -970,9 +981,11 @@ export class WorkflowTaskRegistry {
|
|
|
970
981
|
controller.abort();
|
|
971
982
|
return;
|
|
972
983
|
}
|
|
973
|
-
const workflowTimer =
|
|
974
|
-
|
|
975
|
-
|
|
984
|
+
const workflowTimer = this.options.requestTimeoutMs > 0
|
|
985
|
+
? setTimeout(() => {
|
|
986
|
+
controller.abort();
|
|
987
|
+
}, this.options.requestTimeoutMs)
|
|
988
|
+
: null;
|
|
976
989
|
try {
|
|
977
990
|
if (controller.signal.aborted)
|
|
978
991
|
throw workflowInputError('Workflow is aborted.');
|
|
@@ -1002,9 +1015,7 @@ export class WorkflowTaskRegistry {
|
|
|
1002
1015
|
toolCalls: ctx.toolCalls,
|
|
1003
1016
|
durationMs: Date.now() - ctx.startedAt,
|
|
1004
1017
|
});
|
|
1005
|
-
|
|
1006
|
-
await rm(resultPath, { force: true }).catch(() => undefined);
|
|
1007
|
-
}
|
|
1018
|
+
void completedSnapshot;
|
|
1008
1019
|
}
|
|
1009
1020
|
catch (err) {
|
|
1010
1021
|
const abortFailure = controller.signal.aborted
|
|
@@ -1014,7 +1025,8 @@ export class WorkflowTaskRegistry {
|
|
|
1014
1025
|
await this.failTask(task, abortFailure ? abortFailure.message : workflowErrorMessage(err), abortFailure ? abortFailure.reason : workflowFailureReason(err));
|
|
1015
1026
|
}
|
|
1016
1027
|
finally {
|
|
1017
|
-
|
|
1028
|
+
if (workflowTimer)
|
|
1029
|
+
clearTimeout(workflowTimer);
|
|
1018
1030
|
for (const timer of ctx.timers.values())
|
|
1019
1031
|
clearTimeout(timer);
|
|
1020
1032
|
ctx.timers.clear();
|
|
@@ -1078,6 +1090,7 @@ export class WorkflowTaskRegistry {
|
|
|
1078
1090
|
return this.trackWorkflowPromise(ctx, this.workspaceContext(ctx, options));
|
|
1079
1091
|
});
|
|
1080
1092
|
host.announcePlan = hardenCallable((plan) => this.announcePlan(ctx, plan));
|
|
1093
|
+
host.announcePhasePlan = hardenCallable((phasePlan) => this.announcePhasePlan(ctx, phasePlan));
|
|
1081
1094
|
host.phase = hardenCallable((title) => this.phase(ctx, title));
|
|
1082
1095
|
host.log = hardenCallable(log);
|
|
1083
1096
|
host.consoleLog = hardenCallable((...values) => {
|
|
@@ -1215,6 +1228,7 @@ export class WorkflowTaskRegistry {
|
|
|
1215
1228
|
toolCalls: 0,
|
|
1216
1229
|
resultPreview: previewValue(cached.result, 160),
|
|
1217
1230
|
cached: true,
|
|
1231
|
+
...agentCompletionProgress(ctx, phase),
|
|
1218
1232
|
});
|
|
1219
1233
|
return cached.result;
|
|
1220
1234
|
}
|
|
@@ -1278,6 +1292,7 @@ export class WorkflowTaskRegistry {
|
|
|
1278
1292
|
tokens: usage.totalTokens,
|
|
1279
1293
|
toolCalls,
|
|
1280
1294
|
resultPreview: previewValue(journalResult, 160),
|
|
1295
|
+
...agentCompletionProgress(ctx, phase),
|
|
1281
1296
|
...preservedWorktreeEventProjection(preservedWorktrees),
|
|
1282
1297
|
});
|
|
1283
1298
|
return journalResult;
|
|
@@ -1343,7 +1358,6 @@ export class WorkflowTaskRegistry {
|
|
|
1343
1358
|
};
|
|
1344
1359
|
}
|
|
1345
1360
|
catch (err) {
|
|
1346
|
-
await rm(worktreePath, { recursive: true, force: true }).catch(() => undefined);
|
|
1347
1361
|
throw workflowInputError(`worktree isolation could not create an isolated worktree: ${workflowErrorMessage(err)}`);
|
|
1348
1362
|
}
|
|
1349
1363
|
}
|
|
@@ -1364,16 +1378,10 @@ export class WorkflowTaskRegistry {
|
|
|
1364
1378
|
preservedWorktree: preservedWorktree(worktree, 'changed'),
|
|
1365
1379
|
};
|
|
1366
1380
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
return {
|
|
1372
|
-
preserved: true,
|
|
1373
|
-
preservedWorktree: preservedWorktree(worktree, 'cleanup_failed'),
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
return { preserved: false };
|
|
1381
|
+
return {
|
|
1382
|
+
preserved: true,
|
|
1383
|
+
preservedWorktree: preservedWorktree(worktree, 'clean'),
|
|
1384
|
+
};
|
|
1377
1385
|
}
|
|
1378
1386
|
async runAgentWithStallRetries(ctx, input) {
|
|
1379
1387
|
for (let retryIndex = 0; retryIndex <= this.agentStallRetryLimit; retryIndex += 1) {
|
|
@@ -1448,10 +1456,12 @@ export class WorkflowTaskRegistry {
|
|
|
1448
1456
|
throw workflowInputError('Workflow is aborted.');
|
|
1449
1457
|
}
|
|
1450
1458
|
ctx.controller.signal.addEventListener('abort', abortFromWorkflow, { once: true });
|
|
1451
|
-
const timer =
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1459
|
+
const timer = this.agentStallTimeoutMs > 0
|
|
1460
|
+
? setTimeout(() => {
|
|
1461
|
+
timedOut = true;
|
|
1462
|
+
attemptController.abort();
|
|
1463
|
+
}, this.agentStallTimeoutMs)
|
|
1464
|
+
: null;
|
|
1455
1465
|
try {
|
|
1456
1466
|
const generated = this.options.backend.generate(request, attemptController.signal).then((result) => ({ type: 'result', result }), (err) => ({ type: 'error', error: err }));
|
|
1457
1467
|
const aborted = new Promise((resolve) => {
|
|
@@ -1474,7 +1484,8 @@ export class WorkflowTaskRegistry {
|
|
|
1474
1484
|
throw outcome.error;
|
|
1475
1485
|
}
|
|
1476
1486
|
finally {
|
|
1477
|
-
|
|
1487
|
+
if (timer)
|
|
1488
|
+
clearTimeout(timer);
|
|
1478
1489
|
ctx.controller.signal.removeEventListener('abort', abortFromWorkflow);
|
|
1479
1490
|
}
|
|
1480
1491
|
}
|
|
@@ -1529,6 +1540,7 @@ export class WorkflowTaskRegistry {
|
|
|
1529
1540
|
throw workflowInputError('Workflow is aborted.');
|
|
1530
1541
|
}
|
|
1531
1542
|
const normalized = normalizeWorkflowExecutionPlan(plan);
|
|
1543
|
+
ctx.announcedPlan = normalized;
|
|
1532
1544
|
this.emit(ctx.task, {
|
|
1533
1545
|
type: 'workflow.plan.ready',
|
|
1534
1546
|
taskId: ctx.task.taskId,
|
|
@@ -1536,22 +1548,54 @@ export class WorkflowTaskRegistry {
|
|
|
1536
1548
|
...normalized,
|
|
1537
1549
|
});
|
|
1538
1550
|
}
|
|
1551
|
+
announcePhasePlan(ctx, phasePlan) {
|
|
1552
|
+
if (ctx.controller.signal.aborted || ctx.task.status !== 'running') {
|
|
1553
|
+
throw workflowInputError('Workflow is aborted.');
|
|
1554
|
+
}
|
|
1555
|
+
const normalized = normalizeWorkflowPhasePlan(phasePlan, 'announcePhasePlan(phasePlan)');
|
|
1556
|
+
ctx.pendingPhasePlan = normalized;
|
|
1557
|
+
const phaseIndex = ctx.task.events
|
|
1558
|
+
.filter((event) => event.type === 'workflow.phase.started')
|
|
1559
|
+
.length;
|
|
1560
|
+
this.emit(ctx.task, {
|
|
1561
|
+
type: 'workflow.phase.planned',
|
|
1562
|
+
taskId: ctx.task.taskId,
|
|
1563
|
+
runId: ctx.task.runId,
|
|
1564
|
+
phaseIndex,
|
|
1565
|
+
title: normalized.title,
|
|
1566
|
+
...(normalized.goal ? { goal: normalized.goal } : {}),
|
|
1567
|
+
plannedAgentCount: normalized.agents.length,
|
|
1568
|
+
plannedAgents: normalized.agents,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1539
1571
|
phase(ctx, title) {
|
|
1540
1572
|
if (typeof title !== 'string' || title.trim() === '') {
|
|
1541
1573
|
throw workflowInputError('phase() requires a non-empty string title.');
|
|
1542
1574
|
}
|
|
1543
|
-
|
|
1575
|
+
const normalizedTitle = title.trim();
|
|
1576
|
+
ctx.currentPhase = normalizedTitle;
|
|
1544
1577
|
const phaseIndex = ctx.task.events
|
|
1545
1578
|
.filter((event) => event.type === 'workflow.phase.started')
|
|
1546
1579
|
.length;
|
|
1547
|
-
const detail = ctx.parsed.meta.phases?.find((item) => item.title ===
|
|
1580
|
+
const detail = ctx.parsed.meta.phases?.find((item) => item.title === normalizedTitle)?.detail;
|
|
1581
|
+
const pendingPhase = ctx.pendingPhasePlan?.title === normalizedTitle
|
|
1582
|
+
? ctx.pendingPhasePlan
|
|
1583
|
+
: undefined;
|
|
1584
|
+
if (pendingPhase)
|
|
1585
|
+
ctx.pendingPhasePlan = undefined;
|
|
1586
|
+
const plannedPhase = pendingPhase ?? workflowPlannedPhase(ctx.announcedPlan, phaseIndex, normalizedTitle);
|
|
1548
1587
|
this.emit(ctx.task, {
|
|
1549
1588
|
type: 'workflow.phase.started',
|
|
1550
1589
|
taskId: ctx.task.taskId,
|
|
1551
1590
|
runId: ctx.task.runId,
|
|
1552
1591
|
phaseIndex,
|
|
1553
|
-
title,
|
|
1592
|
+
title: normalizedTitle,
|
|
1554
1593
|
...(detail ? { detail } : {}),
|
|
1594
|
+
...(plannedPhase?.goal ? { goal: plannedPhase.goal } : {}),
|
|
1595
|
+
...(plannedPhase ? {
|
|
1596
|
+
plannedAgentCount: plannedPhase.agents.length,
|
|
1597
|
+
plannedAgents: plannedPhase.agents,
|
|
1598
|
+
} : {}),
|
|
1555
1599
|
});
|
|
1556
1600
|
}
|
|
1557
1601
|
async completeTask(ctx, result, event) {
|
|
@@ -1946,19 +1990,6 @@ function uniqueStrings(values) {
|
|
|
1946
1990
|
}
|
|
1947
1991
|
return out;
|
|
1948
1992
|
}
|
|
1949
|
-
async function removeCleanGitWorktree(worktree) {
|
|
1950
|
-
try {
|
|
1951
|
-
await gitOutput(worktree.gitRoot, ['worktree', 'remove', '--force', worktree.path]);
|
|
1952
|
-
}
|
|
1953
|
-
catch (err) {
|
|
1954
|
-
if (/No such file|not a working tree|is not a working tree/i.test(workflowErrorMessage(err))) {
|
|
1955
|
-
await rm(worktree.path, { recursive: true, force: true }).catch(() => undefined);
|
|
1956
|
-
await gitOutput(worktree.gitRoot, ['worktree', 'prune']).catch(() => undefined);
|
|
1957
|
-
return;
|
|
1958
|
-
}
|
|
1959
|
-
throw err;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
1993
|
function preservedWorktree(worktree, reason) {
|
|
1963
1994
|
return {
|
|
1964
1995
|
path: worktree.path,
|
|
@@ -1970,7 +2001,7 @@ function preservedWorktreeEventProjection(preservedWorktrees) {
|
|
|
1970
2001
|
if (preservedWorktrees.length === 0)
|
|
1971
2002
|
return {};
|
|
1972
2003
|
const primary = preservedWorktrees.find((item) => item.reason === 'changed')
|
|
1973
|
-
?? preservedWorktrees.find((item) => item.reason === '
|
|
2004
|
+
?? preservedWorktrees.find((item) => item.reason === 'status_unavailable')
|
|
1974
2005
|
?? preservedWorktrees[0];
|
|
1975
2006
|
return {
|
|
1976
2007
|
worktreePath: primary?.path,
|
|
@@ -1978,6 +2009,33 @@ function preservedWorktreeEventProjection(preservedWorktrees) {
|
|
|
1978
2009
|
preservedWorktrees: [...preservedWorktrees],
|
|
1979
2010
|
};
|
|
1980
2011
|
}
|
|
2012
|
+
function workflowPlannedPhase(plan, phaseIndex, title) {
|
|
2013
|
+
const indexed = plan?.phases[phaseIndex];
|
|
2014
|
+
if (indexed?.title === title)
|
|
2015
|
+
return indexed;
|
|
2016
|
+
return plan?.phases.find((phase) => phase.title === title);
|
|
2017
|
+
}
|
|
2018
|
+
function agentCompletionProgress(ctx, phase) {
|
|
2019
|
+
const completedAgentCount = ctx.task.events
|
|
2020
|
+
.filter((event) => event.type === 'workflow.agent.completed')
|
|
2021
|
+
.length + 1;
|
|
2022
|
+
const base = {
|
|
2023
|
+
elapsedMs: Date.now() - ctx.startedAt,
|
|
2024
|
+
completedAgentCount,
|
|
2025
|
+
knownAgentCount: ctx.agentCount,
|
|
2026
|
+
};
|
|
2027
|
+
if (!phase)
|
|
2028
|
+
return base;
|
|
2029
|
+
const phaseCompletedAgentCount = ctx.task.events
|
|
2030
|
+
.filter((event) => event.type === 'workflow.agent.completed' && event.phase === phase)
|
|
2031
|
+
.length + 1;
|
|
2032
|
+
const phaseKnownAgentCount = Math.max(phaseCompletedAgentCount, ctx.task.events.filter((event) => event.type === 'workflow.agent.started' && event.phase === phase).length);
|
|
2033
|
+
return {
|
|
2034
|
+
...base,
|
|
2035
|
+
phaseCompletedAgentCount,
|
|
2036
|
+
phaseKnownAgentCount,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
1981
2039
|
function shortHash(value) {
|
|
1982
2040
|
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
1983
2041
|
}
|
|
@@ -2684,6 +2742,8 @@ function normalizeAgentStallTimeoutMs(configured, requestTimeoutMs) {
|
|
|
2684
2742
|
if (configured !== undefined && Number.isFinite(configured) && configured > 0) {
|
|
2685
2743
|
return Math.max(1, Math.floor(configured));
|
|
2686
2744
|
}
|
|
2745
|
+
if (configured === 0 || requestTimeoutMs === 0)
|
|
2746
|
+
return 0;
|
|
2687
2747
|
return Math.max(1, Math.floor(requestTimeoutMs));
|
|
2688
2748
|
}
|
|
2689
2749
|
function workflowTaskSnapshot(task) {
|
|
@@ -2972,6 +3032,7 @@ function installWorkflowVmGlobals(context, globals) {
|
|
|
2972
3032
|
' define(globalThis, "pipeline", { value: (...values) => __host.pipeline(...values), writable: false, configurable: false });',
|
|
2973
3033
|
' define(globalThis, "workspaceContext", { value: (...values) => __host.workspaceContext(...values), writable: false, configurable: false });',
|
|
2974
3034
|
' define(globalThis, "announcePlan", { value: (...values) => __host.announcePlan(...values), writable: false, configurable: false });',
|
|
3035
|
+
' define(globalThis, "announcePhasePlan", { value: (...values) => __host.announcePhasePlan(...values), writable: false, configurable: false });',
|
|
2975
3036
|
' define(globalThis, "phase", { value: (...values) => __host.phase(...values), writable: false, configurable: false });',
|
|
2976
3037
|
' define(globalThis, "log", { value: (...values) => __host.log(...values), writable: false, configurable: false });',
|
|
2977
3038
|
' define(globalThis, "workflow", { value: (...values) => __host.workflow(...values), writable: false, configurable: false });',
|
|
@@ -3664,38 +3725,41 @@ function normalizeWorkflowExecutionPlan(value) {
|
|
|
3664
3725
|
const rawPhases = Array.isArray(record.phases) ? Array.from(record.phases) : [];
|
|
3665
3726
|
if (rawPhases.length === 0)
|
|
3666
3727
|
throw workflowInputError('announcePlan(plan) requires at least one phase.');
|
|
3667
|
-
const phases = rawPhases
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
throw workflowInputError(`announcePlan(plan).phases[${phaseIndex}] must be an object.`);
|
|
3671
|
-
const rawAgents = Array.isArray(phase.agents) ? Array.from(phase.agents) : [];
|
|
3672
|
-
if (rawAgents.length === 0) {
|
|
3673
|
-
throw workflowInputError(`announcePlan(plan).phases[${phaseIndex}].agents requires at least one agent.`);
|
|
3674
|
-
}
|
|
3675
|
-
return {
|
|
3676
|
-
...(typeof phase.id === 'string' && phase.id.trim() ? { id: boundedPlanString(phase.id, '', 48) } : {}),
|
|
3677
|
-
title: boundedPlanString(phase.title, `Phase ${phaseIndex + 1}`, 96),
|
|
3678
|
-
...(typeof phase.goal === 'string' && phase.goal.trim() ? { goal: boundedPlanString(phase.goal, '', 600) } : {}),
|
|
3679
|
-
agents: rawAgents.slice(0, 16).map((agentValue, agentIndex) => {
|
|
3680
|
-
const agent = asRecord(agentValue);
|
|
3681
|
-
if (!agent) {
|
|
3682
|
-
throw workflowInputError(`announcePlan(plan).phases[${phaseIndex}].agents[${agentIndex}] must be an object.`);
|
|
3683
|
-
}
|
|
3684
|
-
return {
|
|
3685
|
-
...(typeof agent.id === 'string' && agent.id.trim() ? { id: boundedPlanString(agent.id, '', 48) } : {}),
|
|
3686
|
-
title: boundedPlanString(agent.title, `Agent ${agentIndex + 1}`, 96),
|
|
3687
|
-
...(typeof agent.focus === 'string' && agent.focus.trim() ? { focus: boundedPlanString(agent.focus, '', 600) } : {}),
|
|
3688
|
-
...(typeof agent.label === 'string' && agent.label.trim() ? { label: boundedPlanString(agent.label, '', 96) } : {}),
|
|
3689
|
-
};
|
|
3690
|
-
}),
|
|
3691
|
-
};
|
|
3692
|
-
});
|
|
3728
|
+
const phases = rawPhases
|
|
3729
|
+
.slice(0, 16)
|
|
3730
|
+
.map((phaseValue, phaseIndex) => normalizeWorkflowPhasePlan(phaseValue, `announcePlan(plan).phases[${phaseIndex}]`, phaseIndex));
|
|
3693
3731
|
return {
|
|
3694
3732
|
mode,
|
|
3695
3733
|
...(rationale ? { rationale } : {}),
|
|
3696
3734
|
phases,
|
|
3697
3735
|
};
|
|
3698
3736
|
}
|
|
3737
|
+
function normalizeWorkflowPhasePlan(value, label, phaseIndex = 0) {
|
|
3738
|
+
const phase = asRecord(value);
|
|
3739
|
+
if (!phase)
|
|
3740
|
+
throw workflowInputError(`${label} must be an object.`);
|
|
3741
|
+
const rawAgents = Array.isArray(phase.agents) ? Array.from(phase.agents) : [];
|
|
3742
|
+
if (rawAgents.length === 0) {
|
|
3743
|
+
throw workflowInputError(`${label}.agents requires at least one agent.`);
|
|
3744
|
+
}
|
|
3745
|
+
return {
|
|
3746
|
+
...(typeof phase.id === 'string' && phase.id.trim() ? { id: boundedPlanString(phase.id, '', 48) } : {}),
|
|
3747
|
+
title: boundedPlanString(phase.title, `Phase ${phaseIndex + 1}`, 96),
|
|
3748
|
+
...(typeof phase.goal === 'string' && phase.goal.trim() ? { goal: boundedPlanString(phase.goal, '', 600) } : {}),
|
|
3749
|
+
agents: rawAgents.slice(0, 16).map((agentValue, agentIndex) => {
|
|
3750
|
+
const agent = asRecord(agentValue);
|
|
3751
|
+
if (!agent) {
|
|
3752
|
+
throw workflowInputError(`${label}.agents[${agentIndex}] must be an object.`);
|
|
3753
|
+
}
|
|
3754
|
+
return {
|
|
3755
|
+
...(typeof agent.id === 'string' && agent.id.trim() ? { id: boundedPlanString(agent.id, '', 48) } : {}),
|
|
3756
|
+
title: boundedPlanString(agent.title, `Agent ${agentIndex + 1}`, 96),
|
|
3757
|
+
...(typeof agent.focus === 'string' && agent.focus.trim() ? { focus: boundedPlanString(agent.focus, '', 600) } : {}),
|
|
3758
|
+
...(typeof agent.label === 'string' && agent.label.trim() ? { label: boundedPlanString(agent.label, '', 96) } : {}),
|
|
3759
|
+
};
|
|
3760
|
+
}),
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3699
3763
|
function boundedPlanString(value, fallback, limit) {
|
|
3700
3764
|
const text = typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
3701
3765
|
return preview(text, limit);
|
package/dist/settings.js
CHANGED
|
@@ -34,7 +34,7 @@ export function loadSettings() {
|
|
|
34
34
|
progress: readWorkflowProgressModeSetting(workflow?.progress, 'workflow.progress'),
|
|
35
35
|
permission: readWorkflowPermissionPolicySetting(workflow?.permission, 'workflow.permission'),
|
|
36
36
|
retryLimit: readNonNegativeIntegerSetting(workflow?.retryLimit, 'workflow.retryLimit'),
|
|
37
|
-
timeoutMs:
|
|
37
|
+
timeoutMs: readNonNegativeIntegerSetting(workflow?.timeoutMs, 'workflow.timeoutMs'),
|
|
38
38
|
background: {
|
|
39
39
|
runDir: readTemplateSetting(background?.runDir, 'workflow.background.runDir', true),
|
|
40
40
|
resultFile: readRelativePathSetting(background?.resultFile, 'workflow.background.resultFile'),
|
|
@@ -119,11 +119,6 @@ function readNonNegativeIntegerSetting(value, key) {
|
|
|
119
119
|
return value;
|
|
120
120
|
throw new Error(`${key} must be a non-negative integer.`);
|
|
121
121
|
}
|
|
122
|
-
function readPositiveIntegerSetting(value, key) {
|
|
123
|
-
if (typeof value === 'number' && Number.isInteger(value) && value > 0)
|
|
124
|
-
return value;
|
|
125
|
-
throw new Error(`${key} must be a positive integer.`);
|
|
126
|
-
}
|
|
127
122
|
function readTemplateSetting(value, key, requireJobId) {
|
|
128
123
|
const text = readNonEmptyStringSetting(value, key);
|
|
129
124
|
if (requireJobId && !text.includes('{jobId}')) {
|
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.2.
|
|
10
|
+
- generated npm package contents for `ultracode-for-codex@0.2.6`;
|
|
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,7 @@ 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.
|
|
26
|
+
- package version is prepared as `0.2.6`.
|
|
27
27
|
|
|
28
28
|
## Evidence
|
|
29
29
|
|
|
@@ -11,8 +11,8 @@ P3-C is done when:
|
|
|
11
11
|
|
|
12
12
|
- the runtime creates a detached git worktree before the backend call;
|
|
13
13
|
- the backend turn runs with that worktree as its workspace;
|
|
14
|
-
-
|
|
15
|
-
|
|
14
|
+
- isolated worktrees are preserved after the agent finishes, including clean,
|
|
15
|
+
changed, stalled, aborted, or status-unavailable worktrees;
|
|
16
16
|
- `semanticOpts.isolation` participates in the agent cache key.
|
|
17
17
|
|
|
18
18
|
## Authority Model
|
|
@@ -22,8 +22,8 @@ P3-C is done when:
|
|
|
22
22
|
| isolation request | workflow script | Only `isolation: "worktree"` is accepted. |
|
|
23
23
|
| worktree path | runtime | Runtime creates paths outside the source repo working tree. |
|
|
24
24
|
| backend cwd | runtime request packet | Subagent backend executes the turn in `worktreePath`. |
|
|
25
|
-
| changed/unchanged decision | runtime git status | `git status --porcelain --untracked-files=all --ignored=matching` decides
|
|
26
|
-
| preserved path projection | runtime event |
|
|
25
|
+
| changed/unchanged decision | runtime git status | `git status --porcelain --untracked-files=all --ignored=matching` decides the preservation reason. |
|
|
26
|
+
| preserved path projection | runtime event | Preserved worktrees are surfaced on agent final events. |
|
|
27
27
|
|
|
28
28
|
The accepted isolation values are `"none"` and `"worktree"`; any other value
|
|
29
29
|
fails as invalid workflow input.
|
|
@@ -48,13 +48,13 @@ copied into the isolated worktree.
|
|
|
48
48
|
4. Pass `worktreePath` to the subagent backend and append path-free worktree
|
|
49
49
|
context to the backend prompt.
|
|
50
50
|
5. Inspect worktree status after the attempt settles.
|
|
51
|
-
6.
|
|
52
|
-
|
|
53
|
-
worktrees and surface them on the agent final event.
|
|
51
|
+
6. Preserve the worktree with reason `clean`, `changed`, `stalled`, `aborted`,
|
|
52
|
+
or `status_unavailable` and surface it on the agent final event.
|
|
54
53
|
|
|
55
54
|
## Verification
|
|
56
55
|
|
|
57
56
|
- `test/codex-isolation.test.mjs` verifies Codex backend request projection.
|
|
58
|
-
- `test/workflow-runtime.test.mjs` verifies changed worktree
|
|
57
|
+
- `test/workflow-runtime.test.mjs` verifies clean and changed worktree
|
|
58
|
+
preservation.
|
|
59
59
|
- `scripts/e2e-installed-ultracode-for-codex.mjs` verifies packaged CLI workflow
|
|
60
60
|
execution through the fake Codex app-server boundary.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultracode-for-codex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Run local Codex-backed workflows from a command-owned CLI runtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"dist/ultracode-install-guide.d.ts",
|
|
37
37
|
"dist/settings.js",
|
|
38
38
|
"dist/settings.d.ts",
|
|
39
|
+
"dist/runtime/package-info.js",
|
|
40
|
+
"dist/runtime/package-info.d.ts",
|
|
39
41
|
"dist/codex/",
|
|
40
42
|
"dist/runtime/",
|
|
41
43
|
"skills/ultracode-for-codex/",
|
package/settings.json
CHANGED
|
@@ -13,9 +13,10 @@ binary, tests, package exports, journal layer, and workflow runtime code.
|
|
|
13
13
|
|
|
14
14
|
Workflow execution runs through the local CLI command. Progress,
|
|
15
15
|
cancellation, permission review, retry, and result projection stay in that
|
|
16
|
-
command process. `settings.json` defaults runs to OS background execution
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
command process. `settings.json` defaults runs to OS background execution; use
|
|
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.
|
|
19
20
|
|
|
20
21
|
The default Ultracode work shape is phase-wise parallel execution: built-in
|
|
21
22
|
`task` and `code-review` first call a planner agent, then execute each planned
|
|
@@ -44,25 +45,33 @@ For source-checkout validation before publish:
|
|
|
44
45
|
|
|
45
46
|
```bash
|
|
46
47
|
npm run pack:ultracode-for-codex
|
|
47
|
-
npm install --save-dev ./artifacts/ultracode-for-codex-0.2.
|
|
48
|
+
npm install --save-dev ./artifacts/ultracode-for-codex-0.2.6.tgz
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
CLI behavior:
|
|
51
52
|
|
|
53
|
+
- `--version` or `-v` prints the installed package version;
|
|
52
54
|
- default execution is `background`; stdout contains a launch record with
|
|
53
55
|
`jobId`, `pid`, `resultPath`, `progressPath`, `metadataPath`, and `pidPath`;
|
|
54
|
-
- attached execution is available with `--execution attached
|
|
56
|
+
- attached execution is available with `--execution attached` when the caller
|
|
57
|
+
should stay connected until completion;
|
|
55
58
|
- attached progress prints to stderr as JSONL by default;
|
|
56
59
|
- attached final workflow result prints as JSON to stdout;
|
|
57
60
|
- JSONL records include `kind`, `version`, `event`, `status`, and `summary`,
|
|
58
61
|
with agent identity and label fields on agent records;
|
|
59
|
-
- built-in `task` and `code-review` emit `workflow.plan.ready`
|
|
60
|
-
|
|
62
|
+
- built-in `task` and `code-review` emit `workflow.plan.ready` as a planning
|
|
63
|
+
snapshot, not a promise that every later phase is already known;
|
|
64
|
+
- `workflow.phase.planned` is emitted immediately before each phase starts and
|
|
65
|
+
carries that phase's current planned agent role labels;
|
|
66
|
+
- each `workflow.phase.started` record repeats the same role labels when the
|
|
67
|
+
phase begins;
|
|
68
|
+
- each `workflow.agent.completed` record includes phase progress, total known
|
|
69
|
+
agent progress, and elapsed time;
|
|
61
70
|
- `Ctrl-C` cancels the active attached workflow;
|
|
62
71
|
- `--retry-limit <n>` retries failed workflows inside the same process;
|
|
63
|
-
- `--timeout-ms`
|
|
64
|
-
|
|
65
|
-
divided by the retry budget.
|
|
72
|
+
- `--timeout-ms 0` waits for completion, cancellation, or app-server exit;
|
|
73
|
+
positive values opt into a workflow deadline and per-agent silence budget,
|
|
74
|
+
and that budget is not divided by the retry budget.
|
|
66
75
|
- `--permission ask|allow|deny` handles project/user/plugin/scriptPath reviews.
|
|
67
76
|
- `--progress plain` switches to human-readable progress lines.
|
|
68
77
|
- background file locations are controlled by `workflow.background` in
|
|
@@ -77,14 +86,15 @@ CLI behavior:
|
|
|
77
86
|
- Built-in `task` and `code-review` inject deterministic workspace context into
|
|
78
87
|
planner-selected phase-wise parallel subagents.
|
|
79
88
|
- Keep workflow execution local and command-owned; settings default to OS
|
|
80
|
-
background execution
|
|
81
|
-
|
|
89
|
+
background execution so long runs can keep waiting while Codex does other
|
|
90
|
+
work.
|
|
82
91
|
- Keep `journalPath`, `journal.jsonl`, and journal contents out of CLI output.
|
|
83
92
|
- Treat `.ultracode-for-codex` workflow state as sensitive local data.
|
|
84
93
|
- Keep `resumeFromRunId` runtime-internal unless cross-process resume
|
|
85
94
|
gets an explicit durable design.
|
|
86
95
|
- Use `isolation: "worktree"` only inside a git repo with at least one commit;
|
|
87
|
-
|
|
96
|
+
isolated worktrees are intentionally preserved for review, including clean
|
|
97
|
+
worktrees.
|
|
88
98
|
|
|
89
99
|
## Packaging And Verification
|
|
90
100
|
|