@chllming/wave-orchestration 0.9.0 → 0.9.1

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 (54) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +119 -18
  3. package/docs/README.md +7 -3
  4. package/docs/architecture/README.md +1498 -0
  5. package/docs/concepts/operating-modes.md +2 -2
  6. package/docs/guides/author-and-run-waves.md +14 -4
  7. package/docs/guides/planner.md +2 -2
  8. package/docs/guides/{recommendations-0.9.0.md → recommendations-0.9.1.md} +8 -7
  9. package/docs/guides/sandboxed-environments.md +158 -0
  10. package/docs/guides/terminal-surfaces.md +14 -12
  11. package/docs/plans/current-state.md +5 -3
  12. package/docs/plans/end-state-architecture.md +3 -1
  13. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  14. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  15. package/docs/plans/migration.md +46 -19
  16. package/docs/plans/sandbox-end-state-architecture.md +153 -0
  17. package/docs/reference/cli-reference.md +71 -7
  18. package/docs/reference/coordination-and-closure.md +1 -1
  19. package/docs/reference/github-packages-setup.md +1 -1
  20. package/docs/reference/migration-0.2-to-0.5.md +9 -7
  21. package/docs/reference/npmjs-token-publishing.md +53 -0
  22. package/docs/reference/npmjs-trusted-publishing.md +4 -50
  23. package/docs/reference/package-publishing-flow.md +272 -0
  24. package/docs/reference/runtime-config/README.md +2 -2
  25. package/docs/reference/sample-waves.md +5 -5
  26. package/docs/reference/skills.md +1 -1
  27. package/docs/roadmap.md +43 -201
  28. package/package.json +1 -1
  29. package/releases/manifest.json +19 -0
  30. package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
  31. package/scripts/wave-orchestrator/agent-state.mjs +0 -1
  32. package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
  33. package/scripts/wave-orchestrator/autonomous.mjs +47 -14
  34. package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
  35. package/scripts/wave-orchestrator/control-cli.mjs +42 -5
  36. package/scripts/wave-orchestrator/dashboard-renderer.mjs +115 -43
  37. package/scripts/wave-orchestrator/derived-state-engine.mjs +6 -3
  38. package/scripts/wave-orchestrator/gate-engine.mjs +106 -38
  39. package/scripts/wave-orchestrator/install.mjs +13 -0
  40. package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
  41. package/scripts/wave-orchestrator/launcher-runtime.mjs +179 -68
  42. package/scripts/wave-orchestrator/launcher.mjs +201 -53
  43. package/scripts/wave-orchestrator/ledger.mjs +7 -2
  44. package/scripts/wave-orchestrator/projection-writer.mjs +13 -1
  45. package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
  46. package/scripts/wave-orchestrator/retry-control.mjs +3 -3
  47. package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
  48. package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
  49. package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
  50. package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
  51. package/scripts/wave-orchestrator/terminals.mjs +12 -32
  52. package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
  53. package/scripts/wave-orchestrator/wave-files.mjs +38 -5
  54. package/scripts/wave.mjs +13 -0
@@ -36,7 +36,7 @@ import {
36
36
  isDocsOnlyDesignAgent,
37
37
  isDesignAgent,
38
38
  isImplementationOwningDesignAgent,
39
- isSecurityReviewAgent,
39
+ isSecurityReviewAgentForLane,
40
40
  resolveWaveRoleBindings,
41
41
  } from "./role-helpers.mjs";
42
42
  import {
@@ -283,7 +283,10 @@ export function reconcileFailuresAgainstSharedComponentState(wave, agentRuns, fa
283
283
  const summariesByAgentId = Object.fromEntries(
284
284
  (agentRuns || []).map((runInfo) => [
285
285
  runInfo.agent.agentId,
286
- readRunExecutionSummary(runInfo, wave, { mode: "live" }),
286
+ readRunExecutionSummary(runInfo, wave, {
287
+ mode: "live",
288
+ securityRolePromptPath: lanePaths?.securityRolePromptPath,
289
+ }),
287
290
  ]),
288
291
  );
289
292
  const failureAgentIds = new Set(failures.map((failure) => failure.agentId).filter(Boolean));
@@ -407,7 +410,7 @@ function isClosureAgentId(agent, lanePaths, waveDefinition = null) {
407
410
  return (
408
411
  resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents).closureAgentIds.includes(
409
412
  agent?.agentId,
410
- ) || isSecurityReviewAgent(agent)
413
+ ) || isSecurityReviewAgentForLane(agent, lanePaths)
411
414
  );
412
415
  }
