ultracode-for-codex 0.2.4 → 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 +28 -9
- package/ULTRACODE_INSTALL.md +29 -10
- package/dist/cli.js +97 -6
- 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 +39 -1
- package/dist/runtime/workflow-runtime.js +208 -51
- 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 +26 -10
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:
|
|
@@ -68,6 +68,10 @@ The built-in `task` and `code-review` workflows use an LLM planner first, then
|
|
|
68
68
|
run work phase by phase. Within each phase, multiple focused Codex subagents run
|
|
69
69
|
in parallel by default, followed by phase and final synthesis. The planner may
|
|
70
70
|
choose a single-agent path only when parallel execution would add risk or waste.
|
|
71
|
+
Planner guidance includes dynamic workflow patterns such as classify-and-act,
|
|
72
|
+
fan-out-and-synthesize, adversarial verification, generate-and-filter,
|
|
73
|
+
tournament, and loop-until-done, so different work types can use different
|
|
74
|
+
phase shapes.
|
|
71
75
|
|
|
72
76
|
## Settings
|
|
73
77
|
|
|
@@ -80,7 +84,7 @@ Package defaults live in `settings.json`:
|
|
|
80
84
|
"progress": "jsonl",
|
|
81
85
|
"permission": "ask",
|
|
82
86
|
"retryLimit": 0,
|
|
83
|
-
"timeoutMs":
|
|
87
|
+
"timeoutMs": 0,
|
|
84
88
|
"background": {
|
|
85
89
|
"runDir": ".ultracode-for-codex/background/{jobId}",
|
|
86
90
|
"resultFile": "result.json",
|
|
@@ -94,22 +98,38 @@ Package defaults live in `settings.json`:
|
|
|
94
98
|
|
|
95
99
|
Use `--execution attached`, `--progress`, `--permission`, `--retry-limit`, and
|
|
96
100
|
`--timeout-ms` to override settings for one run.
|
|
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.
|
|
97
107
|
|
|
98
108
|
## CLI Controls
|
|
99
109
|
|
|
110
|
+
- Use `--version` or `-v` to print the installed package version.
|
|
100
111
|
- Progress is printed to stderr as JSONL by default.
|
|
101
112
|
- The final workflow result is printed as JSON to stdout.
|
|
102
113
|
- JSONL records include `kind`, `version`, `event`, `status`, and `summary`;
|
|
103
114
|
agent records also include stable agent identity and label fields.
|
|
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.
|
|
104
123
|
- Press `Ctrl-C` once to cancel the active workflow.
|
|
105
124
|
- Use `--retry-limit <n>` to retry failed workflows inside the same process.
|
|
106
|
-
- `--timeout-ms`
|
|
107
|
-
|
|
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.
|
|
108
128
|
- Use `--permission ask|allow|deny` for project/user/plugin/scriptPath workflow
|
|
109
129
|
permission reviews.
|
|
110
130
|
- Use `--progress plain` for human-readable log lines.
|
|
111
131
|
- Use `--execution background` for OS background runs and `--execution attached`
|
|
112
|
-
when
|
|
132
|
+
only when the caller should stay connected until completion.
|
|
113
133
|
|
|
114
134
|
## Codex Companion Skill
|
|
115
135
|
|
|
@@ -131,8 +151,8 @@ want Codex to auto-load the package boundaries and verification routine.
|
|
|
131
151
|
planner-selected phase-wise parallel subagents, then synthesize each phase and
|
|
132
152
|
the final result.
|
|
133
153
|
- Workflow execution is local and command-owned; settings default to OS
|
|
134
|
-
background execution
|
|
135
|
-
|
|
154
|
+
background execution so long runs can keep waiting while Codex does other
|
|
155
|
+
work.
|
|
136
156
|
- `.ultracode-for-codex` workflow state is sensitive local data.
|
|
137
157
|
- `journalPath`, `journal.jsonl`, and journal contents stay out of CLI output.
|
|
138
158
|
Local runtime state may still contain runtime-owned
|
|
@@ -140,8 +160,7 @@ want Codex to auto-load the package boundaries and verification routine.
|
|
|
140
160
|
- `resumeFromRunId` remains runtime-internal and same-session; users retry the
|
|
141
161
|
active run or rerun the workflow command.
|
|
142
162
|
- `agent(..., { isolation: "worktree" })` runs the agent in a detached git
|
|
143
|
-
worktree
|
|
144
|
-
worktrees for review.
|
|
163
|
+
worktree and preserves the worktree for review, including clean worktrees.
|
|
145
164
|
|
|
146
165
|
## Development
|
|
147
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,14 +55,19 @@ 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
|
|
63
65
|
Codex subagents in parallel within each phase by default, and synthesize phase
|
|
64
66
|
and final results. The planner chooses a single-agent path only when parallel
|
|
65
67
|
execution would add risk or waste.
|
|
68
|
+
Planner guidance includes classify-and-act, fan-out-and-synthesize,
|
|
69
|
+
adversarial verification, generate-and-filter, tournament, and loop-until-done
|
|
70
|
+
patterns so different work types can use different phase shapes.
|
|
66
71
|
|
|
67
72
|
Settings defaults:
|
|
68
73
|
|
|
@@ -73,7 +78,7 @@ Settings defaults:
|
|
|
73
78
|
"progress": "jsonl",
|
|
74
79
|
"permission": "ask",
|
|
75
80
|
"retryLimit": 0,
|
|
76
|
-
"timeoutMs":
|
|
81
|
+
"timeoutMs": 0,
|
|
77
82
|
"background": {
|
|
78
83
|
"runDir": ".ultracode-for-codex/background/{jobId}",
|
|
79
84
|
"resultFile": "result.json",
|
|
@@ -87,26 +92,38 @@ Settings defaults:
|
|
|
87
92
|
|
|
88
93
|
Useful controls:
|
|
89
94
|
|
|
95
|
+
- `--version` or `-v` prints the installed package version.
|
|
90
96
|
- Progress events are printed to stderr as JSONL by default.
|
|
91
97
|
- The final workflow result is printed as JSON to stdout.
|
|
98
|
+
- The package default workflow timeout is `0`, meaning the workflow waits until
|
|
99
|
+
it completes, is cancelled, or the Codex app-server exits.
|
|
92
100
|
- JSONL records include `kind`, `version`, `event`, `status`, and `summary`;
|
|
93
101
|
agent records also include stable agent identity and label fields.
|
|
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.
|
|
94
110
|
- Press `Ctrl-C` once to cancel the running workflow.
|
|
95
111
|
- Use `--retry-limit <n>` to retry failed runs in the same process.
|
|
96
|
-
- `--timeout-ms`
|
|
97
|
-
|
|
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.
|
|
98
115
|
- Use `--permission ask|allow|deny` for project/user/plugin/scriptPath
|
|
99
116
|
workflow permission reviews.
|
|
100
117
|
- Use `--progress plain` for human-readable log lines.
|
|
101
118
|
- Use `--execution background` for OS background runs and `--execution attached`
|
|
102
|
-
|
|
119
|
+
only when the caller should stay connected until completion.
|
|
103
120
|
|
|
104
121
|
## Runtime Contract
|
|
105
122
|
|
|
106
123
|
- Use Codex app-server over stdio as the production backend.
|
|
107
124
|
- Keep workflow execution local and command-owned; settings default to OS
|
|
108
|
-
background execution
|
|
109
|
-
|
|
125
|
+
background execution so long runs can keep waiting while Codex does other
|
|
126
|
+
work.
|
|
110
127
|
- Route progress, cancellation, permission review, retry, and result projection
|
|
111
128
|
through the CLI command.
|
|
112
129
|
- Keep stdout reserved for the final JSON result; stream progress records to
|
|
@@ -124,13 +141,15 @@ Useful controls:
|
|
|
124
141
|
- `resumeFromRunId` remains a runtime-internal same-session capability; the
|
|
125
142
|
CLI uses retry or explicit reruns for user-facing recovery.
|
|
126
143
|
- Use `isolation: "worktree"` only in git repositories with at least one commit.
|
|
127
|
-
|
|
144
|
+
Isolated worktrees are intentionally preserved for review, including clean
|
|
145
|
+
worktrees.
|
|
128
146
|
- Treat `.ultracode-for-codex` workflow state as sensitive local data.
|
|
129
147
|
|
|
130
148
|
## First Checks After Install
|
|
131
149
|
|
|
132
150
|
```bash
|
|
133
151
|
npm exec -- ultracode-for-codex --help
|
|
152
|
+
npm exec -- ultracode-for-codex --version
|
|
134
153
|
npm exec -- ultracode-for-codex --llm-guide
|
|
135
154
|
```
|
|
136
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,28 @@ 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
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
case 'workflow.plan.ready':
|
|
370
|
+
process.stderr.write(`[plan] mode=${event.mode} phases=${event.phases.length}${event.rationale ? ` - ${event.rationale}` : ''}\n`);
|
|
371
|
+
for (const [phaseIndex, phase] of event.phases.entries()) {
|
|
372
|
+
process.stderr.write(`[plan] ${phaseIndex + 1}. ${phase.title}${phase.goal ? ` - ${phase.goal}` : ''}\n`);
|
|
373
|
+
for (const agent of phase.agents) {
|
|
374
|
+
process.stderr.write(`[plan] - ${agent.title}${agent.label ? ` (${agent.label})` : ''}${agent.focus ? `: ${agent.focus}` : ''}\n`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
352
377
|
return;
|
|
353
378
|
case 'workflow.log':
|
|
354
379
|
process.stderr.write(`[log] ${event.message}\n`);
|
|
@@ -357,7 +382,7 @@ function renderWorkflowEvent(event, progressMode) {
|
|
|
357
382
|
process.stderr.write(`[agent:${event.agentIndex + 1}] started ${event.label}\n`);
|
|
358
383
|
return;
|
|
359
384
|
case 'workflow.agent.completed':
|
|
360
|
-
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`);
|
|
361
386
|
return;
|
|
362
387
|
case 'workflow.agent.failed':
|
|
363
388
|
process.stderr.write(`[agent:${event.agentIndex + 1}] failed ${event.label} ${event.error}\n`);
|
|
@@ -400,6 +425,38 @@ function writeJsonlProgress(payload) {
|
|
|
400
425
|
...payload,
|
|
401
426
|
})}\n`);
|
|
402
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
|
+
}
|
|
403
460
|
function progressPayloadForEvent(event) {
|
|
404
461
|
switch (event.type) {
|
|
405
462
|
case 'workflow.started':
|
|
@@ -414,16 +471,44 @@ function progressPayloadForEvent(event) {
|
|
|
414
471
|
workflowSourcePath: event.workflowSourcePath,
|
|
415
472
|
scriptHash: event.scriptHash,
|
|
416
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
|
+
};
|
|
417
487
|
case 'workflow.phase.started':
|
|
418
488
|
return {
|
|
419
489
|
event: event.type,
|
|
420
490
|
status: 'running',
|
|
421
|
-
summary: event
|
|
491
|
+
summary: phaseStartedSummary(event),
|
|
422
492
|
taskId: event.taskId,
|
|
423
493
|
runId: event.runId,
|
|
424
494
|
phaseIndex: event.phaseIndex,
|
|
425
495
|
title: event.title,
|
|
426
496
|
detail: event.detail,
|
|
497
|
+
goal: event.goal,
|
|
498
|
+
plannedAgentCount: event.plannedAgentCount,
|
|
499
|
+
plannedAgents: event.plannedAgents,
|
|
500
|
+
};
|
|
501
|
+
case 'workflow.plan.ready':
|
|
502
|
+
return {
|
|
503
|
+
event: event.type,
|
|
504
|
+
status: 'planned',
|
|
505
|
+
summary: `Workflow planning snapshot: ${event.phases.length} known phase${event.phases.length === 1 ? '' : 's'}, mode=${event.mode}`,
|
|
506
|
+
taskId: event.taskId,
|
|
507
|
+
runId: event.runId,
|
|
508
|
+
mode: event.mode,
|
|
509
|
+
rationale: event.rationale,
|
|
510
|
+
phaseCount: event.phases.length,
|
|
511
|
+
planPhases: event.phases,
|
|
427
512
|
};
|
|
428
513
|
case 'workflow.log':
|
|
429
514
|
return {
|
|
@@ -451,7 +536,7 @@ function progressPayloadForEvent(event) {
|
|
|
451
536
|
return {
|
|
452
537
|
event: event.type,
|
|
453
538
|
status: 'completed',
|
|
454
|
-
summary: `Agent ${event.agentIndex + 1} completed`,
|
|
539
|
+
summary: `Agent ${event.agentIndex + 1} completed: ${event.label}. ${agentCompletionProgressSummary(event)}`,
|
|
455
540
|
taskId: event.taskId,
|
|
456
541
|
runId: event.runId,
|
|
457
542
|
agentIndex: event.agentIndex,
|
|
@@ -462,6 +547,11 @@ function progressPayloadForEvent(event) {
|
|
|
462
547
|
toolCalls: event.toolCalls,
|
|
463
548
|
resultPreview: event.resultPreview,
|
|
464
549
|
cached: event.cached,
|
|
550
|
+
elapsedMs: event.elapsedMs,
|
|
551
|
+
completedAgentCount: event.completedAgentCount,
|
|
552
|
+
knownAgentCount: event.knownAgentCount,
|
|
553
|
+
phaseCompletedAgentCount: event.phaseCompletedAgentCount,
|
|
554
|
+
phaseKnownAgentCount: event.phaseKnownAgentCount,
|
|
465
555
|
worktreePreserved: event.worktreePreserved,
|
|
466
556
|
preservedWorktrees: event.preservedWorktrees,
|
|
467
557
|
};
|
|
@@ -516,7 +606,7 @@ function parseIntOption(value, fallback) {
|
|
|
516
606
|
if (value === undefined)
|
|
517
607
|
return fallback;
|
|
518
608
|
const parsed = Number.parseInt(value, 10);
|
|
519
|
-
if (!Number.isFinite(parsed) || parsed
|
|
609
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
520
610
|
return fallback;
|
|
521
611
|
return parsed;
|
|
522
612
|
}
|
|
@@ -575,6 +665,7 @@ Commands:
|
|
|
575
665
|
run Run a workflow as a local CLI command.
|
|
576
666
|
|
|
577
667
|
Options:
|
|
668
|
+
--version, -v Print the package version.
|
|
578
669
|
--llm-guide Print the Ultracode install and usage guide.
|
|
579
670
|
--accept-llm-guide <version> Required for run. Current version: ${ULTRACODE_INSTALL_GUIDE_ACCEPT_VERSION}.
|
|
580
671
|
--script <js> Inline workflow script.
|
|
@@ -589,7 +680,7 @@ Options:
|
|
|
589
680
|
--execution <background|attached> Execution mode. Default: settings.json (${workflowDefaultExecutionMode()}).
|
|
590
681
|
--command <path> Override Codex CLI binary path.
|
|
591
682
|
--model <model> Pass a model to Codex app-server.
|
|
592
|
-
--timeout-ms <number> Runtime timeout. Default: settings.json (${workflowDefaultTimeoutMs()}).
|
|
683
|
+
--timeout-ms <number> Runtime timeout; 0 waits for completion/cancel. Default: settings.json (${workflowDefaultTimeoutMs()}).
|
|
593
684
|
--cwd <dir> Working directory for workflow execution. Default: current cwd.
|
|
594
685
|
--reasoning-effort <effort> Codex reasoning effort. Default: settings.json (${codexDefaultReasoningEffort()}).
|
|
595
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.`);
|
|
@@ -4,6 +4,18 @@ export type WorkflowTaskStatus = 'running' | 'completed' | 'failed';
|
|
|
4
4
|
export type WorkflowTaskType = 'local_workflow';
|
|
5
5
|
export type WorkflowSource = 'inline' | 'script_path' | 'project' | 'user' | 'plugin' | 'built_in';
|
|
6
6
|
export type WorkflowPermissionDecision = 'allow' | 'deny';
|
|
7
|
+
export interface WorkflowPlanAgent {
|
|
8
|
+
readonly id?: string;
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly focus?: string;
|
|
11
|
+
readonly label?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface WorkflowPlanPhase {
|
|
14
|
+
readonly id?: string;
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly goal?: string;
|
|
17
|
+
readonly agents: readonly WorkflowPlanAgent[];
|
|
18
|
+
}
|
|
7
19
|
export type WorkflowEvent = {
|
|
8
20
|
readonly type: 'workflow.started';
|
|
9
21
|
readonly taskId: string;
|
|
@@ -20,6 +32,25 @@ export type WorkflowEvent = {
|
|
|
20
32
|
readonly phaseIndex: number;
|
|
21
33
|
readonly title: string;
|
|
22
34
|
readonly detail?: string;
|
|
35
|
+
readonly goal?: string;
|
|
36
|
+
readonly plannedAgentCount?: number;
|
|
37
|
+
readonly plannedAgents?: readonly WorkflowPlanAgent[];
|
|
38
|
+
} | {
|
|
39
|
+
readonly type: 'workflow.plan.ready';
|
|
40
|
+
readonly taskId: string;
|
|
41
|
+
readonly runId: string;
|
|
42
|
+
readonly mode: string;
|
|
43
|
+
readonly rationale?: string;
|
|
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[];
|
|
23
54
|
} | {
|
|
24
55
|
readonly type: 'workflow.log';
|
|
25
56
|
readonly taskId: string;
|
|
@@ -46,6 +77,11 @@ export type WorkflowEvent = {
|
|
|
46
77
|
readonly toolCalls: number;
|
|
47
78
|
readonly resultPreview?: string;
|
|
48
79
|
readonly cached?: boolean;
|
|
80
|
+
readonly elapsedMs: number;
|
|
81
|
+
readonly completedAgentCount: number;
|
|
82
|
+
readonly knownAgentCount: number;
|
|
83
|
+
readonly phaseCompletedAgentCount?: number;
|
|
84
|
+
readonly phaseKnownAgentCount?: number;
|
|
49
85
|
readonly worktreePath?: string;
|
|
50
86
|
readonly worktreePreserved?: boolean;
|
|
51
87
|
readonly preservedWorktrees?: readonly WorkflowAgentPreservedWorktree[];
|
|
@@ -192,7 +228,7 @@ interface BuiltinWorkflow {
|
|
|
192
228
|
export interface WorkflowAgentPreservedWorktree {
|
|
193
229
|
readonly path: string;
|
|
194
230
|
readonly attemptIndex: number;
|
|
195
|
-
readonly reason: '
|
|
231
|
+
readonly reason: 'clean' | 'changed' | 'stalled' | 'aborted' | 'status_unavailable';
|
|
196
232
|
}
|
|
197
233
|
export declare class WorkflowTaskRegistry implements WorkflowRuntime {
|
|
198
234
|
private readonly options;
|
|
@@ -250,6 +286,8 @@ export declare class WorkflowTaskRegistry implements WorkflowRuntime {
|
|
|
250
286
|
private runAgentAttempt;
|
|
251
287
|
private parallel;
|
|
252
288
|
private pipeline;
|
|
289
|
+
private announcePlan;
|
|
290
|
+
private announcePhasePlan;
|
|
253
291
|
private phase;
|
|
254
292
|
private completeTask;
|
|
255
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;
|
|
@@ -98,6 +98,16 @@ const WORKSPACE_CONTEXT_PRIORITY_FILES = new Set([
|
|
|
98
98
|
'package.json',
|
|
99
99
|
'tsconfig.json',
|
|
100
100
|
]);
|
|
101
|
+
const DYNAMIC_WORKFLOW_PATTERN_GUIDANCE = [
|
|
102
|
+
'Use dynamic workflow patterns intentionally:',
|
|
103
|
+
'- classify-and-act: classify the request, risk, or repository area before choosing phase shape.',
|
|
104
|
+
'- fan-out-and-synthesize: split independent lenses across parallel agents, then merge evidence.',
|
|
105
|
+
'- adversarial verification: assign at least one agent to challenge correctness, security, or assumptions on high-risk work.',
|
|
106
|
+
'- generate-and-filter: create candidate approaches or fixes, then select by evidence and constraints.',
|
|
107
|
+
'- tournament: compare competing alternatives when the best path is unclear.',
|
|
108
|
+
'- loop-until-done: iterate repair and verification only when there is a clear stop condition.',
|
|
109
|
+
'Prefer pipelines when later phases need earlier summaries; prefer parallel agents when independent evidence can be gathered at the same time.',
|
|
110
|
+
].join('\n');
|
|
101
111
|
const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
102
112
|
{
|
|
103
113
|
name: 'task',
|
|
@@ -106,7 +116,7 @@ const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
|
106
116
|
description: 'Run an LLM-planned phase-wise parallel task workflow',
|
|
107
117
|
defaultPrompt: 'Complete the requested repository task.',
|
|
108
118
|
plannerKind: 'general task',
|
|
109
|
-
plannerGuidance: 'Plan phases that make the work faster and more accurate through parallel agents. Default to phase_parallel. Choose single only for tiny changes, strictly sequential investigations, or one indivisible failure mode.',
|
|
119
|
+
plannerGuidance: 'Plan phases that make the work faster and more accurate through parallel agents. Default to phase_parallel. Choose single only for tiny changes, strictly sequential investigations, or one indivisible failure mode. Pick workflow patterns that match the request instead of using one fixed shape.',
|
|
110
120
|
agentGuidance: 'Complete the assigned phase work. Prefer concrete evidence, file paths, commands, and risks over broad narration.',
|
|
111
121
|
finalGuidance: 'Return the completed task result, key evidence, decisions made, verification status, and residual risk.',
|
|
112
122
|
}),
|
|
@@ -118,7 +128,7 @@ const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
|
118
128
|
description: 'Run an LLM-planned phase-wise parallel code review workflow',
|
|
119
129
|
defaultPrompt: 'Review the current repository for correctness risks.',
|
|
120
130
|
plannerKind: 'code review',
|
|
121
|
-
plannerGuidance: 'Plan an effective code review. Default to phase_parallel with multiple focused reviewers per phase. Commonly useful lenses include runtime correctness, security/capability boundaries, API/CLI contracts, persistence/retry/cancel behavior, and test coverage.
|
|
131
|
+
plannerGuidance: 'Plan an effective code review. Default to phase_parallel with multiple focused reviewers per phase. Commonly useful lenses include runtime correctness, security/capability boundaries, API/CLI contracts, persistence/retry/cancel behavior, and test coverage. Prefer fan-out-and-synthesize plus adversarial verification unless the diff is tiny or one indivisible failure mode.',
|
|
122
132
|
agentGuidance: 'Return material findings only. Prioritize root cause, severity, exact file/line evidence, reproduction or impact, and residual risk.',
|
|
123
133
|
finalGuidance: 'Return findings ordered by severity with exact file/line references. Deduplicate overlaps, preserve dissent or uncertainty, and say clearly if there are no material findings.',
|
|
124
134
|
}),
|
|
@@ -131,6 +141,15 @@ const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
|
131
141
|
};
|
|
132
142
|
const input = args && typeof args === "object" ? args : {};
|
|
133
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
|
+
}
|
|
134
153
|
phase("Batch");
|
|
135
154
|
return await parallel(prompts.map((prompt, index) => () => agent(
|
|
136
155
|
prompt == null ? "" : "" + prompt,
|
|
@@ -152,6 +171,8 @@ const plan = await agent([
|
|
|
152
171
|
${JSON.stringify(`Plan the phase-wise execution strategy for ${input.plannerKind}.`)},
|
|
153
172
|
"",
|
|
154
173
|
${JSON.stringify(input.plannerGuidance)},
|
|
174
|
+
"",
|
|
175
|
+
${JSON.stringify(DYNAMIC_WORKFLOW_PATTERN_GUIDANCE)},
|
|
155
176
|
"A phase runs after previous phase summaries are available. Within each phase, use parallel agents by default.",
|
|
156
177
|
"Return 1 to 4 phases. For ordinary work, prefer 2 phases with 2 to 4 parallel agents each. Use concise stable ids.",
|
|
157
178
|
"",
|
|
@@ -202,9 +223,31 @@ const plan = await agent([
|
|
|
202
223
|
}
|
|
203
224
|
});
|
|
204
225
|
const selectedPhases = plan.mode === "single" ? [plan.phases[0]] : plan.phases;
|
|
226
|
+
function plannedPhaseFor(phasePlan) {
|
|
227
|
+
return {
|
|
228
|
+
id: phasePlan.id,
|
|
229
|
+
title: phasePlan.title,
|
|
230
|
+
goal: phasePlan.goal,
|
|
231
|
+
agents: (plan.mode === "single" ? [phasePlan.agents[0]] : phasePlan.agents).map((phaseAgent) => ({
|
|
232
|
+
id: phaseAgent.id,
|
|
233
|
+
title: phaseAgent.title,
|
|
234
|
+
focus: phaseAgent.focus,
|
|
235
|
+
label: plan.mode === "single"
|
|
236
|
+
? ${JSON.stringify(`${input.name}-single`)}
|
|
237
|
+
: ${JSON.stringify(`${input.name}-`)} + phasePlan.id + "-" + phaseAgent.id
|
|
238
|
+
}))
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const firstPhasePlan = plannedPhaseFor(selectedPhases[0]);
|
|
242
|
+
announcePlan({
|
|
243
|
+
mode: plan.mode,
|
|
244
|
+
rationale: plan.rationale,
|
|
245
|
+
phases: [firstPhasePlan]
|
|
246
|
+
});
|
|
205
247
|
if (plan.mode === "single") {
|
|
206
|
-
const singlePhase =
|
|
248
|
+
const singlePhase = firstPhasePlan;
|
|
207
249
|
const singleAgent = singlePhase.agents[0];
|
|
250
|
+
announcePhasePlan(singlePhase);
|
|
208
251
|
phase(singlePhase.title);
|
|
209
252
|
return await agent([
|
|
210
253
|
"Single-agent execution selected by the LLM planner.",
|
|
@@ -224,7 +267,9 @@ if (plan.mode === "single") {
|
|
|
224
267
|
}
|
|
225
268
|
const phaseOutputs = [];
|
|
226
269
|
const priorSummaries = [];
|
|
227
|
-
for (const
|
|
270
|
+
for (const rawPhasePlan of selectedPhases) {
|
|
271
|
+
const phasePlan = plannedPhaseFor(rawPhasePlan);
|
|
272
|
+
announcePhasePlan(phasePlan);
|
|
228
273
|
phase(phasePlan.title);
|
|
229
274
|
const agents = phasePlan.agents;
|
|
230
275
|
const agentOutputs = agents.length < 2
|
|
@@ -369,11 +414,6 @@ export class WorkflowTaskRegistry {
|
|
|
369
414
|
}
|
|
370
415
|
}
|
|
371
416
|
catch (err) {
|
|
372
|
-
await cleanupWorkflowJournalTranscriptDir(transcriptDir).catch(() => undefined);
|
|
373
|
-
if (!resolved.scriptPath) {
|
|
374
|
-
await rm(scriptPath, { force: true }).catch(() => undefined);
|
|
375
|
-
await rm(workflowScriptMetadataPath(scriptPath), { force: true }).catch(() => undefined);
|
|
376
|
-
}
|
|
377
417
|
throw workflowJournalRequestError(err);
|
|
378
418
|
}
|
|
379
419
|
const task = {
|
|
@@ -941,9 +981,11 @@ export class WorkflowTaskRegistry {
|
|
|
941
981
|
controller.abort();
|
|
942
982
|
return;
|
|
943
983
|
}
|
|
944
|
-
const workflowTimer =
|
|
945
|
-
|
|
946
|
-
|
|
984
|
+
const workflowTimer = this.options.requestTimeoutMs > 0
|
|
985
|
+
? setTimeout(() => {
|
|
986
|
+
controller.abort();
|
|
987
|
+
}, this.options.requestTimeoutMs)
|
|
988
|
+
: null;
|
|
947
989
|
try {
|
|
948
990
|
if (controller.signal.aborted)
|
|
949
991
|
throw workflowInputError('Workflow is aborted.');
|
|
@@ -973,9 +1015,7 @@ export class WorkflowTaskRegistry {
|
|
|
973
1015
|
toolCalls: ctx.toolCalls,
|
|
974
1016
|
durationMs: Date.now() - ctx.startedAt,
|
|
975
1017
|
});
|
|
976
|
-
|
|
977
|
-
await rm(resultPath, { force: true }).catch(() => undefined);
|
|
978
|
-
}
|
|
1018
|
+
void completedSnapshot;
|
|
979
1019
|
}
|
|
980
1020
|
catch (err) {
|
|
981
1021
|
const abortFailure = controller.signal.aborted
|
|
@@ -985,7 +1025,8 @@ export class WorkflowTaskRegistry {
|
|
|
985
1025
|
await this.failTask(task, abortFailure ? abortFailure.message : workflowErrorMessage(err), abortFailure ? abortFailure.reason : workflowFailureReason(err));
|
|
986
1026
|
}
|
|
987
1027
|
finally {
|
|
988
|
-
|
|
1028
|
+
if (workflowTimer)
|
|
1029
|
+
clearTimeout(workflowTimer);
|
|
989
1030
|
for (const timer of ctx.timers.values())
|
|
990
1031
|
clearTimeout(timer);
|
|
991
1032
|
ctx.timers.clear();
|
|
@@ -1048,6 +1089,8 @@ export class WorkflowTaskRegistry {
|
|
|
1048
1089
|
host.workspaceContext = hardenCallable((options) => {
|
|
1049
1090
|
return this.trackWorkflowPromise(ctx, this.workspaceContext(ctx, options));
|
|
1050
1091
|
});
|
|
1092
|
+
host.announcePlan = hardenCallable((plan) => this.announcePlan(ctx, plan));
|
|
1093
|
+
host.announcePhasePlan = hardenCallable((phasePlan) => this.announcePhasePlan(ctx, phasePlan));
|
|
1051
1094
|
host.phase = hardenCallable((title) => this.phase(ctx, title));
|
|
1052
1095
|
host.log = hardenCallable(log);
|
|
1053
1096
|
host.consoleLog = hardenCallable((...values) => {
|
|
@@ -1185,6 +1228,7 @@ export class WorkflowTaskRegistry {
|
|
|
1185
1228
|
toolCalls: 0,
|
|
1186
1229
|
resultPreview: previewValue(cached.result, 160),
|
|
1187
1230
|
cached: true,
|
|
1231
|
+
...agentCompletionProgress(ctx, phase),
|
|
1188
1232
|
});
|
|
1189
1233
|
return cached.result;
|
|
1190
1234
|
}
|
|
@@ -1248,6 +1292,7 @@ export class WorkflowTaskRegistry {
|
|
|
1248
1292
|
tokens: usage.totalTokens,
|
|
1249
1293
|
toolCalls,
|
|
1250
1294
|
resultPreview: previewValue(journalResult, 160),
|
|
1295
|
+
...agentCompletionProgress(ctx, phase),
|
|
1251
1296
|
...preservedWorktreeEventProjection(preservedWorktrees),
|
|
1252
1297
|
});
|
|
1253
1298
|
return journalResult;
|
|
@@ -1313,7 +1358,6 @@ export class WorkflowTaskRegistry {
|
|
|
1313
1358
|
};
|
|
1314
1359
|
}
|
|
1315
1360
|
catch (err) {
|
|
1316
|
-
await rm(worktreePath, { recursive: true, force: true }).catch(() => undefined);
|
|
1317
1361
|
throw workflowInputError(`worktree isolation could not create an isolated worktree: ${workflowErrorMessage(err)}`);
|
|
1318
1362
|
}
|
|
1319
1363
|
}
|
|
@@ -1334,16 +1378,10 @@ export class WorkflowTaskRegistry {
|
|
|
1334
1378
|
preservedWorktree: preservedWorktree(worktree, 'changed'),
|
|
1335
1379
|
};
|
|
1336
1380
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
return {
|
|
1342
|
-
preserved: true,
|
|
1343
|
-
preservedWorktree: preservedWorktree(worktree, 'cleanup_failed'),
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
return { preserved: false };
|
|
1381
|
+
return {
|
|
1382
|
+
preserved: true,
|
|
1383
|
+
preservedWorktree: preservedWorktree(worktree, 'clean'),
|
|
1384
|
+
};
|
|
1347
1385
|
}
|
|
1348
1386
|
async runAgentWithStallRetries(ctx, input) {
|
|
1349
1387
|
for (let retryIndex = 0; retryIndex <= this.agentStallRetryLimit; retryIndex += 1) {
|
|
@@ -1418,10 +1456,12 @@ export class WorkflowTaskRegistry {
|
|
|
1418
1456
|
throw workflowInputError('Workflow is aborted.');
|
|
1419
1457
|
}
|
|
1420
1458
|
ctx.controller.signal.addEventListener('abort', abortFromWorkflow, { once: true });
|
|
1421
|
-
const timer =
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1459
|
+
const timer = this.agentStallTimeoutMs > 0
|
|
1460
|
+
? setTimeout(() => {
|
|
1461
|
+
timedOut = true;
|
|
1462
|
+
attemptController.abort();
|
|
1463
|
+
}, this.agentStallTimeoutMs)
|
|
1464
|
+
: null;
|
|
1425
1465
|
try {
|
|
1426
1466
|
const generated = this.options.backend.generate(request, attemptController.signal).then((result) => ({ type: 'result', result }), (err) => ({ type: 'error', error: err }));
|
|
1427
1467
|
const aborted = new Promise((resolve) => {
|
|
@@ -1444,7 +1484,8 @@ export class WorkflowTaskRegistry {
|
|
|
1444
1484
|
throw outcome.error;
|
|
1445
1485
|
}
|
|
1446
1486
|
finally {
|
|
1447
|
-
|
|
1487
|
+
if (timer)
|
|
1488
|
+
clearTimeout(timer);
|
|
1448
1489
|
ctx.controller.signal.removeEventListener('abort', abortFromWorkflow);
|
|
1449
1490
|
}
|
|
1450
1491
|
}
|
|
@@ -1494,22 +1535,67 @@ export class WorkflowTaskRegistry {
|
|
|
1494
1535
|
return current;
|
|
1495
1536
|
});
|
|
1496
1537
|
}
|
|
1538
|
+
announcePlan(ctx, plan) {
|
|
1539
|
+
if (ctx.controller.signal.aborted || ctx.task.status !== 'running') {
|
|
1540
|
+
throw workflowInputError('Workflow is aborted.');
|
|
1541
|
+
}
|
|
1542
|
+
const normalized = normalizeWorkflowExecutionPlan(plan);
|
|
1543
|
+
ctx.announcedPlan = normalized;
|
|
1544
|
+
this.emit(ctx.task, {
|
|
1545
|
+
type: 'workflow.plan.ready',
|
|
1546
|
+
taskId: ctx.task.taskId,
|
|
1547
|
+
runId: ctx.task.runId,
|
|
1548
|
+
...normalized,
|
|
1549
|
+
});
|
|
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
|
+
}
|
|
1497
1571
|
phase(ctx, title) {
|
|
1498
1572
|
if (typeof title !== 'string' || title.trim() === '') {
|
|
1499
1573
|
throw workflowInputError('phase() requires a non-empty string title.');
|
|
1500
1574
|
}
|
|
1501
|
-
|
|
1575
|
+
const normalizedTitle = title.trim();
|
|
1576
|
+
ctx.currentPhase = normalizedTitle;
|
|
1502
1577
|
const phaseIndex = ctx.task.events
|
|
1503
1578
|
.filter((event) => event.type === 'workflow.phase.started')
|
|
1504
1579
|
.length;
|
|
1505
|
-
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);
|
|
1506
1587
|
this.emit(ctx.task, {
|
|
1507
1588
|
type: 'workflow.phase.started',
|
|
1508
1589
|
taskId: ctx.task.taskId,
|
|
1509
1590
|
runId: ctx.task.runId,
|
|
1510
1591
|
phaseIndex,
|
|
1511
|
-
title,
|
|
1592
|
+
title: normalizedTitle,
|
|
1512
1593
|
...(detail ? { detail } : {}),
|
|
1594
|
+
...(plannedPhase?.goal ? { goal: plannedPhase.goal } : {}),
|
|
1595
|
+
...(plannedPhase ? {
|
|
1596
|
+
plannedAgentCount: plannedPhase.agents.length,
|
|
1597
|
+
plannedAgents: plannedPhase.agents,
|
|
1598
|
+
} : {}),
|
|
1513
1599
|
});
|
|
1514
1600
|
}
|
|
1515
1601
|
async completeTask(ctx, result, event) {
|
|
@@ -1904,19 +1990,6 @@ function uniqueStrings(values) {
|
|
|
1904
1990
|
}
|
|
1905
1991
|
return out;
|
|
1906
1992
|
}
|
|
1907
|
-
async function removeCleanGitWorktree(worktree) {
|
|
1908
|
-
try {
|
|
1909
|
-
await gitOutput(worktree.gitRoot, ['worktree', 'remove', '--force', worktree.path]);
|
|
1910
|
-
}
|
|
1911
|
-
catch (err) {
|
|
1912
|
-
if (/No such file|not a working tree|is not a working tree/i.test(workflowErrorMessage(err))) {
|
|
1913
|
-
await rm(worktree.path, { recursive: true, force: true }).catch(() => undefined);
|
|
1914
|
-
await gitOutput(worktree.gitRoot, ['worktree', 'prune']).catch(() => undefined);
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
throw err;
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
1993
|
function preservedWorktree(worktree, reason) {
|
|
1921
1994
|
return {
|
|
1922
1995
|
path: worktree.path,
|
|
@@ -1928,7 +2001,7 @@ function preservedWorktreeEventProjection(preservedWorktrees) {
|
|
|
1928
2001
|
if (preservedWorktrees.length === 0)
|
|
1929
2002
|
return {};
|
|
1930
2003
|
const primary = preservedWorktrees.find((item) => item.reason === 'changed')
|
|
1931
|
-
?? preservedWorktrees.find((item) => item.reason === '
|
|
2004
|
+
?? preservedWorktrees.find((item) => item.reason === 'status_unavailable')
|
|
1932
2005
|
?? preservedWorktrees[0];
|
|
1933
2006
|
return {
|
|
1934
2007
|
worktreePath: primary?.path,
|
|
@@ -1936,6 +2009,33 @@ function preservedWorktreeEventProjection(preservedWorktrees) {
|
|
|
1936
2009
|
preservedWorktrees: [...preservedWorktrees],
|
|
1937
2010
|
};
|
|
1938
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
|
+
}
|
|
1939
2039
|
function shortHash(value) {
|
|
1940
2040
|
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
1941
2041
|
}
|
|
@@ -2642,6 +2742,8 @@ function normalizeAgentStallTimeoutMs(configured, requestTimeoutMs) {
|
|
|
2642
2742
|
if (configured !== undefined && Number.isFinite(configured) && configured > 0) {
|
|
2643
2743
|
return Math.max(1, Math.floor(configured));
|
|
2644
2744
|
}
|
|
2745
|
+
if (configured === 0 || requestTimeoutMs === 0)
|
|
2746
|
+
return 0;
|
|
2645
2747
|
return Math.max(1, Math.floor(requestTimeoutMs));
|
|
2646
2748
|
}
|
|
2647
2749
|
function workflowTaskSnapshot(task) {
|
|
@@ -2929,6 +3031,8 @@ function installWorkflowVmGlobals(context, globals) {
|
|
|
2929
3031
|
' define(globalThis, "parallel", { value: (...values) => __host.parallel(...values), writable: false, configurable: false });',
|
|
2930
3032
|
' define(globalThis, "pipeline", { value: (...values) => __host.pipeline(...values), writable: false, configurable: false });',
|
|
2931
3033
|
' define(globalThis, "workspaceContext", { value: (...values) => __host.workspaceContext(...values), writable: false, configurable: false });',
|
|
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 });',
|
|
2932
3036
|
' define(globalThis, "phase", { value: (...values) => __host.phase(...values), writable: false, configurable: false });',
|
|
2933
3037
|
' define(globalThis, "log", { value: (...values) => __host.log(...values), writable: false, configurable: false });',
|
|
2934
3038
|
' define(globalThis, "workflow", { value: (...values) => __host.workflow(...values), writable: false, configurable: false });',
|
|
@@ -3612,6 +3716,59 @@ function previewValue(value, limit) {
|
|
|
3612
3716
|
: JSON.stringify(value) ?? String(value);
|
|
3613
3717
|
return preview(text, limit);
|
|
3614
3718
|
}
|
|
3719
|
+
function normalizeWorkflowExecutionPlan(value) {
|
|
3720
|
+
const record = asRecord(value);
|
|
3721
|
+
if (!record)
|
|
3722
|
+
throw workflowInputError('announcePlan(plan) requires a plan object.');
|
|
3723
|
+
const mode = boundedPlanString(record.mode, 'phase_parallel', 48);
|
|
3724
|
+
const rationale = optionalBoundedPlanString(record.rationale, 400);
|
|
3725
|
+
const rawPhases = Array.isArray(record.phases) ? Array.from(record.phases) : [];
|
|
3726
|
+
if (rawPhases.length === 0)
|
|
3727
|
+
throw workflowInputError('announcePlan(plan) requires at least one phase.');
|
|
3728
|
+
const phases = rawPhases
|
|
3729
|
+
.slice(0, 16)
|
|
3730
|
+
.map((phaseValue, phaseIndex) => normalizeWorkflowPhasePlan(phaseValue, `announcePlan(plan).phases[${phaseIndex}]`, phaseIndex));
|
|
3731
|
+
return {
|
|
3732
|
+
mode,
|
|
3733
|
+
...(rationale ? { rationale } : {}),
|
|
3734
|
+
phases,
|
|
3735
|
+
};
|
|
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
|
+
}
|
|
3763
|
+
function boundedPlanString(value, fallback, limit) {
|
|
3764
|
+
const text = typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
3765
|
+
return preview(text, limit);
|
|
3766
|
+
}
|
|
3767
|
+
function optionalBoundedPlanString(value, limit) {
|
|
3768
|
+
if (typeof value !== 'string' || !value.trim())
|
|
3769
|
+
return undefined;
|
|
3770
|
+
return boundedPlanString(value, '', limit);
|
|
3771
|
+
}
|
|
3615
3772
|
function asRecord(value) {
|
|
3616
3773
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3617
3774
|
return null;
|
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,15 +13,19 @@ 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
|
|
22
23
|
phase with parallel focused subagents by default, followed by phase and final
|
|
23
24
|
synthesis. A single-agent path is reserved for cases where the planner judges
|
|
24
25
|
parallel execution risky or wasteful.
|
|
26
|
+
Planner guidance includes classify-and-act, fan-out-and-synthesize,
|
|
27
|
+
adversarial verification, generate-and-filter, tournament, and loop-until-done
|
|
28
|
+
patterns so workflow shape can follow the task instead of a fixed template.
|
|
25
29
|
|
|
26
30
|
## Install And Run
|
|
27
31
|
|
|
@@ -41,22 +45,33 @@ For source-checkout validation before publish:
|
|
|
41
45
|
|
|
42
46
|
```bash
|
|
43
47
|
npm run pack:ultracode-for-codex
|
|
44
|
-
npm install --save-dev ./artifacts/ultracode-for-codex-0.2.
|
|
48
|
+
npm install --save-dev ./artifacts/ultracode-for-codex-0.2.6.tgz
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
CLI behavior:
|
|
48
52
|
|
|
53
|
+
- `--version` or `-v` prints the installed package version;
|
|
49
54
|
- default execution is `background`; stdout contains a launch record with
|
|
50
55
|
`jobId`, `pid`, `resultPath`, `progressPath`, `metadataPath`, and `pidPath`;
|
|
51
|
-
- attached execution is available with `--execution attached
|
|
56
|
+
- attached execution is available with `--execution attached` when the caller
|
|
57
|
+
should stay connected until completion;
|
|
52
58
|
- attached progress prints to stderr as JSONL by default;
|
|
53
59
|
- attached final workflow result prints as JSON to stdout;
|
|
54
60
|
- JSONL records include `kind`, `version`, `event`, `status`, and `summary`,
|
|
55
61
|
with agent identity and label fields on agent records;
|
|
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;
|
|
56
70
|
- `Ctrl-C` cancels the active attached workflow;
|
|
57
71
|
- `--retry-limit <n>` retries failed workflows inside the same process;
|
|
58
|
-
- `--timeout-ms`
|
|
59
|
-
|
|
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.
|
|
60
75
|
- `--permission ask|allow|deny` handles project/user/plugin/scriptPath reviews.
|
|
61
76
|
- `--progress plain` switches to human-readable progress lines.
|
|
62
77
|
- background file locations are controlled by `workflow.background` in
|
|
@@ -71,14 +86,15 @@ CLI behavior:
|
|
|
71
86
|
- Built-in `task` and `code-review` inject deterministic workspace context into
|
|
72
87
|
planner-selected phase-wise parallel subagents.
|
|
73
88
|
- Keep workflow execution local and command-owned; settings default to OS
|
|
74
|
-
background execution
|
|
75
|
-
|
|
89
|
+
background execution so long runs can keep waiting while Codex does other
|
|
90
|
+
work.
|
|
76
91
|
- Keep `journalPath`, `journal.jsonl`, and journal contents out of CLI output.
|
|
77
92
|
- Treat `.ultracode-for-codex` workflow state as sensitive local data.
|
|
78
93
|
- Keep `resumeFromRunId` runtime-internal unless cross-process resume
|
|
79
94
|
gets an explicit durable design.
|
|
80
95
|
- Use `isolation: "worktree"` only inside a git repo with at least one commit;
|
|
81
|
-
|
|
96
|
+
isolated worktrees are intentionally preserved for review, including clean
|
|
97
|
+
worktrees.
|
|
82
98
|
|
|
83
99
|
## Packaging And Verification
|
|
84
100
|
|