@chllming/wave-orchestration 0.9.0 → 0.9.2
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/CHANGELOG.md +57 -0
- package/LICENSE.md +21 -0
- package/README.md +133 -20
- package/docs/README.md +12 -4
- package/docs/agents/wave-security-role.md +1 -0
- package/docs/architecture/README.md +1498 -0
- package/docs/concepts/operating-modes.md +2 -2
- package/docs/guides/author-and-run-waves.md +14 -4
- package/docs/guides/planner.md +2 -2
- package/docs/guides/{recommendations-0.9.0.md → recommendations-0.9.2.md} +8 -7
- package/docs/guides/sandboxed-environments.md +158 -0
- package/docs/guides/terminal-surfaces.md +14 -12
- package/docs/plans/current-state.md +11 -3
- package/docs/plans/end-state-architecture.md +3 -1
- package/docs/plans/examples/wave-example-design-handoff.md +1 -1
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +70 -19
- package/docs/plans/sandbox-end-state-architecture.md +153 -0
- package/docs/reference/cli-reference.md +71 -7
- package/docs/reference/coordination-and-closure.md +18 -1
- package/docs/reference/corridor.md +225 -0
- package/docs/reference/github-packages-setup.md +1 -1
- package/docs/reference/migration-0.2-to-0.5.md +9 -7
- package/docs/reference/npmjs-token-publishing.md +53 -0
- package/docs/reference/npmjs-trusted-publishing.md +4 -50
- package/docs/reference/package-publishing-flow.md +272 -0
- package/docs/reference/runtime-config/README.md +61 -3
- package/docs/reference/sample-waves.md +5 -5
- package/docs/reference/skills.md +1 -1
- package/docs/reference/wave-control.md +358 -27
- package/docs/roadmap.md +39 -204
- package/package.json +1 -1
- package/releases/manifest.json +38 -0
- package/scripts/wave-cli-bootstrap.mjs +52 -1
- package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
- package/scripts/wave-orchestrator/agent-state.mjs +0 -1
- package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
- package/scripts/wave-orchestrator/autonomous.mjs +47 -14
- package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
- package/scripts/wave-orchestrator/config.mjs +199 -3
- package/scripts/wave-orchestrator/context7.mjs +231 -29
- package/scripts/wave-orchestrator/control-cli.mjs +42 -5
- package/scripts/wave-orchestrator/coordination.mjs +14 -0
- package/scripts/wave-orchestrator/corridor.mjs +363 -0
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +115 -43
- package/scripts/wave-orchestrator/derived-state-engine.mjs +44 -4
- package/scripts/wave-orchestrator/gate-engine.mjs +126 -38
- package/scripts/wave-orchestrator/install.mjs +46 -0
- package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +290 -75
- package/scripts/wave-orchestrator/launcher.mjs +201 -53
- package/scripts/wave-orchestrator/ledger.mjs +7 -2
- package/scripts/wave-orchestrator/planner.mjs +1 -0
- package/scripts/wave-orchestrator/projection-writer.mjs +36 -1
- package/scripts/wave-orchestrator/provider-runtime.mjs +104 -0
- package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
- package/scripts/wave-orchestrator/retry-control.mjs +3 -3
- package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
- package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
- package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
- package/scripts/wave-orchestrator/shared.mjs +1 -0
- package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
- package/scripts/wave-orchestrator/terminals.mjs +12 -32
- package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
- package/scripts/wave-orchestrator/traces.mjs +25 -0
- package/scripts/wave-orchestrator/wave-control-client.mjs +14 -1
- package/scripts/wave-orchestrator/wave-files.mjs +38 -5
- package/scripts/wave.mjs +13 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
REPO_ROOT,
|
|
7
|
+
ensureDirectory,
|
|
8
|
+
readFileTail,
|
|
9
|
+
readJsonOrNull,
|
|
10
|
+
sleep,
|
|
11
|
+
toIsoTimestamp,
|
|
12
|
+
writeJsonAtomic,
|
|
13
|
+
} from "./shared.mjs";
|
|
14
|
+
|
|
15
|
+
export const AGENT_RUNTIME_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
16
|
+
|
|
17
|
+
function isProcessAlive(pid) {
|
|
18
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parsePositiveInt(value, fallback = null) {
|
|
30
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readRuntimeRecord(runtimePath) {
|
|
35
|
+
return runtimePath ? readJsonOrNull(runtimePath) || {} : {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeRuntimeRecord(runtimePath, payload) {
|
|
39
|
+
if (!runtimePath) {
|
|
40
|
+
return payload;
|
|
41
|
+
}
|
|
42
|
+
ensureDirectory(path.dirname(runtimePath));
|
|
43
|
+
writeJsonAtomic(runtimePath, payload);
|
|
44
|
+
return payload;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateRuntimeRecord(runtimePath, transform) {
|
|
48
|
+
const current = readRuntimeRecord(runtimePath);
|
|
49
|
+
const next = transform(current) || current;
|
|
50
|
+
return writeRuntimeRecord(runtimePath, next);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function appendLogLine(logPath, message) {
|
|
54
|
+
ensureDirectory(path.dirname(logPath));
|
|
55
|
+
fs.appendFileSync(logPath, `${String(message || "").trimEnd()}\n`, "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeExitCode(code, signal) {
|
|
59
|
+
if (Number.isInteger(code)) {
|
|
60
|
+
return code;
|
|
61
|
+
}
|
|
62
|
+
if (!signal) {
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
if (signal === "SIGTERM") {
|
|
66
|
+
return 143;
|
|
67
|
+
}
|
|
68
|
+
if (signal === "SIGINT") {
|
|
69
|
+
return 130;
|
|
70
|
+
}
|
|
71
|
+
if (signal === "SIGKILL") {
|
|
72
|
+
return 137;
|
|
73
|
+
}
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function exitReasonForOutcome(code, signal) {
|
|
78
|
+
if (signal) {
|
|
79
|
+
return signal.toLowerCase();
|
|
80
|
+
}
|
|
81
|
+
return Number(code) === 0 ? "completed" : "failed";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function terminalDispositionForOutcome(code, signal) {
|
|
85
|
+
if (signal === "SIGTERM" || signal === "SIGINT" || signal === "SIGKILL") {
|
|
86
|
+
return "terminated";
|
|
87
|
+
}
|
|
88
|
+
return Number(code) === 0 ? "completed" : "failed";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function terminateAgentProcessRuntime(runtimeRecord, { graceMs = 1000 } = {}) {
|
|
92
|
+
const pgid = parsePositiveInt(runtimeRecord?.pgid, null);
|
|
93
|
+
const candidatePid = parsePositiveInt(
|
|
94
|
+
runtimeRecord?.executorPid ?? runtimeRecord?.pid ?? runtimeRecord?.runnerPid,
|
|
95
|
+
null,
|
|
96
|
+
);
|
|
97
|
+
if (!pgid && !candidatePid) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const terminate = (signal) => {
|
|
101
|
+
if (pgid) {
|
|
102
|
+
try {
|
|
103
|
+
process.kill(-pgid, signal);
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
// fall through
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (candidatePid) {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(candidatePid, signal);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
// no-op
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
const sent = terminate("SIGTERM");
|
|
120
|
+
if (!sent) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
await sleep(graceMs);
|
|
124
|
+
if (
|
|
125
|
+
(pgid && isProcessAlive(pgid)) ||
|
|
126
|
+
(candidatePid && isProcessAlive(candidatePid))
|
|
127
|
+
) {
|
|
128
|
+
terminate("SIGKILL");
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildAgentAttachInfo(runtimeRecord) {
|
|
134
|
+
return {
|
|
135
|
+
sessionBackend: String(runtimeRecord?.sessionBackend || "process").trim() || "process",
|
|
136
|
+
attachMode: String(runtimeRecord?.attachMode || "log-tail").trim() || "log-tail",
|
|
137
|
+
sessionName: String(runtimeRecord?.sessionName || "").trim() || null,
|
|
138
|
+
tmuxSessionName: String(runtimeRecord?.tmuxSessionName || "").trim() || null,
|
|
139
|
+
logPath: String(runtimeRecord?.logPath || "").trim() || null,
|
|
140
|
+
statusPath: String(runtimeRecord?.statusPath || "").trim() || null,
|
|
141
|
+
terminalDisposition: String(runtimeRecord?.terminalDisposition || "").trim() || null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function renderAgentAttachFallback(runtimeRecord, { terminal = false } = {}) {
|
|
146
|
+
const logPath = String(runtimeRecord?.logPath || "").trim();
|
|
147
|
+
if (!logPath || !fs.existsSync(logPath)) {
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
return terminal ? readFileTail(logPath, 12000) : logPath;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function spawnAgentProcessRunner(payload, { env = process.env } = {}) {
|
|
154
|
+
const payloadPath = String(payload?.payloadPath || "").trim();
|
|
155
|
+
if (!payloadPath) {
|
|
156
|
+
throw new Error("Detached agent runner requires payloadPath.");
|
|
157
|
+
}
|
|
158
|
+
ensureDirectory(path.dirname(payloadPath));
|
|
159
|
+
writeJsonAtomic(payloadPath, payload);
|
|
160
|
+
const runnerPath = fileURLToPath(new URL("./agent-process-runner.mjs", import.meta.url));
|
|
161
|
+
const child = spawn(process.execPath, [runnerPath, "--payload-file", payloadPath], {
|
|
162
|
+
cwd: REPO_ROOT,
|
|
163
|
+
detached: true,
|
|
164
|
+
stdio: "ignore",
|
|
165
|
+
env,
|
|
166
|
+
});
|
|
167
|
+
child.unref();
|
|
168
|
+
return {
|
|
169
|
+
runnerPid: child.pid,
|
|
170
|
+
payloadPath,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseArgs(argv) {
|
|
175
|
+
const options = {
|
|
176
|
+
payloadFile: "",
|
|
177
|
+
};
|
|
178
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
179
|
+
const arg = argv[index];
|
|
180
|
+
if (arg === "--payload-file") {
|
|
181
|
+
options.payloadFile = String(argv[++index] || "").trim();
|
|
182
|
+
} else if (arg && arg !== "--") {
|
|
183
|
+
throw new Error(`Unknown agent-process-runner argument: ${arg}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!options.payloadFile) {
|
|
187
|
+
throw new Error("--payload-file is required");
|
|
188
|
+
}
|
|
189
|
+
return options;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function runAgentProcessRunner(payloadFile) {
|
|
193
|
+
const payload = readJsonOrNull(payloadFile);
|
|
194
|
+
if (!payload || typeof payload !== "object") {
|
|
195
|
+
throw new Error(`Invalid detached agent runner payload: ${payloadFile}`);
|
|
196
|
+
}
|
|
197
|
+
const runtimePath = String(payload.runtimePath || "").trim();
|
|
198
|
+
const statusPath = String(payload.statusPath || "").trim();
|
|
199
|
+
const logPath = String(payload.logPath || "").trim();
|
|
200
|
+
const command = String(payload.command || "").trim();
|
|
201
|
+
if (!statusPath || !logPath || !command) {
|
|
202
|
+
throw new Error("Detached agent runner payload is missing statusPath, logPath, or command.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const startedAt = toIsoTimestamp();
|
|
206
|
+
writeRuntimeRecord(runtimePath, {
|
|
207
|
+
runId: payload.runId || null,
|
|
208
|
+
waveNumber: Number(payload.waveNumber) || null,
|
|
209
|
+
attempt: Number(payload.attempt) || 1,
|
|
210
|
+
agentId: payload.agentId || null,
|
|
211
|
+
sessionName: payload.sessionName || null,
|
|
212
|
+
tmuxSessionName: null,
|
|
213
|
+
sessionBackend: "process",
|
|
214
|
+
attachMode: "log-tail",
|
|
215
|
+
runnerPid: process.pid,
|
|
216
|
+
executorPid: null,
|
|
217
|
+
pid: null,
|
|
218
|
+
pgid: null,
|
|
219
|
+
startedAt,
|
|
220
|
+
lastHeartbeatAt: startedAt,
|
|
221
|
+
statusPath,
|
|
222
|
+
logPath,
|
|
223
|
+
exitCode: null,
|
|
224
|
+
exitReason: null,
|
|
225
|
+
terminalDisposition: "launching",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
ensureDirectory(path.dirname(logPath));
|
|
229
|
+
const child = spawn("bash", ["-lc", command], {
|
|
230
|
+
cwd: REPO_ROOT,
|
|
231
|
+
detached: true,
|
|
232
|
+
stdio: "ignore",
|
|
233
|
+
env: {
|
|
234
|
+
...process.env,
|
|
235
|
+
...(payload.env && typeof payload.env === "object" ? payload.env : {}),
|
|
236
|
+
WAVE_ORCHESTRATOR_ID: String(payload.orchestratorId || ""),
|
|
237
|
+
WAVE_EXECUTOR_MODE: String(payload.executorId || ""),
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
const executorPid = parsePositiveInt(child.pid, null);
|
|
241
|
+
const pgid = executorPid;
|
|
242
|
+
const markRuntime = (patch = {}) =>
|
|
243
|
+
updateRuntimeRecord(runtimePath, (current) => ({
|
|
244
|
+
...current,
|
|
245
|
+
...patch,
|
|
246
|
+
lastHeartbeatAt: toIsoTimestamp(),
|
|
247
|
+
}));
|
|
248
|
+
markRuntime({
|
|
249
|
+
runnerPid: process.pid,
|
|
250
|
+
executorPid,
|
|
251
|
+
pid: executorPid,
|
|
252
|
+
pgid,
|
|
253
|
+
terminalDisposition: "running",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const heartbeat = setInterval(() => {
|
|
257
|
+
if (fs.existsSync(statusPath)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
markRuntime({
|
|
261
|
+
terminalDisposition: "running",
|
|
262
|
+
});
|
|
263
|
+
}, AGENT_RUNTIME_HEARTBEAT_INTERVAL_MS);
|
|
264
|
+
heartbeat.unref?.();
|
|
265
|
+
|
|
266
|
+
let forwardedSignal = "";
|
|
267
|
+
const handleSignal = async (signal) => {
|
|
268
|
+
forwardedSignal = signal;
|
|
269
|
+
try {
|
|
270
|
+
await terminateAgentProcessRuntime({ pgid, executorPid, pid: executorPid });
|
|
271
|
+
} catch {
|
|
272
|
+
// best-effort only
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
process.once("SIGTERM", () => {
|
|
276
|
+
void handleSignal("SIGTERM");
|
|
277
|
+
});
|
|
278
|
+
process.once("SIGINT", () => {
|
|
279
|
+
void handleSignal("SIGINT");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await new Promise((resolve, reject) => {
|
|
283
|
+
child.once("error", (error) => {
|
|
284
|
+
reject(error);
|
|
285
|
+
});
|
|
286
|
+
child.once("close", (code, signal) => {
|
|
287
|
+
clearInterval(heartbeat);
|
|
288
|
+
const completedAt = toIsoTimestamp();
|
|
289
|
+
const exitCode = normalizeExitCode(code, signal || forwardedSignal);
|
|
290
|
+
const exitReason = exitReasonForOutcome(exitCode, signal || forwardedSignal);
|
|
291
|
+
const terminalDisposition = terminalDispositionForOutcome(exitCode, signal || forwardedSignal);
|
|
292
|
+
writeJsonAtomic(statusPath, {
|
|
293
|
+
code: exitCode,
|
|
294
|
+
promptHash: payload.promptHash || null,
|
|
295
|
+
orchestratorId: payload.orchestratorId || null,
|
|
296
|
+
attempt: Number(payload.attempt) || 1,
|
|
297
|
+
completedAt,
|
|
298
|
+
});
|
|
299
|
+
markRuntime({
|
|
300
|
+
exitCode,
|
|
301
|
+
exitReason,
|
|
302
|
+
terminalDisposition,
|
|
303
|
+
});
|
|
304
|
+
appendLogLine(
|
|
305
|
+
logPath,
|
|
306
|
+
`[${payload.lane || "wave"}-wave-launcher] ${payload.sessionName || payload.agentId || "agent"} finished with code ${exitCode}`,
|
|
307
|
+
);
|
|
308
|
+
resolve();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
314
|
+
const options = parseArgs(process.argv.slice(2));
|
|
315
|
+
runAgentProcessRunner(options.payloadFile).catch((error) => {
|
|
316
|
+
const payload = readJsonOrNull(options.payloadFile);
|
|
317
|
+
const runtimePath = String(payload?.runtimePath || "").trim();
|
|
318
|
+
const statusPath = String(payload?.statusPath || "").trim();
|
|
319
|
+
const logPath = String(payload?.logPath || "").trim();
|
|
320
|
+
if (logPath) {
|
|
321
|
+
appendLogLine(logPath, `[wave-agent-runner] ${error instanceof Error ? error.message : String(error)}`);
|
|
322
|
+
}
|
|
323
|
+
if (runtimePath) {
|
|
324
|
+
updateRuntimeRecord(runtimePath, (current) => ({
|
|
325
|
+
...current,
|
|
326
|
+
runnerPid: process.pid,
|
|
327
|
+
exitCode: 1,
|
|
328
|
+
exitReason: error instanceof Error ? error.message : String(error),
|
|
329
|
+
terminalDisposition: "failed",
|
|
330
|
+
lastHeartbeatAt: toIsoTimestamp(),
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
if (statusPath && !fs.existsSync(statusPath)) {
|
|
334
|
+
writeJsonAtomic(statusPath, {
|
|
335
|
+
code: 1,
|
|
336
|
+
promptHash: payload?.promptHash || null,
|
|
337
|
+
orchestratorId: payload?.orchestratorId || null,
|
|
338
|
+
attempt: Number(payload?.attempt) || 1,
|
|
339
|
+
completedAt: toIsoTimestamp(),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
@@ -350,7 +350,6 @@ function detectTermination(agent, logText, statusRecord) {
|
|
|
350
350
|
const patterns = [
|
|
351
351
|
{ reason: "max-turns", regex: /Reached max turns \((\d+)\)/i },
|
|
352
352
|
{ reason: "timeout", regex: /(timed out(?: after [^\n.]+)?)/i },
|
|
353
|
-
{ reason: "session-missing", regex: /(session [^\n]+ disappeared before [^\n]+ was written)/i },
|
|
354
353
|
];
|
|
355
354
|
for (const pattern of patterns) {
|
|
356
355
|
const match = String(logText || "").match(pattern.regex);
|
|
@@ -101,6 +101,13 @@ export function normalizeRelaunchPlan(payload, defaults = {}) {
|
|
|
101
101
|
attempt: normalizeInteger(source.attempt, null),
|
|
102
102
|
phase: normalizeText(source.phase, null),
|
|
103
103
|
selectedAgentIds: Array.isArray(source.selectedAgentIds) ? source.selectedAgentIds : [],
|
|
104
|
+
resumeFromPhase: normalizeText(source.resumeFromPhase, null),
|
|
105
|
+
invalidatedAgentIds: normalizeStringArray(source.invalidatedAgentIds),
|
|
106
|
+
reusableAgentIds: normalizeStringArray(source.reusableAgentIds),
|
|
107
|
+
reusableProofBundleIds: normalizeStringArray(source.reusableProofBundleIds),
|
|
108
|
+
forwardedClosureGaps: Array.isArray(source.forwardedClosureGaps)
|
|
109
|
+
? cloneJson(source.forwardedClosureGaps)
|
|
110
|
+
: [],
|
|
104
111
|
reasonBuckets: isPlainObject(source.reasonBuckets) ? source.reasonBuckets : {},
|
|
105
112
|
executorStates: isPlainObject(source.executorStates) ? source.executorStates : {},
|
|
106
113
|
fallbackHistory: isPlainObject(source.fallbackHistory) ? source.fallbackHistory : {},
|
|
@@ -2,7 +2,6 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
|
-
DEFAULT_CODEX_SANDBOX_MODE,
|
|
6
5
|
DEFAULT_EXECUTOR_MODE,
|
|
7
6
|
loadWaveConfig,
|
|
8
7
|
normalizeCodexSandboxMode,
|
|
@@ -36,6 +35,10 @@ import {
|
|
|
36
35
|
readMaterializedCoordinationState,
|
|
37
36
|
} from "./coordination-store.mjs";
|
|
38
37
|
import { readWaveLedger } from "./ledger.mjs";
|
|
38
|
+
import {
|
|
39
|
+
submitLauncherRun,
|
|
40
|
+
waitForRunState,
|
|
41
|
+
} from "./supervisor-cli.mjs";
|
|
39
42
|
|
|
40
43
|
const AUTONOMOUS_EXECUTOR_MODES = SUPPORTED_EXECUTOR_MODES.filter((mode) => mode !== "local");
|
|
41
44
|
|
|
@@ -58,7 +61,7 @@ Options:
|
|
|
58
61
|
--orchestrator-id <id> Orchestrator ID for coordination board
|
|
59
62
|
--resident-orchestrator Launch a resident orchestrator session for each live wave
|
|
60
63
|
--executor <mode> Default executor passed to launcher: ${AUTONOMOUS_EXECUTOR_MODES.join(" | ")} (default: lane config)
|
|
61
|
-
--codex-sandbox <mode>
|
|
64
|
+
--codex-sandbox <mode> Codex sandbox mode override passed to launcher (default: lane config)
|
|
62
65
|
--dashboard Enable dashboards (default: disabled)
|
|
63
66
|
--keep-sessions Keep tmux sessions between waves
|
|
64
67
|
--keep-terminals Keep temporary terminal entries between waves
|
|
@@ -80,7 +83,7 @@ export function parseArgs(argv) {
|
|
|
80
83
|
orchestratorId: null,
|
|
81
84
|
residentOrchestrator: false,
|
|
82
85
|
executorMode: DEFAULT_EXECUTOR_MODE,
|
|
83
|
-
codexSandboxMode:
|
|
86
|
+
codexSandboxMode: null,
|
|
84
87
|
noDashboard: true,
|
|
85
88
|
keepSessions: false,
|
|
86
89
|
keepTerminals: false,
|
|
@@ -234,9 +237,8 @@ function listPendingFeedback(lane, project) {
|
|
|
234
237
|
]);
|
|
235
238
|
}
|
|
236
239
|
|
|
237
|
-
function
|
|
238
|
-
const
|
|
239
|
-
path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"),
|
|
240
|
+
export function buildSingleWaveLauncherArgs(params) {
|
|
241
|
+
const launcherArgs = [
|
|
240
242
|
"--project",
|
|
241
243
|
params.project,
|
|
242
244
|
"--lane",
|
|
@@ -259,26 +261,57 @@ function launchSingleWave(params) {
|
|
|
259
261
|
String(params.agentLaunchStaggerMs),
|
|
260
262
|
"--executor",
|
|
261
263
|
params.executorMode,
|
|
262
|
-
"--codex-sandbox",
|
|
263
|
-
params.codexSandboxMode,
|
|
264
264
|
"--orchestrator-id",
|
|
265
265
|
params.orchestratorId,
|
|
266
266
|
"--coordination-note",
|
|
267
267
|
`autonomous single-wave run wave=${params.wave} attempt=${params.attempt}`,
|
|
268
268
|
];
|
|
269
269
|
if (params.noDashboard) {
|
|
270
|
-
|
|
270
|
+
launcherArgs.push("--no-dashboard");
|
|
271
|
+
}
|
|
272
|
+
if (params.codexSandboxMode) {
|
|
273
|
+
launcherArgs.push("--codex-sandbox", params.codexSandboxMode);
|
|
271
274
|
}
|
|
272
275
|
if (params.keepSessions) {
|
|
273
|
-
|
|
276
|
+
launcherArgs.push("--keep-sessions");
|
|
274
277
|
}
|
|
275
278
|
if (params.keepTerminals) {
|
|
276
|
-
|
|
279
|
+
launcherArgs.push("--keep-terminals");
|
|
277
280
|
}
|
|
278
281
|
if (params.residentOrchestrator) {
|
|
279
|
-
|
|
282
|
+
launcherArgs.push("--resident-orchestrator");
|
|
280
283
|
}
|
|
281
|
-
return
|
|
284
|
+
return launcherArgs;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function launchSingleWave(params) {
|
|
288
|
+
const launcherArgs = buildSingleWaveLauncherArgs(params);
|
|
289
|
+
const submission = submitLauncherRun(launcherArgs);
|
|
290
|
+
console.log(
|
|
291
|
+
`[autonomous] submitted wave ${params.wave} as run_id=${submission.runId} lane=${submission.lane} project=${submission.project}`,
|
|
292
|
+
);
|
|
293
|
+
const observeTimeoutSeconds = Math.max(30, Math.min(60, params.timeoutMinutes * 60));
|
|
294
|
+
return (async () => {
|
|
295
|
+
while (true) {
|
|
296
|
+
const located = await waitForRunState({
|
|
297
|
+
project: submission.project,
|
|
298
|
+
lane: submission.lane,
|
|
299
|
+
adhocRunId: submission.adhocRunId,
|
|
300
|
+
runId: submission.runId,
|
|
301
|
+
timeoutSeconds: observeTimeoutSeconds,
|
|
302
|
+
});
|
|
303
|
+
if (located.state.status === "completed") {
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
if (located.state.status === "failed") {
|
|
307
|
+
return Number.isInteger(located.state.exitCode) ? located.state.exitCode : 1;
|
|
308
|
+
}
|
|
309
|
+
const reconcileStatus = reconcile(params.lane, params.project);
|
|
310
|
+
if (reconcileStatus !== 0) {
|
|
311
|
+
return reconcileStatus;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
})();
|
|
282
315
|
}
|
|
283
316
|
|
|
284
317
|
function requiredInboundDependenciesOpen(lanePaths, lane) {
|
|
@@ -434,7 +467,7 @@ export async function runAutonomousCli(argv) {
|
|
|
434
467
|
console.log(
|
|
435
468
|
`\n[autonomous] launching wave ${wave} (attempt ${attempt}/${options.maxAttemptsPerWave})`,
|
|
436
469
|
);
|
|
437
|
-
const status = launchSingleWave({
|
|
470
|
+
const status = await launchSingleWave({
|
|
438
471
|
...options,
|
|
439
472
|
wave,
|
|
440
473
|
attempt,
|