ultracode-for-codex 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.3.0`;
11
11
  - the locally installed companion Codex skill.
12
12
 
13
13
  Generated build output and package tarballs were checked as projections of the
@@ -23,7 +23,8 @@ License transition completed:
23
23
 
24
24
  - Apache-2.0 `LICENSE` file is present;
25
25
  - `package.json` and `package-lock.json` declare `Apache-2.0`;
26
- - package version is prepared as `0.2.3` for the license-bearing patch release.
26
+ - release-candidate package version is `0.3.0`;
27
+ - npm latest before this release remains `0.2.6`.
27
28
 
28
29
  ## Evidence
29
30