@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
@@ -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) {
@@ -279,6 +279,7 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
279
279
  executors: laneProfile.executors,
280
280
  skills: laneProfile.skills,
281
281
  capabilityRouting: laneProfile.capabilityRouting,
282
+ externalProviders: laneProfile.externalProviders,
282
283
  projectId: buildTelemetryProjectId(laneProfile.waveControl || { projectId }),
283
284
  projectRootDir: path.join(REPO_ROOT, laneProfile.projectRootDir || "."),
284
285
  runtimeVersion: readRuntimeVersion(),