@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +133 -20
  4. package/docs/README.md +12 -4
  5. package/docs/agents/wave-security-role.md +1 -0
  6. package/docs/architecture/README.md +1498 -0
  7. package/docs/concepts/operating-modes.md +2 -2
  8. package/docs/guides/author-and-run-waves.md +14 -4
  9. package/docs/guides/planner.md +2 -2
  10. package/docs/guides/{recommendations-0.9.0.md → recommendations-0.9.2.md} +8 -7
  11. package/docs/guides/sandboxed-environments.md +158 -0
  12. package/docs/guides/terminal-surfaces.md +14 -12
  13. package/docs/plans/current-state.md +11 -3
  14. package/docs/plans/end-state-architecture.md +3 -1
  15. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  16. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  17. package/docs/plans/migration.md +70 -19
  18. package/docs/plans/sandbox-end-state-architecture.md +153 -0
  19. package/docs/reference/cli-reference.md +71 -7
  20. package/docs/reference/coordination-and-closure.md +18 -1
  21. package/docs/reference/corridor.md +225 -0
  22. package/docs/reference/github-packages-setup.md +1 -1
  23. package/docs/reference/migration-0.2-to-0.5.md +9 -7
  24. package/docs/reference/npmjs-token-publishing.md +53 -0
  25. package/docs/reference/npmjs-trusted-publishing.md +4 -50
  26. package/docs/reference/package-publishing-flow.md +272 -0
  27. package/docs/reference/runtime-config/README.md +61 -3
  28. package/docs/reference/sample-waves.md +5 -5
  29. package/docs/reference/skills.md +1 -1
  30. package/docs/reference/wave-control.md +358 -27
  31. package/docs/roadmap.md +39 -204
  32. package/package.json +1 -1
  33. package/releases/manifest.json +38 -0
  34. package/scripts/wave-cli-bootstrap.mjs +52 -1
  35. package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
  36. package/scripts/wave-orchestrator/agent-state.mjs +0 -1
  37. package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
  38. package/scripts/wave-orchestrator/autonomous.mjs +47 -14
  39. package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
  40. package/scripts/wave-orchestrator/config.mjs +199 -3
  41. package/scripts/wave-orchestrator/context7.mjs +231 -29
  42. package/scripts/wave-orchestrator/control-cli.mjs +42 -5
  43. package/scripts/wave-orchestrator/coordination.mjs +14 -0
  44. package/scripts/wave-orchestrator/corridor.mjs +363 -0
  45. package/scripts/wave-orchestrator/dashboard-renderer.mjs +115 -43
  46. package/scripts/wave-orchestrator/derived-state-engine.mjs +44 -4
  47. package/scripts/wave-orchestrator/gate-engine.mjs +126 -38
  48. package/scripts/wave-orchestrator/install.mjs +46 -0
  49. package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
  50. package/scripts/wave-orchestrator/launcher-runtime.mjs +290 -75
  51. package/scripts/wave-orchestrator/launcher.mjs +201 -53
  52. package/scripts/wave-orchestrator/ledger.mjs +7 -2
  53. package/scripts/wave-orchestrator/planner.mjs +1 -0
  54. package/scripts/wave-orchestrator/projection-writer.mjs +36 -1
  55. package/scripts/wave-orchestrator/provider-runtime.mjs +104 -0
  56. package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
  57. package/scripts/wave-orchestrator/retry-control.mjs +3 -3
  58. package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
  59. package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
  60. package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
  61. package/scripts/wave-orchestrator/shared.mjs +1 -0
  62. package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
  63. package/scripts/wave-orchestrator/terminals.mjs +12 -32
  64. package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
  65. package/scripts/wave-orchestrator/traces.mjs +25 -0
  66. package/scripts/wave-orchestrator/wave-control-client.mjs +14 -1
  67. package/scripts/wave-orchestrator/wave-files.mjs +38 -5
  68. 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> Default Codex sandbox mode passed to launcher (default: ${DEFAULT_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: DEFAULT_CODEX_SANDBOX_MODE,
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 launchSingleWave(params) {
238
- const args = [
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
- args.push("--no-dashboard");
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
- args.push("--keep-sessions");
276
+ launcherArgs.push("--keep-sessions");
274
277
  }
275
278
  if (params.keepTerminals) {
276
- args.push("--keep-terminals");
279
+ launcherArgs.push("--keep-terminals");
277
280
  }
278
281
  if (params.residentOrchestrator) {
279
- args.push("--resident-orchestrator");
282
+ launcherArgs.push("--resident-orchestrator");
280
283
  }
281
- return runCommand(args, { [WAVE_SUPPRESS_UPDATE_NOTICE_ENV]: "1" });
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,