413
416
 
@@ -658,7 +661,7 @@ function resolveRunsForResumePhase(agentRuns, lanePaths, resumePhase, waveDefini
658
661
  return runsFromAgentIds(agentRuns, [roleBindings.integrationAgentId]);
659
662
  }
660
663
  if (resumePhase === "security-review") {
661
- return (agentRuns || []).filter((run) => isSecurityReviewAgent(run.agent));
664
+ return (agentRuns || []).filter((run) => isSecurityReviewAgentForLane(run.agent, lanePaths));
662
665
  }
663
666
  if (resumePhase === "docs-closure") {
664
667
  return runsFromAgentIds(agentRuns, [roleBindings.documentationAgentId]);
@@ -820,7 +823,7 @@ function resolveRelaunchRunsLegacy(agentRuns, failures, derivedState, lanePaths,
820
823
  }
821
824
  if (derivedState?.ledger?.phase === "security-review") {
822
825
  return {
823
- runs: agentRuns.filter((run) => isSecurityReviewAgent(run.agent)),
826
+ runs: agentRuns.filter((run) => isSecurityReviewAgentForLane(run.agent, lanePaths)),
824
827
  barrier: null,
825
828
  };
826
829
  }
@@ -1196,6 +1199,66 @@ function collectResumeExecutorChanges(waveState) {
1196
1199
  }));
1197
1200
  }
1198
1201
 
