@chllming/wave-orchestration 0.5.4 → 0.6.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 (126) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/README.md +33 -5
  3. package/docs/README.md +18 -4
  4. package/docs/agents/wave-cont-eval-role.md +36 -0
  5. package/docs/agents/{wave-evaluator-role.md → wave-cont-qa-role.md} +14 -11
  6. package/docs/agents/wave-documentation-role.md +1 -1
  7. package/docs/agents/wave-infra-role.md +1 -1
  8. package/docs/agents/wave-integration-role.md +3 -3
  9. package/docs/agents/wave-launcher-role.md +4 -3
  10. package/docs/agents/wave-security-role.md +40 -0
  11. package/docs/concepts/context7-vs-skills.md +1 -1
  12. package/docs/concepts/what-is-a-wave.md +56 -6
  13. package/docs/evals/README.md +166 -0
  14. package/docs/evals/benchmark-catalog.json +663 -0
  15. package/docs/guides/author-and-run-waves.md +135 -0
  16. package/docs/guides/planner.md +5 -0
  17. package/docs/guides/terminal-surfaces.md +2 -0
  18. package/docs/plans/component-cutover-matrix.json +1 -1
  19. package/docs/plans/component-cutover-matrix.md +1 -1
  20. package/docs/plans/current-state.md +19 -1
  21. package/docs/plans/examples/wave-example-live-proof.md +435 -0
  22. package/docs/plans/migration.md +42 -0
  23. package/docs/plans/wave-orchestrator.md +46 -7
  24. package/docs/plans/waves/wave-0.md +4 -4
  25. package/docs/reference/live-proof-waves.md +177 -0
  26. package/docs/reference/migration-0.2-to-0.5.md +26 -19
  27. package/docs/reference/npmjs-trusted-publishing.md +6 -5
  28. package/docs/reference/runtime-config/README.md +14 -4
  29. package/docs/reference/sample-waves.md +87 -0
  30. package/docs/reference/skills.md +110 -42
  31. package/docs/research/agent-context-sources.md +130 -11
  32. package/docs/research/coordination-failure-review.md +266 -0
  33. package/docs/roadmap.md +6 -2
  34. package/package.json +2 -2
  35. package/releases/manifest.json +35 -2
  36. package/scripts/research/agent-context-archive.mjs +83 -1
  37. package/scripts/research/manifests/agent-context-expanded-2026-03-22.mjs +811 -0
  38. package/scripts/wave-orchestrator/adhoc.mjs +1331 -0
  39. package/scripts/wave-orchestrator/agent-state.mjs +358 -6
  40. package/scripts/wave-orchestrator/artifact-schemas.mjs +173 -0
  41. package/scripts/wave-orchestrator/clarification-triage.mjs +10 -3
  42. package/scripts/wave-orchestrator/config.mjs +48 -12
  43. package/scripts/wave-orchestrator/context7.mjs +2 -0
  44. package/scripts/wave-orchestrator/coord-cli.mjs +51 -19
  45. package/scripts/wave-orchestrator/coordination-store.mjs +26 -4
  46. package/scripts/wave-orchestrator/coordination.mjs +83 -9
  47. package/scripts/wave-orchestrator/dashboard-state.mjs +20 -8
  48. package/scripts/wave-orchestrator/dep-cli.mjs +5 -2
  49. package/scripts/wave-orchestrator/docs-queue.mjs +8 -2
  50. package/scripts/wave-orchestrator/evals.mjs +451 -0
  51. package/scripts/wave-orchestrator/feedback.mjs +15 -1
  52. package/scripts/wave-orchestrator/install.mjs +32 -9
  53. package/scripts/wave-orchestrator/launcher-closure.mjs +281 -0
  54. package/scripts/wave-orchestrator/launcher-runtime.mjs +334 -0
  55. package/scripts/wave-orchestrator/launcher.mjs +709 -601
  56. package/scripts/wave-orchestrator/ledger.mjs +123 -20
  57. package/scripts/wave-orchestrator/local-executor.mjs +99 -12
  58. package/scripts/wave-orchestrator/planner.mjs +177 -42
  59. package/scripts/wave-orchestrator/replay.mjs +6 -3
  60. package/scripts/wave-orchestrator/role-helpers.mjs +84 -0
  61. package/scripts/wave-orchestrator/shared.mjs +75 -11
  62. package/scripts/wave-orchestrator/skills.mjs +637 -106
  63. package/scripts/wave-orchestrator/traces.mjs +71 -48
  64. package/scripts/wave-orchestrator/wave-files.mjs +947 -101
  65. package/scripts/wave.mjs +9 -0
  66. package/skills/README.md +202 -0
  67. package/skills/provider-aws/SKILL.md +111 -0
  68. package/skills/provider-aws/adapters/claude.md +1 -0
  69. package/skills/provider-aws/adapters/codex.md +1 -0
  70. package/skills/provider-aws/references/service-verification.md +39 -0
  71. package/skills/provider-aws/skill.json +50 -1
  72. package/skills/provider-custom-deploy/SKILL.md +59 -0
  73. package/skills/provider-custom-deploy/skill.json +46 -1
  74. package/skills/provider-docker-compose/SKILL.md +90 -0
  75. package/skills/provider-docker-compose/adapters/local.md +1 -0
  76. package/skills/provider-docker-compose/skill.json +49 -1
  77. package/skills/provider-github-release/SKILL.md +116 -1
  78. package/skills/provider-github-release/adapters/claude.md +1 -0
  79. package/skills/provider-github-release/adapters/codex.md +1 -0
  80. package/skills/provider-github-release/skill.json +51 -1
  81. package/skills/provider-kubernetes/SKILL.md +137 -0
  82. package/skills/provider-kubernetes/adapters/claude.md +1 -0
  83. package/skills/provider-kubernetes/adapters/codex.md +1 -0
  84. package/skills/provider-kubernetes/references/kubectl-patterns.md +58 -0
  85. package/skills/provider-kubernetes/skill.json +48 -1
  86. package/skills/provider-railway/SKILL.md +118 -1
  87. package/skills/provider-railway/references/verification-commands.md +39 -0
  88. package/skills/provider-railway/skill.json +67 -1
  89. package/skills/provider-ssh-manual/SKILL.md +91 -0
  90. package/skills/provider-ssh-manual/skill.json +50 -1
  91. package/skills/repo-coding-rules/SKILL.md +84 -0
  92. package/skills/repo-coding-rules/skill.json +30 -1
  93. package/skills/role-cont-eval/SKILL.md +90 -0
  94. package/skills/role-cont-eval/adapters/codex.md +1 -0
  95. package/skills/role-cont-eval/skill.json +36 -0
  96. package/skills/role-cont-qa/SKILL.md +93 -0
  97. package/skills/role-cont-qa/adapters/claude.md +1 -0
  98. package/skills/role-cont-qa/skill.json +36 -0
  99. package/skills/role-deploy/SKILL.md +90 -0
  100. package/skills/role-deploy/skill.json +32 -1
  101. package/skills/role-documentation/SKILL.md +66 -0
  102. package/skills/role-documentation/skill.json +32 -1
  103. package/skills/role-implementation/SKILL.md +62 -0
  104. package/skills/role-implementation/skill.json +32 -1
  105. package/skills/role-infra/SKILL.md +74 -0
  106. package/skills/role-infra/skill.json +32 -1
  107. package/skills/role-integration/SKILL.md +79 -1
  108. package/skills/role-integration/skill.json +32 -1
  109. package/skills/role-research/SKILL.md +58 -0
  110. package/skills/role-research/skill.json +32 -1
  111. package/skills/role-security/SKILL.md +60 -0
  112. package/skills/role-security/skill.json +36 -0
  113. package/skills/runtime-claude/SKILL.md +60 -1
  114. package/skills/runtime-claude/skill.json +32 -1
  115. package/skills/runtime-codex/SKILL.md +52 -1
  116. package/skills/runtime-codex/skill.json +32 -1
  117. package/skills/runtime-local/SKILL.md +39 -0
  118. package/skills/runtime-local/skill.json +32 -1
  119. package/skills/runtime-opencode/SKILL.md +51 -0
  120. package/skills/runtime-opencode/skill.json +32 -1
  121. package/skills/wave-core/SKILL.md +107 -0
  122. package/skills/wave-core/references/marker-syntax.md +62 -0
  123. package/skills/wave-core/skill.json +31 -1
  124. package/wave.config.json +35 -6
  125. package/skills/role-evaluator/SKILL.md +0 -6
  126. package/skills/role-evaluator/skill.json +0 -5
