ultracode-for-codex 0.2.5 → 0.2.6

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