@chllming/wave-orchestration 0.8.9 → 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 (75) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +135 -18
  3. package/docs/README.md +9 -3
  4. package/docs/architecture/README.md +1498 -0
  5. package/docs/concepts/context7-vs-skills.md +1 -1
  6. package/docs/concepts/operating-modes.md +3 -3
  7. package/docs/concepts/what-is-a-wave.md +1 -1
  8. package/docs/guides/author-and-run-waves.md +27 -4
  9. package/docs/guides/monorepo-projects.md +226 -0
  10. package/docs/guides/planner.md +10 -3
  11. package/docs/guides/{recommendations-0.8.9.md → recommendations-0.9.1.md} +8 -7
  12. package/docs/guides/sandboxed-environments.md +158 -0
  13. package/docs/guides/terminal-surfaces.md +14 -12
  14. package/docs/plans/current-state.md +11 -7
  15. package/docs/plans/end-state-architecture.md +3 -1
  16. package/docs/plans/examples/wave-example-design-handoff.md +3 -1
  17. package/docs/plans/examples/wave-example-live-proof.md +6 -1
  18. package/docs/plans/examples/wave-example-rollout-fidelity.md +2 -0
  19. package/docs/plans/migration.md +48 -18
  20. package/docs/plans/sandbox-end-state-architecture.md +153 -0
  21. package/docs/plans/wave-orchestrator.md +4 -4
  22. package/docs/reference/cli-reference.md +125 -57
  23. package/docs/reference/coordination-and-closure.md +1 -1
  24. package/docs/reference/github-packages-setup.md +1 -1
  25. package/docs/reference/migration-0.2-to-0.5.md +9 -7
  26. package/docs/reference/npmjs-token-publishing.md +53 -0
  27. package/docs/reference/npmjs-trusted-publishing.md +4 -50
  28. package/docs/reference/package-publishing-flow.md +272 -0
  29. package/docs/reference/runtime-config/README.md +140 -12
  30. package/docs/reference/sample-waves.md +100 -5
  31. package/docs/reference/skills.md +1 -1
  32. package/docs/reference/wave-control.md +23 -5
  33. package/docs/roadmap.md +43 -201
  34. package/package.json +1 -1
  35. package/releases/manifest.json +38 -0
  36. package/scripts/wave-orchestrator/adhoc.mjs +49 -17
  37. package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
  38. package/scripts/wave-orchestrator/agent-state.mjs +0 -1
  39. package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
  40. package/scripts/wave-orchestrator/autonomous.mjs +96 -29
  41. package/scripts/wave-orchestrator/benchmark-external.mjs +23 -7
  42. package/scripts/wave-orchestrator/benchmark.mjs +33 -10
  43. package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
  44. package/scripts/wave-orchestrator/config.mjs +239 -24
  45. package/scripts/wave-orchestrator/control-cli.mjs +71 -28
  46. package/scripts/wave-orchestrator/coord-cli.mjs +22 -14
  47. package/scripts/wave-orchestrator/coordination-store.mjs +8 -0
  48. package/scripts/wave-orchestrator/dashboard-renderer.mjs +123 -44
  49. package/scripts/wave-orchestrator/dep-cli.mjs +47 -21
  50. package/scripts/wave-orchestrator/derived-state-engine.mjs +6 -3
  51. package/scripts/wave-orchestrator/feedback.mjs +28 -11
  52. package/scripts/wave-orchestrator/gate-engine.mjs +106 -38
  53. package/scripts/wave-orchestrator/human-input-resolution.mjs +5 -1
  54. package/scripts/wave-orchestrator/install.mjs +13 -0
  55. package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
  56. package/scripts/wave-orchestrator/launcher-runtime.mjs +179 -68
  57. package/scripts/wave-orchestrator/launcher.mjs +222 -53
  58. package/scripts/wave-orchestrator/ledger.mjs +7 -2
  59. package/scripts/wave-orchestrator/planner.mjs +48 -27
  60. package/scripts/wave-orchestrator/project-profile.mjs +31 -8
  61. package/scripts/wave-orchestrator/projection-writer.mjs +13 -1
  62. package/scripts/wave-orchestrator/proof-cli.mjs +18 -12
  63. package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
  64. package/scripts/wave-orchestrator/retry-cli.mjs +19 -13
  65. package/scripts/wave-orchestrator/retry-control.mjs +3 -3
  66. package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
  67. package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
  68. package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
  69. package/scripts/wave-orchestrator/shared.mjs +77 -14
  70. package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
  71. package/scripts/wave-orchestrator/terminals.mjs +12 -32
  72. package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
  73. package/scripts/wave-orchestrator/wave-control-client.mjs +84 -16
  74. package/scripts/wave-orchestrator/wave-files.mjs +43 -6
  75. 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) {
@@ -2,9 +2,11 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import {
5
+ DEFAULT_PROJECT_ID,
5
6
  DEFAULT_WAVE_LANE as CONFIG_DEFAULT_WAVE_LANE,
6
7
  loadWaveConfig,
7
8
  resolveLaneProfile,
9
+ sanitizeProjectId,
8
10
  } from "./config.mjs";
9
11
  import { PACKAGE_ROOT, WORKSPACE_ROOT } from "./roots.mjs";
10
12
 
@@ -123,13 +125,25 @@ function buildTelemetryProjectId(config) {
123
125
  );
124
126
  }
