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 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.4.tgz
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": 180000,
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` is the workflow timeout and the default per-agent silence
107
- budget; it is not divided by the retry budget.
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 Codex should read progress until completion.
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 and `--execution attached` keeps the process connected
135
- until completion.
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, removes it when clean, and preserves changed or unsafe-to-clean
144
- worktrees for review.
163
+ worktree and preserves the worktree for review, including clean worktrees.
145
164
 
146
165
  ## Development
147
166
 
@@ -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.4.tgz
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. Use
59
- `--execution attached` when Codex should read progress until completion.
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": 180000,
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` is the workflow timeout and the default per-agent silence
97
- budget; it is not divided by the retry budget.
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
- for current-terminal progress streaming.
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 and `--execution attached` keeps the process connected
109
- until completion.
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
- Changed or unsafe-to-clean worktrees are intentionally preserved for review.
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.detail ? `Phase ${event.title}: ${event.detail}` : `Phase ${event.title}`,
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 <= 0)
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: '0.2.0',
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.timeoutMs}ms`));
288
- }, this.timeoutMs);
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 = setTimeout(() => {
502
- this.turnWaiters.delete(key);
503
- cleanup();
504
- reject(new Error(`turn timed out after ${this.timeoutMs}ms`));
505
- }, this.timeoutMs);
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
- clearTimeout(timer);
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, rm, stat } from 'node:fs/promises';
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: 'changed' | 'stalled' | 'aborted' | 'status_unavailable' | 'cleanup_failed';
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, rm, stat, writeFile } from 'node:fs/promises';
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, cleanupWorkflowJournalTranscriptDir, computeWorkflowAgentCallKey, isWorkflowJournalError, normalizeJournalJsonValue, readWorkflowJournal, workflowJournalPath, } from './workflow-journal.js';
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. Choose single only for a tiny scoped diff or one indivisible failure mode.',
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 = selectedPhases[0];
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 phasePlan of selectedPhases) {
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 = setTimeout(() => {
945
- controller.abort();
946
- }, this.options.requestTimeoutMs);
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
- if (completedSnapshot.status !== 'completed') {
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
- clearTimeout(workflowTimer);
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
- try {
1338
- await removeCleanGitWorktree(worktree);
1339
- }
1340
- catch {
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 = setTimeout(() => {
1422
- timedOut = true;
1423
- attemptController.abort();
1424
- }, this.agentStallTimeoutMs);
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
- clearTimeout(timer);
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
- ctx.currentPhase = title;
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 === title)?.detail;
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 === 'cleanup_failed' || item.reason === 'status_unavailable')
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: readPositiveIntegerSetting(workflow?.timeoutMs, 'workflow.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}')) {
@@ -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.3`;
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.3` for the license-bearing patch release.
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
- - unchanged isolated worktrees are removed after the agent finishes;
15
- - changed or unsafe-to-clean worktrees are preserved for review;
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 preserve vs cleanup. |
26
- | preserved path projection | runtime event | Changed or unsafe-to-clean worktrees are surfaced on agent final events. |
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. Remove clean worktrees.
52
- 7. Preserve changed, stalled, aborted, status-unavailable, or cleanup-failed
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 preservation.
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.4",
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
@@ -4,7 +4,7 @@
4
4
  "progress": "jsonl",
5
5
  "permission": "ask",
6
6
  "retryLimit": 0,
7
- "timeoutMs": 180000,
7
+ "timeoutMs": 0,
8
8
  "background": {
9
9
  "runDir": ".ultracode-for-codex/background/{jobId}",
10
10
  "resultFile": "result.json",
@@ -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
- Attached runs stream stderr JSONL for Codex-readable status, while stdout
18
- remains the final workflow result JSON.
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.4.tgz
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` is the workflow timeout and the default per-agent silence
59
- budget; it is not 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.
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 and `--execution attached` keeps the process connected
75
- until completion.
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
- changed or unsafe worktrees are intentionally preserved for review.
96
+ isolated worktrees are intentionally preserved for review, including clean
97
+ worktrees.
82
98
 
83
99
  ## Packaging And Verification
84
100