1202
+ const CLOSURE_STAGE_ORDER = [
1203
+ "cont-eval",
1204
+ "security-review",
1205
+ "integrating",
1206
+ "docs-closure",
1207
+ "cont-qa-closure",
1208
+ ];
1209
+
1210
+ function closureStageForAgentId(agentId, roleBindings) {
1211
+ if (!agentId) {
1212
+ return null;
1213
+ }
1214
+ if (agentId === roleBindings?.contEvalAgentId) {
1215
+ return "cont-eval";
1216
+ }
1217
+ if ((roleBindings?.securityReviewerAgentIds || []).includes(agentId)) {
1218
+ return "security-review";
1219
+ }
1220
+ if (agentId === roleBindings?.integrationAgentId) {
1221
+ return "integrating";
1222
+ }
1223
+ if (agentId === roleBindings?.documentationAgentId) {
1224
+ return "docs-closure";
1225
+ }
1226
+ if (agentId === roleBindings?.contQaAgentId) {
1227
+ return "cont-qa-closure";
1228
+ }
1229
+ return null;
1230
+ }
1231
+
1232
+ function collectForwardedClosureGaps(waveState, waveDefinition, lanePaths) {
1233
+ const roleBindings = resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents);
1234
+ return normalizeRetryTargets(waveState?.retryTargetSet)
1235
+ .filter((target) => target?.statusCode === "wave-proof-gap" || target?.reason === "wave-proof-gap")
1236
+ .map((target) => {
1237
+ const stageKey = closureStageForAgentId(target.agentId, roleBindings);
1238
+ if (!stageKey) {
1239
+ return null;
1240
+ }
1241
+ return {
1242
+ stageKey,
1243
+ agentId: target.agentId,
1244
+ attempt: waveState?.attempt ?? null,
1245
+ detail: target.detail || target.reason || target.statusCode || null,
1246
+ targets: normalizeRetryTargets(waveState?.retryTargetSet)
1247
+ .filter((entry) => closureStageForAgentId(entry.agentId, roleBindings))
1248
+ .map((entry) => entry.agentId)
1249
+ .filter((agentId) => {
1250
+ const agentStage = closureStageForAgentId(agentId, roleBindings);
1251
+ return CLOSURE_STAGE_ORDER.indexOf(agentStage) >= CLOSURE_STAGE_ORDER.indexOf(stageKey);
1252
+ }),
1253
+ resolved: false,
1254
+ };
1255
+ })
1256
+ .filter(Boolean)
1257
+ .sort(
1258
+ (left, right) => CLOSURE_STAGE_ORDER.indexOf(left.stageKey) - CLOSURE_STAGE_ORDER.indexOf(right.stageKey),
1259
+ );
1260
+ }
1261
+
1199
1262
  /**
1200
1263
  * Deterministic resume planner operating on reducer output (WaveState).
1201
1264
  * Pure function — no file I/O.
@@ -1207,8 +1270,29 @@ export function buildResumePlan(waveState, options = {}) {
1207
1270
  const canResume = reason !== "all-gates-pass";
1208
1271
  const pendingAgentIds = waveState.closureEligibility?.pendingAgentIds || [];
1209
1272
  const provenAgentIds = waveState.closureEligibility?.ownedSliceProvenAgentIds || [];
1273
+ const forwardedClosureGaps = collectForwardedClosureGaps(waveState, waveDefinition, lanePaths);
1210
1274
  const invalidatedAgentIds = [...pendingAgentIds].sort();
1211
1275
  const reusableAgentIds = [...provenAgentIds].sort();
1276
+ if (forwardedClosureGaps.length > 0) {
1277
+ const roleBindings = resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents);
1278
+ const earliestGap = forwardedClosureGaps[0];
1279
+ const invalidatedClosureAgentIds = roleBindings.closureAgentIds.filter((agentId) => {
1280
+ const stageKey = closureStageForAgentId(agentId, roleBindings);
1281
+ return CLOSURE_STAGE_ORDER.indexOf(stageKey) >= CLOSURE_STAGE_ORDER.indexOf(earliestGap.stageKey);
1282
+ });
1283
+ for (const agentId of invalidatedClosureAgentIds) {
1284
+ if (!invalidatedAgentIds.includes(agentId)) {
1285
+ invalidatedAgentIds.push(agentId);
1286
+ }
1287
+ }
1288
+ invalidatedAgentIds.sort();
1289
+ for (const agentId of invalidatedClosureAgentIds) {
1290
+ const index = reusableAgentIds.indexOf(agentId);
1291
+ if (index >= 0) {
1292
+ reusableAgentIds.splice(index, 1);
1293
+ }
1294
+ }
1295
+ }
1212
1296
  const proofBundles =
1213
1297
  waveState.closureEligibility?.proofBundles ||
1214
1298
  waveState.proofAvailability?.activeProofBundles ||
@@ -1219,7 +1303,9 @@ export function buildResumePlan(waveState, options = {}) {
1219
1303
  .sort();
1220
1304
  const gateSnapshot = waveState.gateSnapshot || {};
1221
1305
  let resumeFromPhase = "completed";
1222
- if (canResume && gateSnapshot.overall && !gateSnapshot.overall.ok) {
1306
+ if (forwardedClosureGaps.length > 0) {
1307
+ resumeFromPhase = forwardedClosureGaps[0].stageKey;
1308
+ } else if (canResume && gateSnapshot.overall && !gateSnapshot.overall.ok) {
1223
1309
  resumeFromPhase = phaseFromGate(gateSnapshot.overall.gate);
1224
1310
  } else if (canResume) {
1225
1311
  resumeFromPhase = "implementation";
@@ -1239,6 +1325,7 @@ export function buildResumePlan(waveState, options = {}) {
1239
1325
  humanInputBlockers: collectResumeHumanInputBlockers(waveState),
1240
1326
  gateBlockers: collectResumeGateBlockers(gateSnapshot),
1241
1327
  closureEligibility: waveState.closureEligibility || null,
1328
+ forwardedClosureGaps,
1242
1329
  deterministic: true,
1243
1330
  createdAt: toIsoTimestamp(),
1244
1331
  };
@@ -105,6 +105,12 @@ export function isSecurityReviewAgent(
105
105
  return capabilities.includes("security-review");
106
106
  }
107
107
 
108
+ export function isSecurityReviewAgentForLane(agent, lanePaths = null) {
109
+ return isSecurityReviewAgent(agent, {
110
+ securityRolePromptPath: lanePaths?.securityRolePromptPath,
111
+ });
112
+ }
113
+
108
114
  export function isDesignAgent(
109
115
  agent,
110
116
  { designRolePromptPath = DEFAULT_DESIGN_ROLE_PROMPT_PATH } = {},
@@ -202,6 +208,30 @@ export function resolveWaveRoleBindings(wave = {}, lanePaths = {}, agents = wave
202
208
  };
203
209
  }
204
210
 
211
+ export function resolveAgentClosureRoleKeys(agent, roleBindings = {}, lanePaths = {}) {
212
+ const roles = [];
213
+ if (agent?.agentId === roleBindings?.contEvalAgentId) {
214
+ roles.push("cont-eval");
215
+ }
216
+ if (
217
+ isSecurityReviewAgent(agent, {
218
+ securityRolePromptPath: lanePaths?.securityRolePromptPath,
219
+ })
220
+ ) {
221
+ roles.push("security-review");
222
+ }
223
+ if (agent?.agentId === roleBindings?.integrationAgentId) {
224
+ roles.push("integration");
225
+ }
226
+ if (agent?.agentId === roleBindings?.documentationAgentId) {
227
+ roles.push("documentation");
228
+ }
229
+ if (agent?.agentId === roleBindings?.contQaAgentId) {
230
+ roles.push("cont-qa");
231
+ }
232
+ return roles;
233
+ }
234
+
205
235
  export function isClosureRoleAgentId(agentId, roleBindings) {
206
236
  return (roleBindings?.closureAgentIds || []).includes(agentId);
207
237
  }
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import {
@@ -22,7 +21,6 @@ import {
22
21
  ensureDirectory,
23
22
  shellQuote,
24
23
  PACKAGE_ROOT,
25
- TMUX_COMMAND_TIMEOUT_MS,
26
24
  toIsoTimestamp,
27
25
  writeJsonAtomic,
28
26
  } from "./shared.mjs";
@@ -32,15 +30,25 @@ import {
32
30
  terminalSurfaceUsesTerminalRegistry,
33
31
  pruneOrphanLaneTemporaryTerminalEntries,
34
32
  } from "./terminals.mjs";
33
+ import {
34
+ createSession as createTmuxSession,
35
+ listSessions as listTmuxSessions,
36
+ runTmuxCommand,
37
+ } from "./tmux-adapter.mjs";
35
38
  import {
36
39
  recordGlobalDashboardEvent,
37
40
  } from "./dashboard-state.mjs";
38
41
  import { buildHumanFeedbackWorkflowUpdate } from "./human-input-workflow.mjs";
39
42
  import {
40
- collectUnexpectedSessionFailures as collectUnexpectedSessionFailuresImpl,
43
+ collectUnexpectedSessionWarnings as collectUnexpectedSessionWarningsImpl,
41
44
  launchAgentSession as launchAgentSessionImpl,
42
45
  waitForWaveCompletion as waitForWaveCompletionImpl,
43
46
  } from "./launcher-runtime.mjs";
47
+ import { terminateAgentProcessRuntime } from "./agent-process-runner.mjs";
48
+ import {
49
+ buildSupervisorPaths,
50
+ supervisorAgentRuntimePathForRun,
51
+ } from "./supervisor-cli.mjs";
44
52
  import {
45
53
  agentUsesSignalHygiene,
46
54
  buildSignalStatusLine,
@@ -65,6 +73,13 @@ function relativeArtifactPath(filePath) {
65
73
  return filePath ? path.relative(REPO_ROOT, filePath) : null;
66
74
  }
67
75
 
76
+ function readRuntimeRecord(run) {
77
+ if (!run?.runtimePath || !fs.existsSync(run.runtimePath)) {
78
+ return null;
79
+ }
80
+ return readJsonOrNull(run.runtimePath);
81
+ }
82
+
68
83
  export function recordWaveRunState(lanePaths, waveNumber, state, data = {}) {
69
84
  return appendWaveControlEvent(lanePaths, waveNumber, {
70
85
  entityType: "wave_run",
@@ -235,8 +250,8 @@ function isLaneSessionName(lanePaths, sessionName) {
235
250
  );
236
251
  }
237
252
 
238
- function listLaneTmuxSessionNames(lanePaths) {
239
- return listTmuxSessionNames(lanePaths).filter((sessionName) =>
253
+ async function listLaneTmuxSessionNames(lanePaths) {
254
+ return (await listTmuxSessionNames(lanePaths)).filter((sessionName) =>
240
255
  isLaneSessionName(lanePaths, sessionName),
241
256
  );
242
257
  }
@@ -362,6 +377,7 @@ export function buildResidentOrchestratorRun({
362
377
  promptPath: path.join(lanePaths.promptsDir, `${baseName}.prompt.md`),
363
378
  logPath: path.join(lanePaths.logsDir, `${baseName}.log`),
364
379
  statusPath: path.join(lanePaths.statusDir, `${baseName}.status`),
380
+ runtimePath: path.join(lanePaths.statusDir, `${baseName}.runtime.json`),
365
381
  promptOverride: buildResidentOrchestratorPrompt({
366
382
  lane: lanePaths.lane,
367
383
  wave: wave.wave,
@@ -415,20 +431,28 @@ export function monitorResidentOrchestratorSession({
415
431
  });
416
432
  return true;
417
433
  }
418
- const activeSessions = new Set(listLaneTmuxSessionNames(lanePaths));
419
- if (!activeSessions.has(run.sessionName)) {
434
+ const runtimeRecord = readRuntimeRecord(run);
435
+ if (
436
+ runtimeRecord &&
437
+ ["completed", "failed", "terminated"].includes(
438
+ String(runtimeRecord.terminalDisposition || ""),
439
+ )
440
+ ) {
420
441
  sessionState.closed = true;
442
+ const exitCode = Number.parseInt(String(runtimeRecord.exitCode ?? ""), 10);
421
443
  recordCombinedEvent({
422
- level: "warn",
444
+ level: Number.isFinite(exitCode) && exitCode === 0 ? "info" : "warn",
423
445
  agentId: run.agent.agentId,
424
446
  message:
425
- "Resident orchestrator session disappeared before writing a status file; launcher continues as the control plane.",
447
+ Number.isFinite(exitCode) && exitCode === 0
448
+ ? "Resident orchestrator ended via runtime record before writing a status file; launcher continues as the control plane."
449
+ : "Resident orchestrator ended via runtime record before writing a status file; launcher continues as the control plane.",
426
450
  });
427
451
  appendCoordination({
428
- event: "resident_orchestrator_missing",
452
+ event: "resident_orchestrator_runtime_terminal",
429
453
  waves: [waveNumber],
430
- status: "warn",
431
- details: `tmux session ${run.sessionName} disappeared before ${path.relative(REPO_ROOT, run.statusPath)} was written.`,
454
+ status: Number.isFinite(exitCode) && exitCode === 0 ? "resolved" : "warn",
455
+ details: `runtime record reached ${runtimeRecord.terminalDisposition} before ${path.relative(REPO_ROOT, run.statusPath)} was written.`,
432
456
  actionRequested: "None",
433
457
  });
434
458
  return true;
@@ -505,7 +529,7 @@ export function pruneDryRunExecutorPreviewDirs(lanePaths, waves) {
505
529
  return removedPaths.toSorted();
506
530
  }
507
531
 
508
- export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
532
+ export async function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
509
533
  const outcome = {
510
534
  removedLock: false,
511
535
  removedSessions: [],
@@ -527,8 +551,8 @@ export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
527
551
  outcome.removedLock = true;
528
552
  }
529
553
 
530
- outcome.removedSessions = cleanupLaneTmuxSessions(lanePaths);
531
- const activeSessionNames = new Set(listLaneTmuxSessionNames(lanePaths));
554
+ outcome.removedSessions = await cleanupLaneTmuxSessions(lanePaths);
555
+ const activeSessionNames = new Set(await listLaneTmuxSessionNames(lanePaths));
532
556
  if (terminalSurfaceUsesTerminalRegistry(options.terminalSurface || "vscode")) {
533
557
  const terminalCleanup = pruneOrphanLaneTemporaryTerminalEntries(
534
558
  lanePaths.terminalsPath,
@@ -562,87 +586,35 @@ export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
562
586
  }
563
587
 
564
588
  export function runTmux(lanePaths, args, description) {
565
- const result = spawnSync("tmux", ["-L", lanePaths.tmuxSocketName, ...args], {
566
- cwd: REPO_ROOT,
567
- encoding: "utf8",
568
- env: { ...process.env, TMUX: "" },
569
- timeout: TMUX_COMMAND_TIMEOUT_MS,
589
+ return runTmuxCommand(lanePaths.tmuxSocketName, args, {
590
+ description,
591
+ mutate: ["new-session", "kill-session"].includes(String(args?.[0] || "")),
570
592
  });
571
- if (result.error) {
572
- if (result.error.code === "ETIMEDOUT") {
573
- throw new Error(
574
- `${description} failed: tmux command timed out after ${TMUX_COMMAND_TIMEOUT_MS}ms`,
575
- );
576
- }
577
- throw new Error(`${description} failed: ${result.error.message}`);
578
- }
579
- if (result.status !== 0) {
580
- throw new Error(
581
- `${description} failed: ${(result.stderr || "").trim() || "tmux command failed"}`,
582
- );
583
- }
584
593
  }
585
594
 
586
595
  function listTmuxSessionNames(lanePaths) {
587
- const result = spawnSync(
588
- "tmux",
589
- ["-L", lanePaths.tmuxSocketName, "list-sessions", "-F", "#{session_name}"],
590
- {
591
- cwd: REPO_ROOT,
592
- encoding: "utf8",
593
- env: { ...process.env, TMUX: "" },
594
- timeout: TMUX_COMMAND_TIMEOUT_MS,
595
- },
596
- );
597
- if (result.error) {
598
- if (result.error.code === "ENOENT") {
599
- return [];
600
- }
601
- if (result.error.code === "ETIMEDOUT") {
602
- throw new Error(`list tmux sessions failed: timed out after ${TMUX_COMMAND_TIMEOUT_MS}ms`);
603
- }
604
- throw new Error(`list tmux sessions failed: ${result.error.message}`);
605
- }
606
- if (result.status !== 0) {
607
- const combined = `${String(result.stderr || "").toLowerCase()}\n${String(result.stdout || "").toLowerCase()}`;
608
- if (
609
- combined.includes("no server running") ||
610
- combined.includes("failed to connect") ||
611
- combined.includes("error connecting")
612
- ) {
613
- return [];
614
- }
615
- throw new Error(
616
- `list tmux sessions failed: ${(result.stderr || "").trim() || "unknown error"}`,
617
- );
618
- }
619
- return String(result.stdout || "")
620
- .split(/\r?\n/)
621
- .map((line) => line.trim())
622
- .filter(Boolean);
596
+ return listTmuxSessions(lanePaths.tmuxSocketName);
623
597
  }
624
598
 
625
- export function cleanupLaneTmuxSessions(lanePaths, { excludeSessionNames = new Set() } = {}) {
626
- const sessionNames = listTmuxSessionNames(lanePaths);
599
+ export async function cleanupLaneTmuxSessions(lanePaths, { excludeSessionNames = new Set() } = {}) {
600
+ const sessionNames = await listTmuxSessionNames(lanePaths);
627
601
  const killed = [];
628
602
  for (const sessionName of sessionNames) {
629
603
  if (excludeSessionNames.has(sessionName) || !isLaneSessionName(lanePaths, sessionName)) {
630
604
  continue;
631
605
  }
632
- killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
606
+ await killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
633
607
  killed.push(sessionName);
634
608
  }
635
609
  return killed;
636
610
  }
637
611
 
638
- export function collectUnexpectedSessionFailures(lanePaths, agentRuns, pendingAgentIds) {
639
- return collectUnexpectedSessionFailuresImpl(lanePaths, agentRuns, pendingAgentIds, {
640
- listLaneTmuxSessionNamesFn: listLaneTmuxSessionNames,
641
- });
612
+ export function collectUnexpectedSessionWarnings(lanePaths, agentRuns, pendingAgentIds) {
613
+ return collectUnexpectedSessionWarningsImpl(lanePaths, agentRuns, pendingAgentIds, {});
642
614
  }
643
615
 
644
- export function launchWaveDashboardSession(lanePaths, { sessionName, dashboardPath, messageBoardPath }) {
645
- killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
616
+ export async function launchWaveDashboardSession(lanePaths, { sessionName, dashboardPath, messageBoardPath }) {
617
+ await killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
646
618
  const messageBoardArg = messageBoardPath
647
619
  ? ` --message-board ${shellQuote(messageBoardPath)}`
648
620
  : "";
@@ -653,15 +625,31 @@ export function launchWaveDashboardSession(lanePaths, { sessionName, dashboardPa
653
625
  )}${messageBoardArg} --lane ${shellQuote(lanePaths.lane)} --watch`,
654
626
  "exec bash -l",
655
627
  ].join("; ");
656
- runTmux(
657
- lanePaths,
658
- ["new-session", "-d", "-s", sessionName, `bash -lc ${shellQuote(command)}`],
659
- `launch dashboard session ${sessionName}`,
628
+ await createTmuxSession(
629
+ lanePaths.tmuxSocketName,
630
+ sessionName,
631
+ `bash -lc ${shellQuote(command)}`,
632
+ { description: `launch dashboard session ${sessionName}` },
660
633
  );
661
634
  }
662
635
 
663
636
  export async function launchAgentSession(lanePaths, params) {
664
- const result = await launchAgentSessionImpl(lanePaths, params, { runTmuxFn: runTmux });
637
+ const supervisorRunId = String(process.env.WAVE_SUPERVISOR_RUN_ID || "").trim();
638
+ const runtimePath = supervisorRunId
639
+ ? supervisorAgentRuntimePathForRun(
640
+ buildSupervisorPaths(lanePaths),
641
+ supervisorRunId,
642
+ params?.agent?.agentId || "unknown-agent",
643
+ )
644
+ : params?.runtimePath || null;
645
+ const result = await launchAgentSessionImpl(
646
+ lanePaths,
647
+ {
648
+ ...params,
649
+ runtimePath,
650
+ },
651
+ { runTmuxFn: runTmux },
652
+ );
665
653
  const controlPlane = params?.controlPlane || null;
666
654
  if (!params?.dryRun && controlPlane?.waveNumber !== undefined && controlPlane?.attempt) {
667
655
  recordAgentRunStarted(lanePaths, {
@@ -669,11 +657,32 @@ export async function launchAgentSession(lanePaths, params) {
669
657
  attempt: controlPlane.attempt,
670
658
  runInfo: {
671
659
  ...params,
660
+ runtimePath,
672
661
  lastExecutorId: result?.executorId || params?.agent?.executorResolved?.id || null,
673
662
  },
674
663
  });
675
664
  }
676
- return result;
665
+ return {
666
+ ...result,
667
+ runtimePath,
668
+ };
669
+ }
670
+
671
+ export async function cleanupLaunchedRun(
672
+ lanePaths,
673
+ run,
674
+ {
675
+ terminateRuntimeFn = terminateAgentProcessRuntime,
676
+ killSessionFn = killTmuxSessionIfExists,
677
+ } = {},
678
+ ) {
679
+ const runtimeRecord = readRuntimeRecord(run);
680
+ if (runtimeRecord && typeof runtimeRecord === "object") {
681
+ await terminateRuntimeFn(runtimeRecord);
682
+ }
683
+ if (run?.sessionName) {
684
+ await killSessionFn(lanePaths.tmuxSocketName, run.sessionName);
685
+ }
677
686
  }
678
687
 
679
688
  export async function waitForWaveCompletion(
@@ -684,7 +693,7 @@ export async function waitForWaveCompletion(
684
693
  options = {},
685
694
  ) {
686
695
  const result = await waitForWaveCompletionImpl(lanePaths, agentRuns, timeoutMinutes, onProgress, {
687
- collectUnexpectedSessionFailuresFn: collectUnexpectedSessionFailures,
696
+ collectUnexpectedSessionWarningsFn: collectUnexpectedSessionWarnings,
688
697
  });
689
698
  const controlPlane = options?.controlPlane || null;
690
699
  if (controlPlane?.waveNumber !== undefined && controlPlane?.attempt) {