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.
- package/README.md +54 -12
- package/ULTRACODE_INSTALL.md +58 -13
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +848 -11
- package/dist/codex/subagent-backend.d.ts +1 -0
- package/dist/codex/subagent-backend.js +22 -10
- package/dist/runtime/package-info.d.ts +1 -0
- package/dist/runtime/package-info.js +12 -0
- package/dist/runtime/workflow-journal.d.ts +0 -1
- package/dist/runtime/workflow-journal.js +1 -4
- package/dist/runtime/workflow-runtime.d.ts +19 -1
- package/dist/runtime/workflow-runtime.js +144 -80
- package/dist/settings.js +1 -6
- package/docs/provenance-audit.md +3 -2
- package/docs/ultracode-p3c-worktree-isolation.md +8 -8
- package/package.json +4 -1
- package/settings.json +1 -1
- package/skills/ultracode-for-codex/SKILL.md +32 -13
|
@@ -20,6 +20,7 @@ export declare class CodexSubagentBackend implements SubagentBackend {
|
|
|
20
20
|
private readonly cwd;
|
|
21
21
|
private readonly configuredModel?;
|
|
22
22
|
private readonly timeoutMs;
|
|
23
|
+
private readonly rpcTimeoutMs;
|
|
23
24
|
private readonly reasoningEffort;
|
|
24
25
|
private readonly verbosity;
|
|
25
26
|
private child;
|
|
@@ -6,9 +6,11 @@ import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
|
6
6
|
import readline from 'node:readline';
|
|
7
7
|
import { codexDefaultReasoningEffort, codexDefaultVerbosity, } from '../settings.js';
|
|
8
8
|
import { estimateTokens } from '../runtime/types.js';
|
|
9
|
+
import { ultracodePackageVersion } from '../runtime/package-info.js';
|
|
9
10
|
import { codexChildProcessEnv } from './env.js';
|
|
10
11
|
const USAGE_NOTIFICATION_GRACE_MS = 100;
|
|
11
12
|
const BUFFERED_TURN_STATE_TTL_MS = 30_000;
|
|
13
|
+
const DEFAULT_CODEX_RPC_TIMEOUT_MS = 30_000;
|
|
12
14
|
const FALLBACK_CODEX_MODEL = 'gpt-5.5';
|
|
13
15
|
const WORKSPACE_DYNAMIC_TOOL_NAMESPACE = 'workspace';
|
|
14
16
|
const MAX_WORKSPACE_TOOL_READ_BYTES = 200_000;
|
|
@@ -72,6 +74,7 @@ export class CodexSubagentBackend {
|
|
|
72
74
|
cwd;
|
|
73
75
|
configuredModel;
|
|
74
76
|
timeoutMs;
|
|
77
|
+
rpcTimeoutMs;
|
|
75
78
|
reasoningEffort;
|
|
76
79
|
verbosity;
|
|
77
80
|
child = null;
|
|
@@ -89,7 +92,8 @@ export class CodexSubagentBackend {
|
|
|
89
92
|
this.cwd = options.cwd;
|
|
90
93
|
this.model = options.model ?? 'codex-subagent';
|
|
91
94
|
this.configuredModel = options.model;
|
|
92
|
-
this.timeoutMs = options.timeoutMs;
|
|
95
|
+
this.timeoutMs = normalizeOptionalTimeoutMs(options.timeoutMs);
|
|
96
|
+
this.rpcTimeoutMs = this.timeoutMs > 0 ? this.timeoutMs : DEFAULT_CODEX_RPC_TIMEOUT_MS;
|
|
93
97
|
this.reasoningEffort = options.reasoningEffort ?? codexDefaultReasoningEffort();
|
|
94
98
|
this.verbosity = options.verbosity ?? codexDefaultVerbosity();
|
|
95
99
|
}
|
|
@@ -220,7 +224,7 @@ export class CodexSubagentBackend {
|
|
|
220
224
|
clientInfo: {
|
|
221
225
|
name: 'ultracode_for_codex',
|
|
222
226
|
title: 'Ultracode for Codex',
|
|
223
|
-
version:
|
|
227
|
+
version: ultracodePackageVersion(),
|
|
224
228
|
},
|
|
225
229
|
capabilities: {
|
|
226
230
|
experimentalApi: true,
|
|
@@ -284,8 +288,8 @@ export class CodexSubagentBackend {
|
|
|
284
288
|
return new Promise((resolve, reject) => {
|
|
285
289
|
const timer = setTimeout(() => {
|
|
286
290
|
this.pending.delete(id);
|
|
287
|
-
reject(new Error(`${method} timed out after ${this.
|
|
288
|
-
}, this.
|
|
291
|
+
reject(new Error(`${method} timed out after ${this.rpcTimeoutMs}ms`));
|
|
292
|
+
}, this.rpcTimeoutMs);
|
|
289
293
|
this.pending.set(id, { method, resolve, reject, timer });
|
|
290
294
|
});
|
|
291
295
|
}
|
|
@@ -498,13 +502,16 @@ export class CodexSubagentBackend {
|
|
|
498
502
|
const key = `${threadId}:${turnId}`;
|
|
499
503
|
return new Promise((resolve, reject) => {
|
|
500
504
|
let waiter;
|
|
501
|
-
const timer =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
505
|
+
const timer = this.timeoutMs > 0
|
|
506
|
+
? setTimeout(() => {
|
|
507
|
+
this.turnWaiters.delete(key);
|
|
508
|
+
cleanup();
|
|
509
|
+
reject(new Error(`turn timed out after ${this.timeoutMs}ms`));
|
|
510
|
+
}, this.timeoutMs)
|
|
511
|
+
: null;
|
|
506
512
|
const cleanup = () => {
|
|
507
|
-
|
|
513
|
+
if (timer)
|
|
514
|
+
clearTimeout(timer);
|
|
508
515
|
if (waiter?.usageGraceTimer) {
|
|
509
516
|
clearTimeout(waiter.usageGraceTimer);
|
|
510
517
|
waiter.usageGraceTimer = undefined;
|
|
@@ -660,6 +667,11 @@ function estimatedUsage(prompt, text) {
|
|
|
660
667
|
source: 'estimated',
|
|
661
668
|
};
|
|
662
669
|
}
|
|
670
|
+
function normalizeOptionalTimeoutMs(value) {
|
|
671
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
672
|
+
return 0;
|
|
673
|
+
return Math.floor(value);
|
|
674
|
+
}
|
|
663
675
|
function turnStateKey(threadId, turnId) {
|
|
664
676
|
return typeof threadId === 'string' && typeof turnId === 'string'
|
|
665
677
|
? `${threadId}:${turnId}`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ultracodePackageVersion(): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
let cachedPackageVersion;
|
|
3
|
+
export function ultracodePackageVersion() {
|
|
4
|
+
if (cachedPackageVersion)
|
|
5
|
+
return cachedPackageVersion;
|
|
6
|
+
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
if (typeof packageJson.version !== 'string' || !packageJson.version.trim()) {
|
|
8
|
+
throw new Error('Package version is missing from package.json.');
|
|
9
|
+
}
|
|
10
|
+
cachedPackageVersion = packageJson.version;
|
|
11
|
+
return cachedPackageVersion;
|
|
12
|
+
}
|
|
@@ -131,5 +131,4 @@ export declare function computeWorkflowAgentCallKey(input: {
|
|
|
131
131
|
}): string;
|
|
132
132
|
export declare function workflowJournalHash(entryWithoutEntryHash: unknown): string;
|
|
133
133
|
export declare function readWorkflowJournal(journalPath: string): Promise<WorkflowJournalReadResult>;
|
|
134
|
-
export declare function cleanupWorkflowJournalTranscriptDir(transcriptDir: string): Promise<void>;
|
|
135
134
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { constants as fsConstants } from 'node:fs';
|
|
3
|
-
import { chmod, lstat, mkdir, open, readFile,
|
|
3
|
+
import { chmod, lstat, mkdir, open, readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
export class WorkflowJournalError extends Error {
|
|
6
6
|
cause;
|
|
@@ -250,9 +250,6 @@ export async function readWorkflowJournal(journalPath) {
|
|
|
250
250
|
validateWorkflowJournal(entries);
|
|
251
251
|
return { entries, truncatedTail };
|
|
252
252
|
}
|
|
253
|
-
export async function cleanupWorkflowJournalTranscriptDir(transcriptDir) {
|
|
254
|
-
await rm(transcriptDir, { recursive: true, force: true });
|
|
255
|
-
}
|
|
256
253
|
function parseJournalLine(line, lineNumber) {
|
|
257
254
|
if (Buffer.byteLength(line, 'utf8') > MAX_LINE_BYTES) {
|
|
258
255
|
throw new WorkflowJournalValidationError(`journal line ${lineNumber} exceeds ${MAX_LINE_BYTES} bytes.`);
|
|
@@ -32,6 +32,9 @@ export type WorkflowEvent = {
|
|
|
32
32
|
readonly phaseIndex: number;
|
|
33
33
|
readonly title: string;
|
|
34
34
|
readonly detail?: string;
|
|
35
|
+
readonly goal?: string;
|
|
36
|
+
readonly plannedAgentCount?: number;
|
|
37
|
+
readonly plannedAgents?: readonly WorkflowPlanAgent[];
|
|
35
38
|
} | {
|
|
36
39
|
readonly type: 'workflow.plan.ready';
|
|
37
40
|
readonly taskId: string;
|
|
@@ -39,6 +42,15 @@ export type WorkflowEvent = {
|
|
|
39
42
|
readonly mode: string;
|
|
40
43
|
readonly rationale?: string;
|
|
41
44
|
readonly phases: readonly WorkflowPlanPhase[];
|
|
45
|
+
} | {
|
|
46
|
+
readonly type: 'workflow.phase.planned';
|
|
47
|
+
readonly taskId: string;
|
|
48
|
+
readonly runId: string;
|
|
49
|
+
readonly phaseIndex: number;
|
|
50
|
+
readonly title: string;
|
|
51
|
+
readonly goal?: string;
|
|
52
|
+
readonly plannedAgentCount: number;
|
|
53
|
+
readonly plannedAgents: readonly WorkflowPlanAgent[];
|
|
42
54
|
} | {
|
|
43
55
|
readonly type: 'workflow.log';
|
|
44
56
|
readonly taskId: string;
|
|
@@ -65,6 +77,11 @@ export type WorkflowEvent = {
|
|
|
65
77
|
readonly toolCalls: number;
|
|
66
78
|
readonly resultPreview?: string;
|
|
67
79
|
readonly cached?: boolean;
|
|
80
|
+
readonly elapsedMs: number;
|
|
81
|
+
readonly completedAgentCount: number;
|
|
82
|
+
readonly knownAgentCount: number;
|
|
83
|
+
readonly phaseCompletedAgentCount?: number;
|
|
84
|
+
readonly phaseKnownAgentCount?: number;
|
|
68
85
|
readonly worktreePath?: string;
|
|
69
86
|
readonly worktreePreserved?: boolean;
|
|
70
87
|
readonly preservedWorktrees?: readonly WorkflowAgentPreservedWorktree[];
|
|
@@ -211,7 +228,7 @@ interface BuiltinWorkflow {
|
|
|
211
228
|
export interface WorkflowAgentPreservedWorktree {
|
|
212
229
|
readonly path: string;
|
|
213
230
|
readonly attemptIndex: number;
|
|
214
|
-
readonly reason: '
|
|
231
|
+
readonly reason: 'clean' | 'changed' | 'stalled' | 'aborted' | 'status_unavailable';
|
|
215
232
|
}
|
|
216
233
|
export declare class WorkflowTaskRegistry implements WorkflowRuntime {
|
|
217
234
|
private readonly options;
|
|
@@ -270,6 +287,7 @@ export declare class WorkflowTaskRegistry implements WorkflowRuntime {
|
|
|
270
287
|
private parallel;
|
|
271
288
|
private pipeline;
|
|
272
289
|
private announcePlan;
|
|
290
|
+
private announcePhasePlan;
|
|
273
291
|
private phase;
|
|
274
292
|
private completeTask;
|
|
275
293
|
private failTask;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { chmod, mkdir, readdir, readFile, realpath,
|
|
3
|
+
import { chmod, mkdir, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { createContext, runInContext } from 'node:vm';
|
|
8
8
|
import { UltracodeRequestError, estimateTokens } from './types.js';
|
|
9
|
-
import { WORKFLOW_JOURNAL_GENESIS_AGENT_CALL_KEY, WORKFLOW_JOURNAL_WRITE_FAILED_REASON, WorkflowJournalError, WorkflowJournalWriter,
|
|
9
|
+
import { WORKFLOW_JOURNAL_GENESIS_AGENT_CALL_KEY, WORKFLOW_JOURNAL_WRITE_FAILED_REASON, WorkflowJournalError, WorkflowJournalWriter, computeWorkflowAgentCallKey, isWorkflowJournalError, normalizeJournalJsonValue, readWorkflowJournal, workflowJournalPath, } from './workflow-journal.js';
|
|
10
10
|
const MAX_SCRIPT_BYTES = 64 * 1024;
|
|
11
11
|
const MAX_AGENT_CALLS = 1000;
|
|
12
12
|
const MAX_PARALLELISM = 16;
|
|
@@ -141,6 +141,15 @@ const DEFAULT_BUILTIN_WORKFLOWS = [
|
|
|
141
141
|
};
|
|
142
142
|
const input = args && typeof args === "object" ? args : {};
|
|
143
143
|
const prompts = Array.isArray(input.prompts) ? input.prompts : [];
|
|
144
|
+
if (prompts.length > 0) {
|
|
145
|
+
announcePhasePlan({
|
|
146
|
+
title: "Batch",
|
|
147
|
+
agents: prompts.map((_, index) => ({
|
|
148
|
+
title: "Batch " + (index + 1),
|
|
149
|
+
label: "batch-" + (index + 1)
|
|
150
|
+
}))
|
|
151
|
+
});
|
|
152
|
+
}
|
|
144
153
|
phase("Batch");
|
|
145
154
|
return await parallel(prompts.map((prompt, index) => () => agent(
|
|
146
155
|
prompt == null ? "" : "" + prompt,
|
|
@@ -214,10 +223,8 @@ const plan = await agent([
|
|
|
214
223
|
}
|
|
215
224
|
});
|
|
216
225
|
const selectedPhases = plan.mode === "single" ? [plan.phases[0]] : plan.phases;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
rationale: plan.rationale,
|
|
220
|
-
phases: selectedPhases.map((phasePlan) => ({
|
|
226
|
+
function plannedPhaseFor(phasePlan) {
|
|
227
|
+
return {
|
|
221
228
|
id: phasePlan.id,
|
|
222
229
|
title: phasePlan.title,
|
|
223
230
|
goal: phasePlan.goal,
|
|
@@ -229,11 +236,18 @@ announcePlan({
|
|
|
229
236
|
? ${JSON.stringify(`${input.name}-single`)}
|
|
230
237
|
: ${JSON.stringify(`${input.name}-`)} + phasePlan.id + "-" + phaseAgent.id
|
|
231
238
|
}))
|
|
232
|
-
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const firstPhasePlan = plannedPhaseFor(selectedPhases[0]);
|
|
242
|
+
announcePlan({
|
|
243
|
+
mode: plan.mode,
|
|
244
|
+
rationale: plan.rationale,
|
|
245
|
+
phases: [firstPhasePlan]
|
|
233
246
|
});
|
|
234
247
|
if (plan.mode === "single") {
|
|
235
|
-
const singlePhase =
|
|
248
|
+
const singlePhase = firstPhasePlan;
|
|
236
249
|
const singleAgent = singlePhase.agents[0];
|
|
250
|
+
announcePhasePlan(singlePhase);
|
|
237
251
|
phase(singlePhase.title);
|
|
238
252
|
return await agent([
|
|
239
253
|
"Single-agent execution selected by the LLM planner.",
|
|
@@ -253,7 +267,9 @@ if (plan.mode === "single") {
|
|
|
253
267
|
}
|
|
254
268
|
const phaseOutputs = [];
|
|
255
269
|
const priorSummaries = [];
|
|
256
|
-
for (const
|
|
270
|
+
for (const rawPhasePlan of selectedPhases) {
|
|
271
|
+
const phasePlan = plannedPhaseFor(rawPhasePlan);
|
|
272
|
+
announcePhasePlan(phasePlan);
|
|
257
273
|
phase(phasePlan.title);
|
|
258
274
|
const agents = phasePlan.agents;
|
|
259
275
|
const agentOutputs = agents.length < 2
|
|
@@ -398,11 +414,6 @@ export class WorkflowTaskRegistry {
|
|
|
398
414
|
}
|
|
399
415
|
}
|
|
400
416
|
catch (err) {
|
|
401
|
-
await cleanupWorkflowJournalTranscriptDir(transcriptDir).catch(() => undefined);
|
|
402
|
-
if (!resolved.scriptPath) {
|
|
403
|
-
await rm(scriptPath, { force: true }).catch(() => undefined);
|
|
404
|
-
await rm(workflowScriptMetadataPath(scriptPath), { force: true }).catch(() => undefined);
|
|
405
|
-
}
|
|
406
417
|
throw workflowJournalRequestError(err);
|
|
407
418
|
}
|
|
408
419
|
const task = {
|
|
@@ -970,9 +981,11 @@ export class WorkflowTaskRegistry {
|
|
|
970
981
|
controller.abort();
|
|
971
982
|
return;
|
|
972
983
|
}
|
|
973
|
-
const workflowTimer =
|
|
974
|
-
|
|
975
|
-
|
|
984
|
+
const workflowTimer = this.options.requestTimeoutMs > 0
|
|
985
|
+
? setTimeout(() => {
|
|
986
|
+
controller.abort();
|
|
987
|
+
}, this.options.requestTimeoutMs)
|
|
988
|
+
: null;
|
|
976
989
|
try {
|
|
977
990
|
if (controller.signal.aborted)
|
|
978
991
|
throw workflowInputError('Workflow is aborted.');
|
|
@@ -1002,9 +1015,7 @@ export class WorkflowTaskRegistry {
|
|
|
1002
1015
|
toolCalls: ctx.toolCalls,
|
|
1003
1016
|
durationMs: Date.now() - ctx.startedAt,
|
|
1004
1017
|
});
|
|
1005
|
-
|
|
1006
|
-
await rm(resultPath, { force: true }).catch(() => undefined);
|
|
1007
|
-
}
|
|
1018
|
+
void completedSnapshot;
|
|
1008
1019
|
}
|
|
1009
1020
|
catch (err) {
|
|
1010
1021
|
const abortFailure = controller.signal.aborted
|
|
@@ -1014,7 +1025,8 @@ export class WorkflowTaskRegistry {
|
|
|
1014
1025
|
await this.failTask(task, abortFailure ? abortFailure.message : workflowErrorMessage(err), abortFailure ? abortFailure.reason : workflowFailureReason(err));
|
|
1015
1026
|
}
|
|
1016
1027
|
finally {
|
|
1017
|
-
|
|
1028
|
+
if (workflowTimer)
|
|
1029
|
+
clearTimeout(workflowTimer);
|
|
1018
1030
|
for (const timer of ctx.timers.values())
|
|
1019
1031
|
clearTimeout(timer);
|
|
1020
1032
|
ctx.timers.clear();
|
|
@@ -1078,6 +1090,7 @@ export class WorkflowTaskRegistry {
|
|
|
1078
1090
|
return this.trackWorkflowPromise(ctx, this.workspaceContext(ctx, options));
|
|
1079
1091
|
});
|
|
1080
1092
|
host.announcePlan = hardenCallable((plan) => this.announcePlan(ctx, plan));
|
|
1093
|
+
host.announcePhasePlan = hardenCallable((phasePlan) => this.announcePhasePlan(ctx, phasePlan));
|
|
1081
1094
|
host.phase = hardenCallable((title) => this.phase(ctx, title));
|
|
1082
1095
|
host.log = hardenCallable(log);
|
|
1083
1096
|
host.consoleLog = hardenCallable((...values) => {
|
|
@@ -1215,6 +1228,7 @@ export class WorkflowTaskRegistry {
|
|
|
1215
1228
|
toolCalls: 0,
|
|
1216
1229
|
resultPreview: previewValue(cached.result, 160),
|
|
1217
1230
|
cached: true,
|
|
1231
|
+
...agentCompletionProgress(ctx, phase),
|
|
1218
1232
|
});
|
|
1219
1233
|
return cached.result;
|
|
1220
1234
|
}
|
|
@@ -1278,6 +1292,7 @@ export class WorkflowTaskRegistry {
|
|
|
1278
1292
|
tokens: usage.totalTokens,
|
|
1279
1293
|
toolCalls,
|
|
1280
1294
|
resultPreview: previewValue(journalResult, 160),
|
|
1295
|
+
...agentCompletionProgress(ctx, phase),
|
|
1281
1296
|
...preservedWorktreeEventProjection(preservedWorktrees),
|
|
1282
1297
|
});
|
|
1283
1298
|
return journalResult;
|
|
@@ -1343,7 +1358,6 @@ export class WorkflowTaskRegistry {
|
|
|
1343
1358
|
};
|
|
1344
1359
|
}
|
|
1345
1360
|
catch (err) {
|
|
1346
|
-
await rm(worktreePath, { recursive: true, force: true }).catch(() => undefined);
|
|
1347
1361
|
throw workflowInputError(`worktree isolation could not create an isolated worktree: ${workflowErrorMessage(err)}`);
|
|
1348
1362
|
}
|
|
1349
1363
|
}
|
|
@@ -1364,16 +1378,10 @@ export class WorkflowTaskRegistry {
|
|
|
1364
1378
|
preservedWorktree: preservedWorktree(worktree, 'changed'),
|
|
1365
1379
|
};
|
|
1366
1380
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
return {
|
|
1372
|
-
preserved: true,
|
|
1373
|
-
preservedWorktree: preservedWorktree(worktree, 'cleanup_failed'),
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
return { preserved: false };
|
|
1381
|
+
return {
|
|
1382
|
+
preserved: true,
|
|
1383
|
+
preservedWorktree: preservedWorktree(worktree, 'clean'),
|
|
1384
|
+
};
|
|
1377
1385
|
}
|
|
1378
1386
|
async runAgentWithStallRetries(ctx, input) {
|
|
1379
1387
|
for (let retryIndex = 0; retryIndex <= this.agentStallRetryLimit; retryIndex += 1) {
|
|
@@ -1448,10 +1456,12 @@ export class WorkflowTaskRegistry {
|
|
|
1448
1456
|
throw workflowInputError('Workflow is aborted.');
|
|
1449
1457
|
}
|
|
1450
1458
|
ctx.controller.signal.addEventListener('abort', abortFromWorkflow, { once: true });
|
|
1451
|
-
const timer =
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1459
|
+
const timer = this.agentStallTimeoutMs > 0
|
|
1460
|
+
? setTimeout(() => {
|
|
1461
|
+
timedOut = true;
|
|
1462
|
+
attemptController.abort();
|
|
1463
|
+
}, this.agentStallTimeoutMs)
|
|
1464
|
+
: null;
|
|
1455
1465
|
try {
|
|
1456
1466
|
const generated = this.options.backend.generate(request, attemptController.signal).then((result) => ({ type: 'result', result }), (err) => ({ type: 'error', error: err }));
|
|
1457
1467
|
const aborted = new Promise((resolve) => {
|
|
@@ -1474,7 +1484,8 @@ export class WorkflowTaskRegistry {
|
|
|
1474
1484
|
throw outcome.error;
|
|
1475
1485
|
}
|
|
1476
1486
|
finally {
|
|
1477
|
-
|
|
1487
|
+
if (timer)
|
|
1488
|
+
clearTimeout(timer);
|
|
1478
1489
|
ctx.controller.signal.removeEventListener('abort', abortFromWorkflow);
|
|
1479
1490
|
}
|
|
1480
1491
|
}
|
|
@@ -1529,6 +1540,7 @@ export class WorkflowTaskRegistry {
|
|
|
1529
1540
|
throw workflowInputError('Workflow is aborted.');
|
|
1530
1541
|
}
|
|
1531
1542
|
const normalized = normalizeWorkflowExecutionPlan(plan);
|
|
1543
|
+
ctx.announcedPlan = normalized;
|
|
1532
1544
|
this.emit(ctx.task, {
|
|
1533
1545
|
type: 'workflow.plan.ready',
|
|
1534
1546
|
taskId: ctx.task.taskId,
|
|
@@ -1536,22 +1548,54 @@ export class WorkflowTaskRegistry {
|
|
|
1536
1548
|
...normalized,
|
|
1537
1549
|
});
|
|
1538
1550
|
}
|
|
1551
|
+
announcePhasePlan(ctx, phasePlan) {
|
|
1552
|
+
if (ctx.controller.signal.aborted || ctx.task.status !== 'running') {
|
|
1553
|
+
throw workflowInputError('Workflow is aborted.');
|
|
1554
|
+
}
|
|
1555
|
+
const normalized = normalizeWorkflowPhasePlan(phasePlan, 'announcePhasePlan(phasePlan)');
|
|
1556
|
+
ctx.pendingPhasePlan = normalized;
|
|
1557
|
+
const phaseIndex = ctx.task.events
|
|
1558
|
+
.filter((event) => event.type === 'workflow.phase.started')
|
|
1559
|
+
.length;
|
|
1560
|
+
this.emit(ctx.task, {
|
|
1561
|
+
type: 'workflow.phase.planned',
|
|
1562
|
+
taskId: ctx.task.taskId,
|
|
1563
|
+
runId: ctx.task.runId,
|
|
1564
|
+
phaseIndex,
|
|
1565
|
+
title: normalized.title,
|
|
1566
|
+
...(normalized.goal ? { goal: normalized.goal } : {}),
|
|
1567
|
+
plannedAgentCount: normalized.agents.length,
|
|
1568
|
+
plannedAgents: normalized.agents,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1539
1571
|
phase(ctx, title) {
|
|
1540
1572
|
if (typeof title !== 'string' || title.trim() === '') {
|
|
1541
1573
|
throw workflowInputError('phase() requires a non-empty string title.');
|
|
1542
1574
|
}
|
|
1543
|
-
|
|
1575
|
+
const normalizedTitle = title.trim();
|
|
1576
|
+
ctx.currentPhase = normalizedTitle;
|
|
1544
1577
|
const phaseIndex = ctx.task.events
|
|
1545
1578
|
.filter((event) => event.type === 'workflow.phase.started')
|
|
1546
1579
|
.length;
|
|
1547
|
-
const detail = ctx.parsed.meta.phases?.find((item) => item.title ===
|
|
1580
|
+
const detail = ctx.parsed.meta.phases?.find((item) => item.title === normalizedTitle)?.detail;
|
|
1581
|
+
const pendingPhase = ctx.pendingPhasePlan?.title === normalizedTitle
|
|
1582
|
+
? ctx.pendingPhasePlan
|
|
1583
|
+
: undefined;
|
|
1584
|
+
if (pendingPhase)
|
|
1585
|
+
ctx.pendingPhasePlan = undefined;
|
|
1586
|
+
const plannedPhase = pendingPhase ?? workflowPlannedPhase(ctx.announcedPlan, phaseIndex, normalizedTitle);
|
|
1548
1587
|
this.emit(ctx.task, {
|
|
1549
1588
|
type: 'workflow.phase.started',
|
|
1550
1589
|
taskId: ctx.task.taskId,
|
|
1551
1590
|
runId: ctx.task.runId,
|
|
1552
1591
|
phaseIndex,
|
|
1553
|
-
title,
|
|
1592
|
+
title: normalizedTitle,
|
|
1554
1593
|
...(detail ? { detail } : {}),
|
|
1594
|
+
...(plannedPhase?.goal ? { goal: plannedPhase.goal } : {}),
|
|
1595
|
+
...(plannedPhase ? {
|
|
1596
|
+
plannedAgentCount: plannedPhase.agents.length,
|
|
1597
|
+
plannedAgents: plannedPhase.agents,
|
|
1598
|
+
} : {}),
|
|
1555
1599
|
});
|
|
1556
1600
|
}
|
|
1557
1601
|
async completeTask(ctx, result, event) {
|
|
@@ -1946,19 +1990,6 @@ function uniqueStrings(values) {
|
|
|
1946
1990
|
}
|
|
1947
1991
|
return out;
|
|
1948
1992
|
}
|
|
1949
|
-
async function removeCleanGitWorktree(worktree) {
|
|
1950
|
-
try {
|
|
1951
|
-
await gitOutput(worktree.gitRoot, ['worktree', 'remove', '--force', worktree.path]);
|
|
1952
|
-
}
|
|
1953
|
-
catch (err) {
|
|
1954
|
-
if (/No such file|not a working tree|is not a working tree/i.test(workflowErrorMessage(err))) {
|
|
1955
|
-
await rm(worktree.path, { recursive: true, force: true }).catch(() => undefined);
|
|
1956
|
-
await gitOutput(worktree.gitRoot, ['worktree', 'prune']).catch(() => undefined);
|
|
1957
|
-
return;
|
|
1958
|
-
}
|
|
1959
|
-
throw err;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
1993
|
function preservedWorktree(worktree, reason) {
|
|
1963
1994
|
return {
|
|
1964
1995
|
path: worktree.path,
|
|
@@ -1970,7 +2001,7 @@ function preservedWorktreeEventProjection(preservedWorktrees) {
|
|
|
1970
2001
|
if (preservedWorktrees.length === 0)
|
|
1971
2002
|
return {};
|
|
1972
2003
|
const primary = preservedWorktrees.find((item) => item.reason === 'changed')
|
|
1973
|
-
?? preservedWorktrees.find((item) => item.reason === '
|
|
2004
|
+
?? preservedWorktrees.find((item) => item.reason === 'status_unavailable')
|
|
1974
2005
|
?? preservedWorktrees[0];
|
|
1975
2006
|
return {
|
|
1976
2007
|
worktreePath: primary?.path,
|
|
@@ -1978,6 +2009,33 @@ function preservedWorktreeEventProjection(preservedWorktrees) {
|
|
|
1978
2009
|
preservedWorktrees: [...preservedWorktrees],
|
|
1979
2010
|
};
|
|
1980
2011
|
}
|
|
2012
|
+
function workflowPlannedPhase(plan, phaseIndex, title) {
|
|
2013
|
+
const indexed = plan?.phases[phaseIndex];
|
|
2014
|
+
if (indexed?.title === title)
|
|
2015
|
+
return indexed;
|
|
2016
|
+
return plan?.phases.find((phase) => phase.title === title);
|
|
2017
|
+
}
|
|
2018
|
+
function agentCompletionProgress(ctx, phase) {
|
|
2019
|
+
const completedAgentCount = ctx.task.events
|
|
2020
|
+
.filter((event) => event.type === 'workflow.agent.completed')
|
|
2021
|
+
.length + 1;
|
|
2022
|
+
const base = {
|
|
2023
|
+
elapsedMs: Date.now() - ctx.startedAt,
|
|
2024
|
+
completedAgentCount,
|
|
2025
|
+
knownAgentCount: ctx.agentCount,
|
|
2026
|
+
};
|
|
2027
|
+
if (!phase)
|
|
2028
|
+
return base;
|
|
2029
|
+
const phaseCompletedAgentCount = ctx.task.events
|
|
2030
|
+
.filter((event) => event.type === 'workflow.agent.completed' && event.phase === phase)
|
|
2031
|
+
.length + 1;
|
|
2032
|
+
const phaseKnownAgentCount = Math.max(phaseCompletedAgentCount, ctx.task.events.filter((event) => event.type === 'workflow.agent.started' && event.phase === phase).length);
|
|
2033
|
+
return {
|
|
2034
|
+
...base,
|
|
2035
|
+
phaseCompletedAgentCount,
|
|
2036
|
+
phaseKnownAgentCount,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
1981
2039
|
function shortHash(value) {
|
|
1982
2040
|
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
1983
2041
|
}
|
|
@@ -2684,6 +2742,8 @@ function normalizeAgentStallTimeoutMs(configured, requestTimeoutMs) {
|
|
|
2684
2742
|
if (configured !== undefined && Number.isFinite(configured) && configured > 0) {
|
|
2685
2743
|
return Math.max(1, Math.floor(configured));
|
|
2686
2744
|
}
|
|
2745
|
+
if (configured === 0 || requestTimeoutMs === 0)
|
|
2746
|
+
return 0;
|
|
2687
2747
|
return Math.max(1, Math.floor(requestTimeoutMs));
|
|
2688
2748
|
}
|
|
2689
2749
|
function workflowTaskSnapshot(task) {
|
|
@@ -2972,6 +3032,7 @@ function installWorkflowVmGlobals(context, globals) {
|
|
|
2972
3032
|
' define(globalThis, "pipeline", { value: (...values) => __host.pipeline(...values), writable: false, configurable: false });',
|
|
2973
3033
|
' define(globalThis, "workspaceContext", { value: (...values) => __host.workspaceContext(...values), writable: false, configurable: false });',
|
|
2974
3034
|
' define(globalThis, "announcePlan", { value: (...values) => __host.announcePlan(...values), writable: false, configurable: false });',
|
|
3035
|
+
' define(globalThis, "announcePhasePlan", { value: (...values) => __host.announcePhasePlan(...values), writable: false, configurable: false });',
|
|
2975
3036
|
' define(globalThis, "phase", { value: (...values) => __host.phase(...values), writable: false, configurable: false });',
|
|
2976
3037
|
' define(globalThis, "log", { value: (...values) => __host.log(...values), writable: false, configurable: false });',
|
|
2977
3038
|
' define(globalThis, "workflow", { value: (...values) => __host.workflow(...values), writable: false, configurable: false });',
|
|
@@ -3664,38 +3725,41 @@ function normalizeWorkflowExecutionPlan(value) {
|
|
|
3664
3725
|
const rawPhases = Array.isArray(record.phases) ? Array.from(record.phases) : [];
|
|
3665
3726
|
if (rawPhases.length === 0)
|
|
3666
3727
|
throw workflowInputError('announcePlan(plan) requires at least one phase.');
|
|
3667
|
-
const phases = rawPhases
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
throw workflowInputError(`announcePlan(plan).phases[${phaseIndex}] must be an object.`);
|
|
3671
|
-
const rawAgents = Array.isArray(phase.agents) ? Array.from(phase.agents) : [];
|
|
3672
|
-
if (rawAgents.length === 0) {
|
|
3673
|
-
throw workflowInputError(`announcePlan(plan).phases[${phaseIndex}].agents requires at least one agent.`);
|
|
3674
|
-
}
|
|
3675
|
-
return {
|
|
3676
|
-
...(typeof phase.id === 'string' && phase.id.trim() ? { id: boundedPlanString(phase.id, '', 48) } : {}),
|
|
3677
|
-
title: boundedPlanString(phase.title, `Phase ${phaseIndex + 1}`, 96),
|
|
3678
|
-
...(typeof phase.goal === 'string' && phase.goal.trim() ? { goal: boundedPlanString(phase.goal, '', 600) } : {}),
|
|
3679
|
-
agents: rawAgents.slice(0, 16).map((agentValue, agentIndex) => {
|
|
3680
|
-
const agent = asRecord(agentValue);
|
|
3681
|
-
if (!agent) {
|
|
3682
|
-
throw workflowInputError(`announcePlan(plan).phases[${phaseIndex}].agents[${agentIndex}] must be an object.`);
|
|
3683
|
-
}
|
|
3684
|
-
return {
|
|
3685
|
-
...(typeof agent.id === 'string' && agent.id.trim() ? { id: boundedPlanString(agent.id, '', 48) } : {}),
|
|
3686
|
-
title: boundedPlanString(agent.title, `Agent ${agentIndex + 1}`, 96),
|
|
3687
|
-
...(typeof agent.focus === 'string' && agent.focus.trim() ? { focus: boundedPlanString(agent.focus, '', 600) } : {}),
|
|
3688
|
-
...(typeof agent.label === 'string' && agent.label.trim() ? { label: boundedPlanString(agent.label, '', 96) } : {}),
|
|
3689
|
-
};
|
|
3690
|
-
}),
|
|
3691
|
-
};
|
|
3692
|
-
});
|
|
3728
|
+
const phases = rawPhases
|
|
3729
|
+
.slice(0, 16)
|
|
3730
|
+
.map((phaseValue, phaseIndex) => normalizeWorkflowPhasePlan(phaseValue, `announcePlan(plan).phases[${phaseIndex}]`, phaseIndex));
|
|
3693
3731
|
return {
|
|
3694
3732
|
mode,
|
|
3695
3733
|
...(rationale ? { rationale } : {}),
|
|
3696
3734
|
phases,
|
|
3697
3735
|
};
|
|
3698
3736
|
}
|
|
3737
|
+
function normalizeWorkflowPhasePlan(value, label, phaseIndex = 0) {
|
|
3738
|
+
const phase = asRecord(value);
|
|
3739
|
+
if (!phase)
|
|
3740
|
+
throw workflowInputError(`${label} must be an object.`);
|
|
3741
|
+
const rawAgents = Array.isArray(phase.agents) ? Array.from(phase.agents) : [];
|
|
3742
|
+
if (rawAgents.length === 0) {
|
|
3743
|
+
throw workflowInputError(`${label}.agents requires at least one agent.`);
|
|
3744
|
+
}
|
|
3745
|
+
return {
|
|
3746
|
+
...(typeof phase.id === 'string' && phase.id.trim() ? { id: boundedPlanString(phase.id, '', 48) } : {}),
|
|
3747
|
+
title: boundedPlanString(phase.title, `Phase ${phaseIndex + 1}`, 96),
|
|
3748
|
+
...(typeof phase.goal === 'string' && phase.goal.trim() ? { goal: boundedPlanString(phase.goal, '', 600) } : {}),
|
|
3749
|
+
agents: rawAgents.slice(0, 16).map((agentValue, agentIndex) => {
|
|
3750
|
+
const agent = asRecord(agentValue);
|
|
3751
|
+
if (!agent) {
|
|
3752
|
+
throw workflowInputError(`${label}.agents[${agentIndex}] must be an object.`);
|
|
3753
|
+
}
|
|
3754
|
+
return {
|
|
3755
|
+
...(typeof agent.id === 'string' && agent.id.trim() ? { id: boundedPlanString(agent.id, '', 48) } : {}),
|
|
3756
|
+
title: boundedPlanString(agent.title, `Agent ${agentIndex + 1}`, 96),
|
|
3757
|
+
...(typeof agent.focus === 'string' && agent.focus.trim() ? { focus: boundedPlanString(agent.focus, '', 600) } : {}),
|
|
3758
|
+
...(typeof agent.label === 'string' && agent.label.trim() ? { label: boundedPlanString(agent.label, '', 96) } : {}),
|
|
3759
|
+
};
|
|
3760
|
+
}),
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3699
3763
|
function boundedPlanString(value, fallback, limit) {
|
|
3700
3764
|
const text = typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
3701
3765
|
return preview(text, limit);
|
package/dist/settings.js
CHANGED
|
@@ -34,7 +34,7 @@ export function loadSettings() {
|
|
|
34
34
|
progress: readWorkflowProgressModeSetting(workflow?.progress, 'workflow.progress'),
|
|
35
35
|
permission: readWorkflowPermissionPolicySetting(workflow?.permission, 'workflow.permission'),
|
|
36
36
|
retryLimit: readNonNegativeIntegerSetting(workflow?.retryLimit, 'workflow.retryLimit'),
|
|
37
|
-
timeoutMs:
|
|
37
|
+
timeoutMs: readNonNegativeIntegerSetting(workflow?.timeoutMs, 'workflow.timeoutMs'),
|
|
38
38
|
background: {
|
|
39
39
|
runDir: readTemplateSetting(background?.runDir, 'workflow.background.runDir', true),
|
|
40
40
|
resultFile: readRelativePathSetting(background?.resultFile, 'workflow.background.resultFile'),
|
|
@@ -119,11 +119,6 @@ function readNonNegativeIntegerSetting(value, key) {
|
|
|
119
119
|
return value;
|
|
120
120
|
throw new Error(`${key} must be a non-negative integer.`);
|
|
121
121
|
}
|
|
122
|
-
function readPositiveIntegerSetting(value, key) {
|
|
123
|
-
if (typeof value === 'number' && Number.isInteger(value) && value > 0)
|
|
124
|
-
return value;
|
|
125
|
-
throw new Error(`${key} must be a positive integer.`);
|
|
126
|
-
}
|
|
127
122
|
function readTemplateSetting(value, key, requireJobId) {
|
|
128
123
|
const text = readNonEmptyStringSetting(value, key);
|
|
129
124
|
if (requireJobId && !text.includes('{jobId}')) {
|
package/docs/provenance-audit.md
CHANGED
|
@@ -7,7 +7,7 @@ Date: 2026-06-22
|
|
|
7
7
|
This audit checked:
|
|
8
8
|
|
|
9
9
|
- tracked repository files;
|
|
10
|
-
- generated npm package contents for `ultracode-for-codex@0.
|
|
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
|
|
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
|
|