125
127
 
128
+ function projectStatePrefix(projectId, laneProfile) {
129
+ if (laneProfile?.explicitProject) {
130
+ return path.join("projects", projectId);
131
+ }
132
+ return "";
133
+ }
134
+
126
135
  function readRuntimeVersion() {
127
136
  return String(readJsonOrNull(path.join(PACKAGE_ROOT, "package.json"))?.version || "").trim() || null;
128
137
  }
129
138
 
130
139
  export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
131
140
  const config = options.config || loadWaveConfig();
132
- const baseLaneProfile = resolveLaneProfile(config, laneInput || config.defaultLane);
141
+ const project = sanitizeProjectId(options.project || config.defaultProject || DEFAULT_PROJECT_ID);
142
+ const baseLaneProfile = resolveLaneProfile(
143
+ config,
144
+ laneInput || config.defaultLane,
145
+ project,
146
+ );
133
147
  const adhocRunId = options.adhocRunId ? sanitizeAdhocRunId(options.adhocRunId) : null;
134
148
  const laneProfile = adhocRunId
135
149
  ? {
@@ -142,6 +156,8 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
142
156
  }
143
157
  : baseLaneProfile;
144
158
  const lane = laneProfile.lane;
159
+ const projectId = laneProfile.projectId || project;
160
+ const projectToken = projectId.replace(/-/g, "_");
145
161
  const laneTmux = lane.replace(/-/g, "_");
146
162
  const runKind = adhocRunId ? "adhoc" : "roadmap";
147
163
  const runVariant = String(options.runVariant || "")
@@ -155,19 +171,33 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
155
171
  const plansDir = path.join(REPO_ROOT, laneProfile.plansDir);
156
172
  const preferredWavesDir = path.join(REPO_ROOT, laneProfile.wavesDir);
157
173
  const legacyWavesDir = path.join(docsDir, "waves");
158
- const adhocRootDir = path.join(REPO_ROOT, ".wave", "adhoc");
174
+ const adhocRootDir = path.join(REPO_ROOT, ".wave", "adhoc", projectId);
159
175
  const adhocRunDir = adhocRunId ? path.join(adhocRootDir, "runs", adhocRunId) : null;
176
+ const statePrefix = projectStatePrefix(projectId, laneProfile);
160
177
  const baseStateDir = adhocRunId
161
- ? path.join(REPO_ROOT, laneProfile.paths.stateRoot, `${lane}-wave-launcher`, "adhoc", adhocRunId)
162
- : path.join(REPO_ROOT, laneProfile.paths.stateRoot, `${lane}-wave-launcher`);
178
+ ? path.join(
179
+ REPO_ROOT,
180
+ laneProfile.paths.stateRoot,
181
+ statePrefix,
182
+ `${lane}-wave-launcher`,
183
+ "adhoc",
184
+ adhocRunId,
185
+ )
186
+ : path.join(
187
+ REPO_ROOT,
188
+ laneProfile.paths.stateRoot,
189
+ statePrefix,
190
+ `${lane}-wave-launcher`,
191
+ );
163
192
  const stateDir = runVariant === "dry-run" ? path.join(baseStateDir, "dry-run") : baseStateDir;
164
193
  const orchestratorStateDir =
165
194
  runVariant === "dry-run"
166
195
  ? path.join(stateDir, "orchestrator")
167
- : path.join(REPO_ROOT, laneProfile.paths.orchestratorStateDir);
196
+ : path.join(REPO_ROOT, laneProfile.paths.orchestratorStateDir, statePrefix);
168
197
  const feedbackStateDir = path.join(orchestratorStateDir, "feedback");