@@ -0,0 +1,281 @@
1
+ import path from "node:path";
2
+ import {
3
+ parseStructuredSignalsFromLog,
4
+ refreshWaveDashboardAgentStates,
5
+ setWaveDashboardAgent,
6
+ updateWaveDashboardMessageBoard,
7
+ } from "./dashboard-state.mjs";
8
+ import { REPO_ROOT, toIsoTimestamp } from "./shared.mjs";
9
+ import { isSecurityReviewAgent } from "./role-helpers.mjs";
10
+ import { summarizeResolvedSkills } from "./skills.mjs";
11
+
12
+ function failureResultFromGate(gate, fallbackLogPath) {
13
+ return {
14
+ failures: [
15
+ {
16
+ agentId: gate.agentId,
17
+ statusCode: gate.statusCode,
18
+ logPath: gate.logPath || fallbackLogPath,
19
+ detail: gate.detail,
20
+ },
21
+ ],
22
+ timedOut: false,
23
+ };
24
+ }
25
+
26
+ function recordClosureGateFailure({
27
+ wave,
28
+ lanePaths,
29
+ gate,
30
+ label,
31
+ recordCombinedEvent,
32
+ appendCoordination,
33
+ actionRequested,
34
+ }) {
35
+ recordCombinedEvent({
36
+ level: "error",
37
+ agentId: gate.agentId,
38
+ message: `${label} blocked wave ${wave.wave}: ${gate.detail}`,
39
+ });
40
+ appendCoordination({
41
+ event: "wave_gate_blocked",
42
+ waves: [wave.wave],
43
+ status: "blocked",
44
+ details: `agent=${gate.agentId}; reason=${gate.statusCode}; ${gate.detail}`,
45
+ actionRequested:
46
+ actionRequested ||
47
+ `Lane ${lanePaths.lane} owners should resolve the ${label.toLowerCase()} before wave progression.`,
48
+ });
49
+ }
50
+
51
+ export async function runClosureSweepPhase({
52
+ lanePaths,
53
+ wave,
54
+ closureRuns,
55
+ coordinationLogPath,
56
+ refreshDerivedState,
57
+ dashboardState,
58
+ recordCombinedEvent,
59
+ flushDashboards,
60
+ options,
61
+ feedbackStateByRequestId,
62
+ appendCoordination,
63
+ launchAgentSessionFn,
64
+ waitForWaveCompletionFn,
65
+ readWaveContEvalGateFn,
66
+ readWaveSecurityGateFn,
67
+ readWaveIntegrationBarrierFn,
68
+ readWaveDocumentationGateFn,
69
+ readWaveComponentMatrixGateFn,
70
+ readWaveContQaGateFn,
71
+ materializeAgentExecutionSummaryForRunFn,
72
+ monitorWaveHumanFeedbackFn,
73
+ }) {
74
+ const contQaAgentId = wave.contQaAgentId || "A0";
75
+ const contEvalAgentId = wave.contEvalAgentId || lanePaths.contEvalAgentId || "E0";
76
+ const integrationAgentId = wave.integrationAgentId || lanePaths.integrationAgentId || "A8";
77
+ const documentationAgentId = wave.documentationAgentId || "A9";
78
+ const stagedRuns = [
79
+ {
80
+ agentId: contEvalAgentId,
81
+ label: "cont-EVAL gate",
82
+ runs: closureRuns.filter((run) => run.agent.agentId === contEvalAgentId),
83
+ validate: () =>
84
+ readWaveContEvalGateFn(wave, closureRuns, {
85
+ contEvalAgentId,
86
+ mode: "live",
87
+ evalTargets: wave.evalTargets,
88
+ benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
89
+ }),
90
+ actionRequested:
91
+ `Lane ${lanePaths.lane} owners should resolve cont-EVAL tuning gaps before integration closure.`,
92
+ },
93
+ {
94
+ agentId: "security",
95
+ label: "Security review",
96
+ runs: closureRuns.filter((run) => isSecurityReviewAgent(run.agent)),
97
+ validate: () => readWaveSecurityGateFn(wave, closureRuns),
98
+ actionRequested:
99
+ `Lane ${lanePaths.lane} owners should resolve blocked security findings or missing approvals before integration closure.`,
100
+ },
101
+ {
102
+ agentId: integrationAgentId,
103
+ label: "Integration gate",
104
+ runs: closureRuns.filter((run) => run.agent.agentId === integrationAgentId),
105
+ validate: () =>
106
+ readWaveIntegrationBarrierFn(
107
+ wave,
108
+ closureRuns,
109
+ refreshDerivedState?.(dashboardState?.attempt || 0),
110
+ {
111
+ integrationAgentId,
112
+ requireIntegrationStewardFromWave: lanePaths.requireIntegrationStewardFromWave,
113
+ },
114
+ ),
115
+ actionRequested:
116
+ `Lane ${lanePaths.lane} owners should resolve integration contradictions or blockers before documentation and cont-QA closure.`,
117
+ },
118
+ {
119
+ agentId: documentationAgentId,
120
+ label: "Documentation closure",
121
+ runs: closureRuns.filter((run) => run.agent.agentId === documentationAgentId),
122
+ validate: () => {
123
+ const documentationGate = readWaveDocumentationGateFn(wave, closureRuns);
124
+ if (!documentationGate.ok) {
125
+ return documentationGate;
126
+ }
127
+ return readWaveComponentMatrixGateFn(wave, closureRuns, {
128
+ laneProfile: lanePaths.laneProfile,
129
+ documentationAgentId,
130
+ });
131
+ },
132
+ actionRequested:
133
+ `Lane ${lanePaths.lane} owners should resolve the shared-plan or component-matrix closure state before cont-QA progression.`,
134
+ },
135
+ {
136
+ agentId: contQaAgentId,
137
+ label: "cont-QA gate",
138
+ runs: closureRuns.filter((run) => run.agent.agentId === contQaAgentId),
139
+ validate: () => readWaveContQaGateFn(wave, closureRuns, { contQaAgentId, mode: "live" }),
140
+ actionRequested:
141
+ `Lane ${lanePaths.lane} owners should resolve the cont-QA gate before wave progression.`,
142
+ },
143
+ ];
144
+ for (const stage of stagedRuns) {
145
+ if (stage.runs.length === 0) {
146
+ continue;
147
+ }
148
+ for (const runInfo of stage.runs) {
149
+ const existing = dashboardState.agents.find((entry) => entry.agentId === runInfo.agent.agentId);
150
+ setWaveDashboardAgent(dashboardState, runInfo.agent.agentId, {
151
+ state: "launching",
152
+ attempts: (existing?.attempts || 0) + 1,
153
+ startedAt: existing?.startedAt || toIsoTimestamp(),
154
+ completedAt: null,
155
+ exitCode: null,
156
+ detail: "Launching closure sweep",
157
+ });
158
+ flushDashboards();
159
+ const launchResult = await launchAgentSessionFn(lanePaths, {
160
+ wave: wave.wave,
161
+ waveDefinition: wave,
162
+ agent: runInfo.agent,
163
+ sessionName: runInfo.sessionName,
164
+ promptPath: runInfo.promptPath,
165
+ logPath: runInfo.logPath,
166
+ statusPath: runInfo.statusPath,
167
+ messageBoardPath: runInfo.messageBoardPath,
168
+ messageBoardSnapshot: runInfo.messageBoardSnapshot || "",
169
+ sharedSummaryPath: runInfo.sharedSummaryPath,
170
+ sharedSummaryText: runInfo.sharedSummaryText,
171
+ inboxPath: runInfo.inboxPath,
172
+ inboxText: runInfo.inboxText,
173
+ orchestratorId: options.orchestratorId,
174
+ executorMode: options.executorMode,
175
+ codexSandboxMode: options.codexSandboxMode,
176
+ agentRateLimitRetries: options.agentRateLimitRetries,
177
+ agentRateLimitBaseDelaySeconds: options.agentRateLimitBaseDelaySeconds,
178
+ agentRateLimitMaxDelaySeconds: options.agentRateLimitMaxDelaySeconds,
179
+ context7Enabled: options.context7Enabled,
180
+ });
181
+ runInfo.lastLaunchAttempt = dashboardState?.attempt || null;
182
+ runInfo.lastPromptHash = launchResult?.promptHash || null;
183
+ runInfo.lastContext7 = launchResult?.context7 || null;
184
+ runInfo.lastExecutorId = launchResult?.executorId || runInfo.agent.executorResolved?.id || null;
185
+ runInfo.lastSkillProjection =
186
+ launchResult?.skills || summarizeResolvedSkills(runInfo.agent.skillsResolved);
187
+ setWaveDashboardAgent(dashboardState, runInfo.agent.agentId, {
188
+ state: "running",
189
+ detail: `Closure sweep launched${launchResult?.context7?.mode ? ` (${launchResult.context7.mode})` : ""}`,
190
+ });
191
+ recordCombinedEvent({
192
+ agentId: runInfo.agent.agentId,
193
+ message: `Closure sweep launched in tmux session ${runInfo.sessionName}`,
194
+ });
195
+ flushDashboards();
196
+ const result = await waitForWaveCompletionFn(
197
+ lanePaths,
198
+ [runInfo],
199
+ options.timeoutMinutes,
200
+ ({ pendingAgentIds }) => {
201
+ refreshWaveDashboardAgentStates(dashboardState, [runInfo], pendingAgentIds, (event) =>
202
+ recordCombinedEvent(event),
203
+ );
204
+ monitorWaveHumanFeedbackFn({
205
+ lanePaths,
206
+ waveNumber: wave.wave,
207
+ agentRuns: [runInfo],
208
+ orchestratorId: options.orchestratorId,
209
+ coordinationLogPath,
210
+ feedbackStateByRequestId,
211
+ recordCombinedEvent,
212
+ appendCoordination,
213
+ });
214
+ updateWaveDashboardMessageBoard(dashboardState, runInfo.messageBoardPath);
215
+ flushDashboards();
216
+ },
217
+ );
218
+ materializeAgentExecutionSummaryForRunFn(wave, runInfo);
219
+ refreshDerivedState?.(dashboardState?.attempt || 0);
220
+ if (result.failures.length > 0) {
221
+ return result;
222
+ }
223
+ }
224
+ const gate = stage.validate();
225
+ if (!gate.ok) {
226
+ recordClosureGateFailure({
227
+ wave,
228
+ lanePaths,
229
+ gate,
230
+ label: stage.label,
231
+ recordCombinedEvent,
232
+ appendCoordination,
233
+ actionRequested: stage.actionRequested,
234
+ });
235
+ return failureResultFromGate(
236
+ gate,
237
+ stage.runs[0]?.logPath ? path.relative(REPO_ROOT, stage.runs[0].logPath) : null,
238
+ );
239
+ }
240
+ }
241
+ return { failures: [], timedOut: false };
242
+ }
243
+
244
+ const NON_BLOCKING_INFRA_SIGNAL_STATES = new Set([
245
+ "conformant",
246
+ "setup-required",
247
+ "setup-in-progress",
248
+ "action-required",
249
+ "action-approved",
250
+ "action-complete",
251
+ ]);
252
+
253
+ export function readWaveInfraGate(agentRuns) {
254
+ for (const run of agentRuns) {
255
+ const signals = parseStructuredSignalsFromLog(run.logPath);
256
+ if (!signals?.infra) {
257
+ continue;
258
+ }
259
+ const infra = signals.infra;
260
+ const normalizedState = String(infra.state || "")
261
+ .trim()
262
+ .toLowerCase();
263
+ if (NON_BLOCKING_INFRA_SIGNAL_STATES.has(normalizedState)) {
264
+ continue;
265
+ }
266
+ return {
267
+ ok: false,
268
+ agentId: run.agent.agentId,
269
+ statusCode: `infra-${normalizedState || "blocked"}`,
270
+ detail: `Infra signal ${infra.kind || "unknown"} on ${infra.target || "unknown"} ended in state ${normalizedState || "unknown"}${infra.detail ? ` (${infra.detail})` : ""}.`,
271
+ logPath: path.relative(REPO_ROOT, run.logPath),
272
+ };
273
+ }
274
+ return {
275
+ ok: true,
276
+ agentId: null,
277
+ statusCode: "pass",
278
+ detail: "No blocking infra signals detected.",
279
+ logPath: null,
280
+ };
281
+ }
@@ -0,0 +1,334 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { buildExecutionPrompt } from "./coordination.mjs";
4
+ import {
5
+ DEFAULT_AGENT_RATE_LIMIT_BASE_DELAY_SECONDS,
6
+ DEFAULT_AGENT_RATE_LIMIT_MAX_DELAY_SECONDS,
7
+ DEFAULT_WAIT_PROGRESS_INTERVAL_MS,
8
+ REPO_ROOT,
9
+ ensureDirectory,
10
+ shellQuote,
11
+ writeJsonAtomic,
12
+ } from "./shared.mjs";
13
+ import { readStatusCodeIfPresent } from "./dashboard-state.mjs";
14
+ import { buildExecutorLaunchSpec } from "./executors.mjs";
15
+ import { hashAgentPromptFingerprint, prefetchContext7ForSelection } from "./context7.mjs";
16
+ import { killTmuxSessionIfExists } from "./terminals.mjs";
17
+ import {
18
+ resolveAgentSkills,
19
+ summarizeResolvedSkills,
20
+ writeResolvedSkillArtifacts,
21
+ } from "./skills.mjs";
22
+
23
+ export function refreshResolvedSkillsForRun(runInfo, waveDefinition, lanePaths) {
24
+ runInfo.agent.skillsResolved = resolveAgentSkills(
25
+ runInfo.agent,
26
+ waveDefinition || { deployEnvironments: [] },
27
+ { laneProfile: lanePaths.laneProfile },
28
+ );
29
+ return runInfo.agent.skillsResolved;
30
+ }
31
+
32
+ export function collectUnexpectedSessionFailures(
33
+ lanePaths,
34
+ agentRuns,
35
+ pendingAgentIds,
36
+ { listLaneTmuxSessionNamesFn },
37
+ ) {
38
+ const activeSessionNames = new Set(listLaneTmuxSessionNamesFn(lanePaths));
39
+ const failures = [];
40
+ for (const run of agentRuns) {
41
+ if (!pendingAgentIds.has(run.agent.agentId) || fs.existsSync(run.statusPath)) {
42
+ continue;
43
+ }
44
+ if (activeSessionNames.has(run.sessionName)) {
45
+ continue;
46
+ }
47
+ failures.push({
48
+ agentId: run.agent.agentId,
49
+ statusCode: "session-missing",
50
+ logPath: path.relative(REPO_ROOT, run.logPath),
51
+ detail: `tmux session ${run.sessionName} disappeared before ${path.relative(REPO_ROOT, run.statusPath)} was written.`,
52
+ });
53
+ }
54
+ return failures;
55
+ }
56
+
57
+ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
58
+ const {
59
+ wave,
60
+ waveDefinition = null,
61
+ agent,
62
+ sessionName,
63
+ promptPath,
64
+ logPath,
65
+ statusPath,
66
+ messageBoardPath,
67
+ messageBoardSnapshot,
68
+ sharedSummaryPath,
69
+ sharedSummaryText,
70
+ inboxPath,
71
+ inboxText,
72
+ orchestratorId,
73
+ agentRateLimitRetries,
74
+ agentRateLimitBaseDelaySeconds,
75
+ agentRateLimitMaxDelaySeconds,
76
+ context7Enabled,
77
+ dryRun = false,
78
+ } = params;
79
+ ensureDirectory(path.dirname(promptPath));
80
+ ensureDirectory(path.dirname(logPath));
81
+ ensureDirectory(path.dirname(statusPath));
82
+ fs.rmSync(statusPath, { force: true });
83
+
84
+ const context7 = await prefetchContext7ForSelection(agent.context7Resolved, {
85
+ cacheDir: lanePaths.context7CacheDir,
86
+ disabled: !context7Enabled,
87
+ });
88
+ const overlayDir = path.join(lanePaths.executorOverlaysDir, `wave-${wave}`, agent.slug);
89
+ ensureDirectory(overlayDir);
90
+ const resolvedWaveDefinition = waveDefinition || { deployEnvironments: [] };
91
+ const skillsResolved =
92
+ agent.skillsResolved ||
93
+ resolveAgentSkills(agent, resolvedWaveDefinition, {
94
+ laneProfile: lanePaths.laneProfile,
95
+ });
96
+ agent.skillsResolved = skillsResolved;
97
+ const skillArtifacts = writeResolvedSkillArtifacts(overlayDir, skillsResolved);
98
+ if (skillArtifacts) {
99
+ agent.skillsResolved = {
100
+ ...skillsResolved,
101
+ artifacts: skillArtifacts,
102
+ };
103
+ }
104
+ const prompt = buildExecutionPrompt({
105
+ lane: lanePaths.lane,
106
+ wave,
107
+ agent,
108
+ orchestratorId,
109
+ messageBoardPath,
110
+ messageBoardSnapshot,
111
+ sharedSummaryPath,
112
+ sharedSummaryText,
113
+ inboxPath,
114
+ inboxText,
115
+ context7,
116
+ componentPromotions: resolvedWaveDefinition.componentPromotions,
117
+ evalTargets: resolvedWaveDefinition.evalTargets,
118
+ benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
119
+ sharedPlanDocs: lanePaths.sharedPlanDocs,
120
+ contQaAgentId: lanePaths.contQaAgentId,
121
+ contEvalAgentId: lanePaths.contEvalAgentId,
122
+ integrationAgentId: lanePaths.integrationAgentId,
123
+ documentationAgentId: lanePaths.documentationAgentId,
124
+ });
125
+ const promptHash = hashAgentPromptFingerprint(agent);
126
+ fs.writeFileSync(promptPath, `${prompt}\n`, "utf8");
127
+ const launchSpec = buildExecutorLaunchSpec({
128
+ agent,
129
+ promptPath,
130
+ logPath,
131
+ overlayDir,
132
+ skillProjection: agent.skillsResolved,
133
+ });
134
+ const resolvedExecutorMode = launchSpec.executorId || agent.executorResolved?.id || "codex";
135
+ if (dryRun) {
136
+ writeJsonAtomic(path.join(overlayDir, "launch-preview.json"), {
137
+ executorId: resolvedExecutorMode,
138
+ command: launchSpec.command,
139
+ env: launchSpec.env || {},
140
+ useRateLimitRetries: launchSpec.useRateLimitRetries === true,
141
+ invocationLines: launchSpec.invocationLines,
142
+ skills: summarizeResolvedSkills(agent.skillsResolved),
143
+ });
144
+ return {
145
+ promptHash,
146
+ context7,
147
+ executorId: resolvedExecutorMode,
148
+ launchSpec,
149
+ dryRun: true,
150
+ skills: summarizeResolvedSkills(agent.skillsResolved),
151
+ };
152
+ }
153
+ killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
154
+
155
+ const executionLines = [];
156
+ if (launchSpec.env) {
157
+ for (const [key, value] of Object.entries(launchSpec.env)) {
158
+ executionLines.push(`export ${key}=${shellQuote(value)}`);
159
+ }
160
+ }
161
+ if (!launchSpec.useRateLimitRetries) {
162
+ executionLines.push(...launchSpec.invocationLines);
163
+ executionLines.push("status=$?");
164
+ } else {
165
+ executionLines.push(`: > ${shellQuote(logPath)}`);
166
+ executionLines.push(
167
+ `max_rate_attempts=${Math.max(1, Number.parseInt(String(agentRateLimitRetries || 0), 10) + 1)}`,
168
+ );
169
+ executionLines.push(
170
+ `rate_delay_base=${Math.max(1, Number.parseInt(String(agentRateLimitBaseDelaySeconds || DEFAULT_AGENT_RATE_LIMIT_BASE_DELAY_SECONDS), 10))}`,
171
+ );
172
+ executionLines.push(
173
+ `rate_delay_max=${Math.max(1, Number.parseInt(String(agentRateLimitMaxDelaySeconds || DEFAULT_AGENT_RATE_LIMIT_MAX_DELAY_SECONDS), 10))}`,
174
+ );
175
+ executionLines.push("rate_attempt=1");
176
+ executionLines.push("status=1");
177
+ executionLines.push('while [ "$rate_attempt" -le "$max_rate_attempts" ]; do');
178
+ for (const line of launchSpec.invocationLines) {
179
+ executionLines.push(` ${line}`);
180
+ }
181
+ executionLines.push(" status=$?");
182
+ executionLines.push(' if [ "$status" -eq 0 ]; then');
183
+ executionLines.push(" break");
184
+ executionLines.push(" fi");
185
+ executionLines.push(' if [ "$rate_attempt" -ge "$max_rate_attempts" ]; then');
186
+ executionLines.push(" break");
187
+ executionLines.push(" fi");
188
+ executionLines.push(
189
+ ` if tail -n 120 ${shellQuote(logPath)} | grep -Eqi '429 Too Many Requests|exceeded retry limit|last status: 429|rate limit'; then`,
190
+ );
191
+ executionLines.push(" sleep_seconds=$((rate_delay_base * (2 ** (rate_attempt - 1))))");
192
+ executionLines.push(
193
+ ' if [ "$sleep_seconds" -gt "$rate_delay_max" ]; then sleep_seconds=$rate_delay_max; fi',
194
+ );
195
+ executionLines.push(" jitter=$((RANDOM % 5))");
196
+ executionLines.push(" sleep_seconds=$((sleep_seconds + jitter))");
197
+ executionLines.push(
198
+ ` echo "[${lanePaths.lane}-wave-launcher] rate-limit detected for ${agent.agentId}; retry \${rate_attempt}/\${max_rate_attempts} after \${sleep_seconds}s" | tee -a ${shellQuote(logPath)}`,
199
+ );
200
+ executionLines.push(' sleep "$sleep_seconds"');
201
+ executionLines.push(" rate_attempt=$((rate_attempt + 1))");
202
+ executionLines.push(" continue");
203
+ executionLines.push(" fi");
204
+ executionLines.push(" break");
205
+ executionLines.push("done");
206
+ }
207
+
208
+ const command = [
209
+ `cd ${shellQuote(REPO_ROOT)}`,
210
+ "set -o pipefail",
211
+ `export WAVE_ORCHESTRATOR_ID=${shellQuote(orchestratorId || "")}`,
212
+ `export WAVE_EXECUTOR_MODE=${shellQuote(resolvedExecutorMode)}`,
213
+ ...executionLines,
214
+ `node -e ${shellQuote(
215
+ "const fs=require('node:fs'); const statusPath=process.argv[1]; const payload={code:Number(process.argv[2]),promptHash:process.argv[3]||null,orchestratorId:process.argv[4]||null,completedAt:new Date().toISOString()}; fs.writeFileSync(statusPath, JSON.stringify(payload, null, 2)+'\\n', 'utf8');",
216
+ )} ${shellQuote(statusPath)} "$status" ${shellQuote(promptHash)} ${shellQuote(orchestratorId || "")}`,
217
+ `echo "[${lanePaths.lane}-wave-launcher] ${sessionName} finished with code $status"`,
218
+ "exec bash -l",
219
+ ].join("\n");
220
+
221
+ runTmuxFn(
222
+ lanePaths,
223
+ ["new-session", "-d", "-s", sessionName, `bash -lc ${shellQuote(command)}`],
224
+ `launch session ${sessionName}`,
225
+ );
226
+ return {
227
+ promptHash,
228
+ context7,
229
+ executorId: resolvedExecutorMode,
230
+ skills: summarizeResolvedSkills(agent.skillsResolved),
231
+ };
232
+ }
233
+
234
+ export async function waitForWaveCompletion(
235
+ lanePaths,
236
+ agentRuns,
237
+ timeoutMinutes,
238
+ onProgress = null,
239
+ { collectUnexpectedSessionFailuresFn },
240
+ ) {
241
+ const defaultTimeoutMs = timeoutMinutes * 60 * 1000;
242
+ const startedAt = Date.now();
243
+ const timeoutAtByAgentId = new Map(
244
+ agentRuns.map((run) => {
245
+ const budgetMinutes = Number(run.agent.executorResolved?.budget?.minutes || 0);
246
+ const effectiveBudgetMs =
247
+ Number.isFinite(budgetMinutes) && budgetMinutes > 0
248
+ ? Math.min(defaultTimeoutMs, budgetMinutes * 60 * 1000)
249
+ : defaultTimeoutMs;
250
+ return [run.agent.agentId, startedAt + effectiveBudgetMs];
251
+ }),
252
+ );
253
+ const pending = new Set(agentRuns.map((run) => run.agent.agentId));
254
+ const timedOutAgentIds = new Set();
255
+ let sessionFailures = [];
256
+
257
+ const refreshPending = () => {
258
+ for (const run of agentRuns) {
259
+ if (pending.has(run.agent.agentId) && fs.existsSync(run.statusPath)) {
260
+ pending.delete(run.agent.agentId);
261
+ }
262
+ }
263
+ };
264
+
265
+ await new Promise((resolve) => {
266
+ const interval = setInterval(() => {
267
+ refreshPending();
268
+ onProgress?.({ pendingAgentIds: new Set(pending), timedOut: false });
269
+ if (pending.size === 0) {
270
+ clearInterval(interval);
271
+ resolve();
272
+ return;
273
+ }
274
+ sessionFailures = collectUnexpectedSessionFailuresFn(lanePaths, agentRuns, pending);
275
+ if (sessionFailures.length > 0) {
276
+ onProgress?.({
277
+ pendingAgentIds: new Set(pending),
278
+ timedOut: false,
279
+ failures: sessionFailures,
280
+ });
281
+ clearInterval(interval);
282
+ resolve();
283
+ return;
284
+ }
285
+ const now = Date.now();
286
+ for (const run of agentRuns) {
287
+ if (!pending.has(run.agent.agentId)) {
288
+ continue;
289
+ }
290
+ const deadline = timeoutAtByAgentId.get(run.agent.agentId) || startedAt + defaultTimeoutMs;
291
+ if (now <= deadline) {
292
+ continue;
293
+ }
294
+ timedOutAgentIds.add(run.agent.agentId);
295
+ pending.delete(run.agent.agentId);
296
+ killTmuxSessionIfExists(lanePaths.tmuxSocketName, run.sessionName);
297
+ }
298
+ if (pending.size === 0) {
299
+ clearInterval(interval);
300
+ resolve();
301
+ }
302
+ }, DEFAULT_WAIT_PROGRESS_INTERVAL_MS);
303
+ refreshPending();
304
+ onProgress?.({ pendingAgentIds: new Set(pending), timedOut: false });
305
+ });
306
+
307
+ if (sessionFailures.length > 0) {
308
+ onProgress?.({ pendingAgentIds: new Set(), timedOut: false, failures: sessionFailures });
309
+ return { failures: sessionFailures, timedOut: false };
310
+ }
311
+
312
+ const failures = [];
313
+ for (const run of agentRuns) {
314
+ const code = readStatusCodeIfPresent(run.statusPath);
315
+ if (code === 0) {
316
+ continue;
317
+ }
318
+ if (code === null || timedOutAgentIds.has(run.agent.agentId)) {
319
+ failures.push({
320
+ agentId: run.agent.agentId,
321
+ statusCode: timedOutAgentIds.has(run.agent.agentId) ? "timeout-no-status" : "missing-status",
322
+ logPath: path.relative(REPO_ROOT, run.logPath),
323
+ });
324
+ continue;
325
+ }
326
+ failures.push({
327
+ agentId: run.agent.agentId,
328
+ statusCode: String(code),
329
+ logPath: path.relative(REPO_ROOT, run.logPath),
330
+ });
331
+ }
332
+ onProgress?.({ pendingAgentIds: new Set(), timedOut: timedOutAgentIds.size > 0 });
333
+ return { failures, timedOut: timedOutAgentIds.size > 0 };
334
+ }