169
198
  return {
170
199
  config,
200
+ project: projectId,
171
201
  laneProfile,
172
202
  lane,
173
203
  runKind,
@@ -249,7 +279,8 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
249
279
  executors: laneProfile.executors,
250
280
  skills: laneProfile.skills,
251
281
  capabilityRouting: laneProfile.capabilityRouting,
252
- projectId: buildTelemetryProjectId(config),
282
+ projectId: buildTelemetryProjectId(laneProfile.waveControl || { projectId }),
283
+ projectRootDir: path.join(REPO_ROOT, laneProfile.projectRootDir || "."),
253
284
  runtimeVersion: readRuntimeVersion(),
254
285
  orchestratorId: null,
255
286
  waveControl: laneProfile.waveControl,
@@ -257,13 +288,13 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
257
288
  defaultRunStatePath: path.join(stateDir, "run-state.json"),
258
289
  globalDashboardPath: path.join(stateDir, "dashboards", "global.json"),
259
290
  launcherLockPath: path.join(stateDir, "launcher.lock"),
260
- terminalNamePrefix: `${lane}-wave`,
261
- dashboardTerminalNamePrefix: `${lane}-wave-dashboard`,
262
- globalDashboardTerminalName: `${lane}-wave-dashboard-global`,
263
- tmuxSessionPrefix: `oc_${laneTmux}_${workspaceTmuxToken}_wave`,
264
- tmuxDashboardSessionPrefix: `oc_${laneTmux}_${workspaceTmuxToken}_wave_dashboard`,
265
- tmuxGlobalDashboardSessionPrefix: `oc_${laneTmux}_${workspaceTmuxToken}_wave_dashboard_global`,
266
- tmuxSocketName: `oc_${laneTmux}_${workspaceTmuxToken}_waves`,
291
+ terminalNamePrefix: `${projectId}-${lane}-wave`,
292
+ dashboardTerminalNamePrefix: `${projectId}-${lane}-wave-dashboard`,
293
+ globalDashboardTerminalName: `${projectId}-${lane}-wave-dashboard-global`,
294
+ tmuxSessionPrefix: `oc_${projectToken}_${laneTmux}_${workspaceTmuxToken}_wave`,
295
+ tmuxDashboardSessionPrefix: `oc_${projectToken}_${laneTmux}_${workspaceTmuxToken}_wave_dashboard`,
296
+ tmuxGlobalDashboardSessionPrefix: `oc_${projectToken}_${laneTmux}_${workspaceTmuxToken}_wave_dashboard_global`,
297
+ tmuxSocketName: `oc_${projectToken}_${laneTmux}_${workspaceTmuxToken}_waves`,
267
298
  orchestratorStateDir,
268
299
  defaultOrchestratorBoardPath: path.join(
269
300
  orchestratorStateDir,
@@ -273,7 +304,7 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
273
304
  feedbackStateDir,
274
305
  feedbackRequestsDir: path.join(feedbackStateDir, "requests"),
275
306
  feedbackTriageDir: path.join(stateDir, "feedback", "triage"),
276
- crossLaneDependenciesDir: path.join(REPO_ROOT, laneProfile.paths.orchestratorStateDir, "dependencies"),
307
+ crossLaneDependenciesDir: path.join(orchestratorStateDir, "dependencies"),
277
308
  runtimePolicy: laneProfile.runtimePolicy,
278
309
  };
279
310
  }
@@ -340,6 +371,38 @@ export function readJsonOrNull(filePath) {
340
371
  }
341
372
  }
342
373
 
374
+ export function findAdhocRunRecord(runId) {
375
+ const normalizedRunId = sanitizeAdhocRunId(runId);
376
+ const adhocRoot = path.join(REPO_ROOT, ".wave", "adhoc");
377
+ const legacyResultPath = path.join(adhocRoot, "runs", normalizedRunId, "result.json");
378
+ const legacyResult = readJsonOrNull(legacyResultPath);
379
+ if (legacyResult) {
380
+ return {
381
+ project: legacyResult.project || DEFAULT_PROJECT_ID,
382
+ resultPath: legacyResultPath,
383
+ result: legacyResult,
384
+ };
385
+ }
386
+ if (!fs.existsSync(adhocRoot)) {
387
+ return null;
388
+ }
389
+ for (const entry of fs.readdirSync(adhocRoot, { withFileTypes: true })) {
390
+ if (!entry.isDirectory() || entry.name === "runs") {
391
+ continue;
392
+ }
393
+ const candidatePath = path.join(adhocRoot, entry.name, "runs", normalizedRunId, "result.json");
394
+ const candidate = readJsonOrNull(candidatePath);
395
+ if (candidate) {
396
+ return {
397
+ project: candidate.project || entry.name,
398
+ resultPath: candidatePath,
399
+ result: candidate,
400
+ };
401
+ }
402
+ }
403
+ return null;
404
+ }
405
+
343
406
  export function toIsoTimestamp() {
344
407
  return new Date().toISOString();
345
408
  }