@chllming/wave-orchestration 0.6.0 → 0.6.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 (31) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +75 -30
  3. package/docs/README.md +15 -3
  4. package/docs/concepts/context7-vs-skills.md +24 -0
  5. package/docs/concepts/runtime-agnostic-orchestration.md +17 -2
  6. package/docs/concepts/what-is-a-wave.md +28 -0
  7. package/docs/evals/README.md +4 -2
  8. package/docs/guides/terminal-surfaces.md +2 -0
  9. package/docs/plans/current-state.md +1 -1
  10. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  11. package/docs/plans/migration.md +4 -4
  12. package/docs/plans/wave-orchestrator.md +11 -3
  13. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  14. package/docs/reference/runtime-config/README.md +4 -4
  15. package/docs/reference/runtime-config/claude.md +6 -1
  16. package/docs/reference/runtime-config/codex.md +2 -2
  17. package/docs/reference/runtime-config/opencode.md +1 -1
  18. package/docs/reference/sample-waves.md +4 -4
  19. package/docs/research/agent-context-sources.md +2 -0
  20. package/docs/research/coordination-failure-review.md +37 -13
  21. package/package.json +1 -1
  22. package/releases/manifest.json +33 -0
  23. package/scripts/wave-orchestrator/agent-state.mjs +10 -3
  24. package/scripts/wave-orchestrator/config.mjs +19 -0
  25. package/scripts/wave-orchestrator/dashboard-renderer.mjs +150 -20
  26. package/scripts/wave-orchestrator/dashboard-state.mjs +8 -0
  27. package/scripts/wave-orchestrator/executors.mjs +67 -4
  28. package/scripts/wave-orchestrator/launcher-runtime.mjs +1 -0
  29. package/scripts/wave-orchestrator/launcher.mjs +245 -10
  30. package/scripts/wave-orchestrator/terminals.mjs +25 -0
  31. package/scripts/wave-orchestrator/wave-files.mjs +31 -0
@@ -7,6 +7,8 @@ summary: "Primary external sources used as inspiration for planning, harness des
7
7
 
8
8
  This repository does not commit converted paper/article caches. Keep any hydrated local copies under `docs/research/agent-context-cache/` or another ignored cache directory.
9
9
 
10
+ For a narrative synthesis of the most relevant MAS failure modes and how Wave responds to them, start with [coordination-failure-review.md](./coordination-failure-review.md) and then use this page as the bibliography.
11
+
10
12
  ## Practice Articles
11
13
 
12
14
  - [Harness engineering: leveraging Codex in an agent-first world](https://openai.com/index/harness-engineering/)
@@ -17,7 +17,28 @@ The Wave orchestrator addresses several coordination failure modes constructivel
17
17
 
18
18
  That is materially stronger than the common "agents talk in a shared channel and we hope that was enough" pattern criticized by recent multi-agent papers.
19
19
 
20
- The main weakness is empirical, not architectural. The repo does not yet contain a benchmark family that proves the blackboard actually helps agents reconstruct distributed state under HiddenBench or Silo-Bench style pressure, or that it handles DPBench-style simultaneous coordination reliably.
20
+ The main weakness is empirical, not architectural. The repo now carries coordination-oriented benchmark vocabulary, but it does not yet present enough hard evidence that the blackboard reconstructs distributed state under HiddenBench or Silo-Bench style pressure, or that it handles DPBench-style simultaneous coordination reliably.
21
+
22
+ ## Common MAS Failure Cases
23
+
24
+ The research cited in this repo keeps returning to a fairly stable set of failure modes. In Wave language, the common ones are:
25
+
26
+ - `Cosmetic board, no canonical state`
27
+ Agents appear coordinated because they share a board or chat, but there is no machine-trustable source of truth underneath. Wave responds with a canonical coordination log and treats the board as a projection.
28
+ - `Hidden evidence never gets pooled`
29
+ One agent has the decision-changing fact, but it never reaches the shared state before closure. Wave responds with shared summaries, per-agent inboxes, and integration gating, but this still needs stronger empirical validation.
30
+ - `Communication without global-state reconstruction`
31
+ Agents exchange information, yet nobody reconstructs the correct cross-agent picture. Wave responds with integration summaries and barrier-based closure so the final decision depends on integrated state rather than message volume.
32
+ - `Simultaneous coordination collapse`
33
+ A team that looks competent in serial work falls apart when multiple owners, blockers, or resources must move together. Wave responds with helper assignments, dependency barriers, and staged closure, but still lacks a stronger contention benchmark story.
34
+ - `Expert signal gets averaged away`
35
+ The strongest specialist view is diluted into a weaker compromise. Wave responds with explicit ownership, named stewards, and capability routing instead of free-form consensus, though expertise weighting is still shallow.
36
+ - `Blackboard projection drift`
37
+ The raw shared state may be right, but summaries, inboxes, ledgers, or integration artifacts lose the important fact. Wave responds by compiling those surfaces from canonical state and by adding `blackboard-fidelity` to the eval vocabulary.
38
+ - `Contradictions get smoothed over`
39
+ Conflicting claims look resolved in prose, but the system never turns them into bounded repair work. Wave responds with clarification flow, integration barriers, and contradiction-oriented eval vocabulary, though subtle semantic conflicts can still leak through.
40
+ - `Premature closure`
41
+ Agents say they are done before proof, evals, or integrated state actually support PASS. Wave responds with structured proof markers, exit contracts, eval gates, closure stewards, and replay-visible traces.
21
42
 
22
43
  ## What The Papers Warn About
23
44
 
@@ -175,23 +196,26 @@ That alignment matters. In many MAS projects the docs promise a blackboard, but
175
196
 
176
197
  ## What Is Still Missing To Make The Claim Credible
177
198
 
178
- ### 1. No distributed-information benchmark family yet
199
+ ### 1. The benchmark vocabulary exists, but the empirical proof is still thin
179
200
 
180
- The biggest gap is in [docs/evals/benchmark-catalog.json](../evals/benchmark-catalog.json). The current families are:
201
+ [docs/evals/benchmark-catalog.json](../evals/benchmark-catalog.json) and [docs/evals/README.md](../evals/README.md) now define coordination-oriented benchmark families such as:
181
202
 
182
- - `service-output`
183
- - `latency`
184
- - `quality-regression`
203
+ - `hidden-profile-pooling`
204
+ - `silo-escape`
205
+ - `simultaneous-coordination`
206
+ - `expertise-leverage`
207
+ - `blackboard-fidelity`
208
+ - `contradiction-recovery`
185
209
 
186
- There is nothing yet for:
210
+ That is a real improvement because the repo now has a vocabulary for the exact MAS failures the research highlights.
187
211
 
188
- - hidden-profile reconstruction
189
- - silo escape under partial information
190
- - blackboard consistency across raw log, summary, inboxes, ledger, and integration state
191
- - contradiction injection and recovery
192
- - simultaneous coordination under contention
212
+ The remaining gap is not the absence of categories. The gap is still empirical proof:
193
213
 
194
- So the repo can reasonably claim "we built mechanisms intended to mitigate these failures," but it cannot yet claim "we demonstrated that these mechanisms overcome the failures highlighted by HiddenBench, Silo-Bench, or DPBench."
214
+ - not enough published results showing those families are exercised systematically
215
+ - not enough evidence that the blackboard actually improves hidden-state reconstruction
216
+ - not enough stress data showing simultaneous coordination remains reliable under contention
217
+
218
+ So the repo can reasonably claim "we built mechanisms and eval categories intended to mitigate these failures," but it still cannot claim "we demonstrated that those mechanisms consistently overcome the failures highlighted by HiddenBench, Silo-Bench, or DPBench."
195
219
 
196
220
  ### 2. Information integration is supported, but not measured directly
197
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chllming/wave-orchestration",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,6 +2,39 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.6.2",
7
+ "date": "2026-03-22",
8
+ "summary": "Runtime preview visibility, dashboard/operator UX fixes, dry-run cleanup, and safer shared-component retries.",
9
+ "features": [
10
+ "Claude runtime config now exposes first-class `claude.effort`, and runtime previews now include structured `limits` metadata for known attempt and turn ceilings.",
11
+ "Codex previews and docs now make turn-limit visibility explicit: Wave records when it emitted no Codex turn-limit flag and warns that any effective ceiling may come from the selected Codex profile or upstream runtime.",
12
+ "The dashboard surface now distinguishes done, active, pending, and failed counts, keeps a stable `Current Wave Dashboard` terminal target, and adds simple TTY color cues for faster scanning.",
13
+ "Dry-run executor preview directories are pruned when wave agent sets shrink, so stale overlay folders no longer linger under `.tmp/.../dry-run/executors/`.",
14
+ "Shared promoted-component retries now preserve already-landed owner slices and relaunch only the sibling owners still required for closure proof."
15
+ ],
16
+ "manualSteps": [
17
+ "After upgrading, rerun `pnpm exec wave doctor` and `pnpm exec wave launch --lane main --dry-run --no-dashboard` to inspect the new preview `limits` metadata and confirm your repo runtime config still resolves as expected.",
18
+ "If you relied on a local Claude wrapper only to inject `--effort`, move that setting into `wave.config.json` or the agent `### Executor` block and retire the wrapper when convenient.",
19
+ "If you document Codex turn ceilings in repo-local guidance, update that guidance to reflect that Wave now reports Codex ceiling visibility as opaque unless the limit is surfaced by the selected Codex profile or runtime."
20
+ ],
21
+ "breaking": false
22
+ },
23
+ {
24
+ "version": "0.6.1",
25
+ "date": "2026-03-22",
26
+ "summary": "Patch release that aligns the published package with merged main and refreshed 0.6.1 release docs.",
27
+ "features": [
28
+ "The package version and release metadata now advertise `0.6.1` consistently across `package.json`, the shipped README, the changelog, and the release manifest.",
29
+ "Current-surface docs such as runtime configuration, sample waves, eval guidance, migration notes, and npm publishing instructions now point at the `0.6.1` release instead of the earlier `0.6.0` tag.",
30
+ "This patch release carries forward the `0.6.0` runtime behavior, including the workspace-scoped tmux resource isolation fix that unblocked publish-time CI."
31
+ ],
32
+ "manualSteps": [
33
+ "After upgrading, rerun `pnpm exec wave doctor` and `pnpm exec wave launch --lane main --dry-run --no-dashboard` if you want to verify your repo against the same `0.6.1` package surface documented in this release.",
34
+ "If you pinned package docs or release references to `0.6.0`, update them to `0.6.1` so your operator guidance matches the published package tag."
35
+ ],
36
+ "breaking": false
37
+ },
5
38
  {
6
39
  "version": "0.6.0",
7
40
  "date": "2026-03-22",
@@ -183,7 +183,7 @@ function findLatestComponentMatches(text) {
183
183
  return Array.from(byComponent.values());
184
184
  }
185
185
 
186
- function detectTermination(logText, statusRecord) {
186
+ function detectTermination(agent, logText, statusRecord) {
187
187
  const patterns = [
188
188
  { reason: "max-turns", regex: /(Reached max turns \(\d+\))/i },
189
189
  { reason: "timeout", regex: /(timed out(?: after [^\n.]+)?)/i },
@@ -192,9 +192,16 @@ function detectTermination(logText, statusRecord) {
192
192
  for (const pattern of patterns) {
193
193
  const match = String(logText || "").match(pattern.regex);
194
194
  if (match) {
195
+ const baseHint = cleanText(match[1] || match[0]);
196
+ if (pattern.reason === "max-turns" && agent?.executorResolved?.id === "codex") {
197
+ return {
198
+ reason: pattern.reason,
199
+ hint: `${baseHint}. Wave does not set a Codex turn-limit flag; inspect launch-preview.json limits for any profile or upstream-runtime ceiling notes.`,
200
+ };
201
+ }
195
202
  return {
196
203
  reason: pattern.reason,
197
- hint: cleanText(match[1] || match[0]),
204
+ hint: baseHint,
198
205
  };
199
206
  }
200
207
  }
@@ -341,7 +348,7 @@ export function buildAgentExecutionSummary({ agent, statusRecord, logPath, repor
341
348
  const reportVerdict = parseVerdictFromText(reportText, REPORT_VERDICT_REGEX);
342
349
  const logVerdict = parseVerdictFromText(signalText, WAVE_VERDICT_REGEX);
343
350
  const verdict = reportVerdict.verdict ? reportVerdict : logVerdict;
344
- const termination = detectTermination(logText, statusRecord);
351
+ const termination = detectTermination(agent, logText, statusRecord);
345
352
  return {
346
353
  agentId: agent?.agentId || null,
347
354
  promptHash: statusRecord?.promptHash || null,
@@ -445,6 +445,19 @@ function normalizeClaudeOutputFormat(value, label = "executors.claude.outputForm
445
445
  return normalized;
446
446
  }
447
447
 
448
+ export function normalizeClaudeEffort(value, label = "executors.claude.effort", fallback = null) {
449
+ const normalized = String(value ?? "")
450
+ .trim()
451
+ .toLowerCase();
452
+ if (!normalized) {
453
+ return fallback;
454
+ }
455
+ if (!["low", "medium", "high", "max"].includes(normalized)) {
456
+ throw new Error(`${label} must be one of: low, medium, high, max`);
457
+ }
458
+ return normalized;
459
+ }
460
+
448
461
  function normalizeOpenCodeFormat(value, label = "executors.opencode.format") {
449
462
  const normalized = String(value || "default")
450
463
  .trim()
@@ -519,6 +532,11 @@ function normalizeExecutorProfile(rawProfile = {}, label = "executors.profiles.<
519
532
  rawProfile.claude.permissionPromptTool,
520
533
  null,
521
534
  ),
535
+ effort: normalizeClaudeEffort(
536
+ rawProfile.claude.effort,
537
+ `${label}.claude.effort`,
538
+ null,
539
+ ),
522
540
  maxTurns: normalizeOptionalPositiveInt(
523
541
  rawProfile.claude.maxTurns,
524
542
  `${label}.claude.maxTurns`,
@@ -685,6 +703,7 @@ function normalizeExecutors(rawExecutors = {}) {
685
703
  executors.claude?.permissionPromptTool,
686
704
  null,
687
705
  ),
706
+ effort: normalizeClaudeEffort(executors.claude?.effort, "executors.claude.effort", null),
688
707
  maxTurns: normalizeOptionalPositiveInt(executors.claude?.maxTurns, "executors.claude.maxTurns"),
689
708
  mcpConfig: normalizeOptionalStringOrStringArray(
690
709
  executors.claude?.mcpConfig,
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { analyzeMessageBoardCommunication } from "./coordination.mjs";
4
- import { commsAgeSummary, deploymentSummary, renderCountsByState } from "./dashboard-state.mjs";
4
+ import { commsAgeSummary, deploymentSummary } from "./dashboard-state.mjs";
5
5
  import {
6
6
  DEFAULT_REFRESH_MS,
7
7
  DEFAULT_WAVE_LANE,
@@ -81,20 +81,125 @@ function isGlobalDashboardState(state) {
81
81
  return Boolean(state && Array.isArray(state.waves) && !Array.isArray(state.agents));
82
82
  }
83
83
 
84
- function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
84
+ const ANSI = {
85
+ reset: "\u001b[0m",
86
+ dim: "\u001b[2m",
87
+ red: "\u001b[31m",
88
+ green: "\u001b[32m",
89
+ yellow: "\u001b[33m",
90
+ blue: "\u001b[34m",
91
+ magenta: "\u001b[35m",
92
+ cyan: "\u001b[36m",
93
+ };
94
+
95
+ function paint(text, color, colorize = false) {
96
+ if (!colorize || !color) {
97
+ return text;
98
+ }
99
+ return `${color}${text}${ANSI.reset}`;
100
+ }
101
+
102
+ function paintState(text, state, colorize = false) {
103
+ const normalized = String(state || "").trim().toLowerCase();
104
+ const color =
105
+ normalized === "completed"
106
+ ? ANSI.green
107
+ : normalized === "failed" || normalized === "timed_out"
108
+ ? ANSI.red
109
+ : normalized === "deploying" || normalized === "finalizing" || normalized === "validating"
110
+ ? ANSI.yellow
111
+ : normalized === "running" || normalized === "coding" || normalized === "launching"
112
+ ? ANSI.cyan
113
+ : normalized === "pending"
114
+ ? ANSI.dim
115
+ : normalized === "dry-run"
116
+ ? ANSI.magenta
117
+ : null;
118
+ return paint(text, color, colorize);
119
+ }
120
+
121
+ function paintLevel(text, level, colorize = false) {
122
+ const normalized = String(level || "").trim().toLowerCase();
123
+ const color =
124
+ normalized === "error"
125
+ ? ANSI.red
126
+ : normalized === "warn"
127
+ ? ANSI.yellow
128
+ : normalized === "info"
129
+ ? ANSI.blue
130
+ : null;
131
+ return paint(text, color, colorize);
132
+ }
133
+
134
+ function paintExitCode(text, exitCode, colorize = false) {
135
+ if (exitCode === 0 || exitCode === "0") {
136
+ return paint(text, ANSI.green, colorize);
137
+ }
138
+ if (exitCode === null || exitCode === undefined || exitCode === "-") {
139
+ return text;
140
+ }
141
+ return paint(text, ANSI.red, colorize);
142
+ }
143
+
144
+ function paintDeploymentSummary(text, deploymentState, colorize = false) {
145
+ const normalized = String(deploymentState || "").trim().toLowerCase();
146
+ const color =
147
+ normalized === "healthy"
148
+ ? ANSI.green
149
+ : normalized === "failed"
150
+ ? ANSI.red
151
+ : normalized === "deploying" || normalized === "rolledover"
152
+ ? ANSI.yellow
153
+ : null;
154
+ return paint(text, color, colorize);
155
+ }
156
+
157
+ function renderColoredCountsByState(agents, colorize = false) {
158
+ const counts = new Map();
159
+ for (const agent of agents || []) {
160
+ const key = agent?.state || "unknown";
161
+ counts.set(key, (counts.get(key) || 0) + 1);
162
+ }
163
+ return Array.from(counts.entries())
164
+ .toSorted((a, b) => a[0].localeCompare(b[0]))
165
+ .map(([state, count]) => `${paintState(state, state, colorize)}:${count}`)
166
+ .join(" ");
167
+ }
168
+
169
+ function paintWaveAgentSummary(summary, wave, colorize = false) {
170
+ const failed = Number(wave?.agentsFailed ?? 0) || 0;
171
+ const active = Number(wave?.agentsActive ?? 0) || 0;
172
+ const total = Number(wave?.agentsTotal ?? 0) || 0;
173
+ const completed = Number(wave?.agentsCompleted ?? 0) || 0;
174
+ const color =
175
+ failed > 0
176
+ ? ANSI.red
177
+ : active > 0
178
+ ? ANSI.cyan
179
+ : total > 0 && completed >= total
180
+ ? ANSI.green
181
+ : ANSI.dim;
182
+ return paint(summary, color, colorize);
183
+ }
184
+
185
+ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane, colorize = false }) {
85
186
  if (!state) {
86
187
  return `Dashboard file not found or invalid: ${dashboardPath}`;
87
188
  }
88
189
  const lines = [];
89
190
  const laneName = String(state?.lane || lane || "").trim() || DEFAULT_WAVE_LANE;
90
191
  lines.push(
91
- `${laneName} Wave Dashboard | wave=${state.wave ?? "?"} | status=${state.status ?? "unknown"} | attempt=${state.attempt ?? "?"}/${state.maxAttempts ?? "?"}`,
192
+ `${laneName} Wave Dashboard | wave=${state.wave ?? "?"} | status=${paintState(
193
+ state.status ?? "unknown",
194
+ state.status,
195
+ colorize,
196
+ )} | attempt=${state.attempt ?? "?"}/${state.maxAttempts ?? "?"}`,
92
197
  );
93
198
  lines.push(
94
199
  `Updated ${formatAgeFromTimestamp(Date.parse(state.updatedAt || ""))} | Started ${state.startedAt || "n/a"} | Elapsed ${formatElapsed(state.startedAt)}`,
95
200
  );
96
201
  lines.push(`Run tag: ${state.runTag || "n/a"} | Wave file: ${state.waveFile || "n/a"}`);
97
- lines.push(`Counts: ${renderCountsByState(state.agents || []) || "none"}`);
202
+ lines.push(`Counts: ${renderColoredCountsByState(state.agents || [], colorize) || "none"}`);
98
203
  const comms = analyzeMessageBoardCommunication(messageBoardPath);
99
204
  if (!comms.available) {
100
205
  lines.push(`Comms: unavailable ${comms.reason || ""}`.trim());
@@ -116,10 +221,19 @@ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
116
221
  );
117
222
  for (const agent of state.agents || []) {
118
223
  lines.push(
119
- `${pad(agent.agentId || "-", 8)} ${pad(agent.state || "-", 12)} ${pad(agent.attempts ?? 0, 8)} ${pad(
120
- agent.exitCode ?? "-",
121
- 6,
122
- )} ${pad(deploymentSummary({ service: agent.deploymentService, state: agent.deploymentState }), 24)} ${pad(
224
+ `${pad(agent.agentId || "-", 8)} ${paintState(
225
+ pad(agent.state || "-", 12),
226
+ agent.state,
227
+ colorize,
228
+ )} ${pad(agent.attempts ?? 0, 8)} ${paintExitCode(
229
+ pad(agent.exitCode ?? "-", 6),
230
+ agent.exitCode,
231
+ colorize,
232
+ )} ${paintDeploymentSummary(
233
+ pad(deploymentSummary({ service: agent.deploymentService, state: agent.deploymentState }), 24),
234
+ agent.deploymentState,
235
+ colorize,
236
+ )} ${pad(
123
237
  formatAgeFromTimestamp(Date.parse(agent.lastUpdateAt || "")),
124
238
  12,
125
239
  )} ${truncate(agent.detail || "", 72)}`,
@@ -130,7 +244,7 @@ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
130
244
  for (const event of (state.events || []).slice(-12)) {
131
245
  const prefix = event.agentId ? `[${event.agentId}]` : "[wave]";
132
246
  lines.push(
133
- `${event.at || "n/a"} ${pad(event.level || "info", 5)} ${prefix} ${event.message || ""}`,
247
+ `${event.at || "n/a"} ${paintLevel(pad(event.level || "info", 5), event.level, colorize)} ${prefix} ${event.message || ""}`,
134
248
  );
135
249
  }
136
250
  if ((state.events || []).length === 0) {
@@ -143,14 +257,29 @@ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
143
257
  return lines.join("\n");
144
258
  }
145
259
 
146
- function renderGlobalDashboard({ state, dashboardPath, lane }) {
260
+ function renderGlobalDashboard({ state, dashboardPath, lane, colorize = false }) {
147
261
  if (!state) {
148
262
  return `Dashboard file not found or invalid: ${dashboardPath}`;
149
263
  }
264
+ const formatGlobalWaveAgentSummary = (wave) => {
265
+ const total = Number(wave?.agentsTotal ?? 0) || 0;
266
+ const completed = Number(wave?.agentsCompleted ?? 0) || 0;
267
+ const failed = Number(wave?.agentsFailed ?? 0) || 0;
268
+ const active = Number(wave?.agentsActive ?? 0) || 0;
269
+ const pending =
270
+ wave?.agentsPending === undefined || wave?.agentsPending === null
271
+ ? Math.max(0, total - completed - failed - active)
272
+ : Number(wave.agentsPending ?? 0) || 0;
273
+ return `done ${completed}/${total} active ${active} pending ${pending} fail ${failed}`;
274
+ };
150
275
  const lines = [];
151
276
  const laneName = String(state?.lane || lane || "").trim() || DEFAULT_WAVE_LANE;
152
277
  lines.push(
153
- `${laneName} Wave Global Dashboard | run=${state.runId || "n/a"} | status=${state.status || "unknown"}`,
278
+ `${laneName} Wave Global Dashboard | run=${state.runId || "n/a"} | status=${paintState(
279
+ state.status || "unknown",
280
+ state.status,
281
+ colorize,
282
+ )}`,
154
283
  );
155
284
  lines.push(
156
285
  `Updated ${formatAgeFromTimestamp(Date.parse(state.updatedAt || ""))} | Started ${state.startedAt || "n/a"} | Elapsed ${formatElapsed(state.startedAt)}`,
@@ -161,18 +290,18 @@ function renderGlobalDashboard({ state, dashboardPath, lane }) {
161
290
  lines.push("");
162
291
  lines.push("Waves:");
163
292
  lines.push(
164
- `${pad("Wave", 6)} ${pad("Status", 10)} ${pad("Attempt", 9)} ${pad("Agents", 16)} ${pad("Started", 12)} ${pad("Last Message", 70)}`,
293
+ `${pad("Wave", 6)} ${pad("Status", 10)} ${pad("Attempt", 9)} ${pad("Agents", 36)} ${pad("Started", 12)} ${pad("Last Message", 70)}`,
165
294
  );
166
295
  lines.push(
167
- `${"-".repeat(6)} ${"-".repeat(10)} ${"-".repeat(9)} ${"-".repeat(16)} ${"-".repeat(12)} ${"-".repeat(70)}`,
296
+ `${"-".repeat(6)} ${"-".repeat(10)} ${"-".repeat(9)} ${"-".repeat(36)} ${"-".repeat(12)} ${"-".repeat(70)}`,
168
297
  );
169
298
  for (const wave of state.waves || []) {
170
- const agents = `${wave.agentsCompleted ?? 0}/${wave.agentsTotal ?? 0} ok, ${wave.agentsFailed ?? 0} fail`;
299
+ const agents = formatGlobalWaveAgentSummary(wave);
171
300
  lines.push(
172
- `${pad(wave.wave ?? "-", 6)} ${pad(wave.status || "-", 10)} ${pad(
301
+ `${pad(wave.wave ?? "-", 6)} ${paintState(pad(wave.status || "-", 10), wave.status, colorize)} ${pad(
173
302
  `${wave.attempt ?? 0}/${wave.maxAttempts ?? 0}`,
174
303
  9,
175
- )} ${pad(agents, 16)} ${pad(
304
+ )} ${paintWaveAgentSummary(pad(agents, 36), wave, colorize)} ${pad(
176
305
  formatAgeFromTimestamp(Date.parse(wave.startedAt || "")),
177
306
  12,
178
307
  )} ${truncate(wave.lastMessage || "", 70)}`,
@@ -191,7 +320,7 @@ function renderGlobalDashboard({ state, dashboardPath, lane }) {
191
320
  for (const event of (state.events || []).slice(-16)) {
192
321
  const waveTag = event.wave ? `wave:${event.wave}` : "wave:-";
193
322
  lines.push(
194
- `${event.at || "n/a"} ${pad(event.level || "info", 5)} [${waveTag}] ${event.message || ""}`,
323
+ `${event.at || "n/a"} ${paintLevel(pad(event.level || "info", 5), event.level, colorize)} [${waveTag}] ${event.message || ""}`,
195
324
  );
196
325
  }
197
326
  if ((state.events || []).length === 0) {
@@ -200,10 +329,10 @@ function renderGlobalDashboard({ state, dashboardPath, lane }) {
200
329
  return lines.join("\n");
201
330
  }
202
331
 
203
- export function renderDashboard({ state, dashboardPath, messageBoardPath, lane }) {
332
+ export function renderDashboard({ state, dashboardPath, messageBoardPath, lane, colorize = false }) {
204
333
  return isGlobalDashboardState(state)
205
- ? renderGlobalDashboard({ state, dashboardPath, lane })
206
- : renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane });
334
+ ? renderGlobalDashboard({ state, dashboardPath, lane, colorize })
335
+ : renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane, colorize });
207
336
  }
208
337
 
209
338
  export async function runDashboardCli(argv) {
@@ -232,6 +361,7 @@ Options:
232
361
  dashboardPath: options.dashboardFile,
233
362
  messageBoardPath: boardPath,
234
363
  lane: options.lane,
364
+ colorize: process.stdout.isTTY,
235
365
  });
236
366
  if (process.stdout.isTTY) {
237
367
  process.stdout.write("\u001bc");
@@ -228,8 +228,10 @@ export function buildGlobalDashboardState({
228
228
  startedAt: null,
229
229
  completedAt: null,
230
230
  agentsTotal: wave.agents.length,
231
+ agentsActive: 0,
231
232
  agentsCompleted: 0,
232
233
  agentsFailed: 0,
234
+ agentsPending: wave.agents.length,
233
235
  helperAssignmentsOpen: 0,
234
236
  inboundDependenciesOpen: 0,
235
237
  outboundDependenciesOpen: 0,
@@ -306,10 +308,16 @@ export function syncGlobalWaveFromWaveDashboard(globalState, waveDashboard) {
306
308
  }
307
309
  const agents = Array.isArray(waveDashboard.agents) ? waveDashboard.agents : [];
308
310
  entry.agentsTotal = agents.length;
311
+ entry.agentsActive = agents.filter((agent) =>
312
+ ["launching", "running", "coding", "validating", "deploying", "finalizing"].includes(
313
+ agent.state,
314
+ ),
315
+ ).length;
309
316
  entry.agentsCompleted = agents.filter((agent) => agent.state === "completed").length;
310
317
  entry.agentsFailed = agents.filter(
311
318
  (agent) => agent.state === "failed" || agent.state === "timed_out",
312
319
  ).length;
320
+ entry.agentsPending = agents.filter((agent) => agent.state === "pending").length;
313
321
  const latestEvent = waveDashboard.events.at(-1);
314
322
  entry.lastMessage = latestEvent?.message || entry.lastMessage || "";
315
323
  entry.helperAssignmentsOpen = waveDashboard.helperAssignmentsOpen || 0;
@@ -217,6 +217,7 @@ function buildClaudeLaunchSpec({ agent, promptPath, logPath, overlayDir }) {
217
217
  appendSingleValueFlag(tokens, "--agent", executor.claude.agent);
218
218
  appendSingleValueFlag(tokens, "--permission-mode", executor.claude.permissionMode);
219
219
  appendSingleValueFlag(tokens, "--permission-prompt-tool", executor.claude.permissionPromptTool);
220
+ appendSingleValueFlag(tokens, "--effort", executor.claude.effort);
220
221
  appendSingleValueFlag(tokens, "--max-turns", executor.claude.maxTurns);
221
222
  appendRepeatedFlag(tokens, "--mcp-config", executor.claude.mcpConfig);
222
223
  appendSingleValueFlag(tokens, "--settings", settingsPath);
@@ -299,6 +300,55 @@ function buildLocalLaunchSpec({ promptPath, logPath }) {
299
300
  };
300
301
  }
301
302
 
303
+ function buildLaunchLimitsMetadata(agent) {
304
+ const executor = agent?.executorResolved || {};
305
+ const executorId = normalizeExecutorMode(executor.id || DEFAULT_EXECUTOR_MODE);
306
+ const attemptTimeoutMinutes = executor?.budget?.minutes ?? null;
307
+ if (executorId === "claude") {
308
+ const source = executor?.claude?.maxTurnsSource || null;
309
+ return {
310
+ attemptTimeoutMinutes,
311
+ knownTurnLimit: executor?.claude?.maxTurns ?? null,
312
+ turnLimitSource: source,
313
+ notes:
314
+ source === "budget.turns"
315
+ ? ["Known turn limit was derived from generic budget.turns."]
316
+ : [],
317
+ };
318
+ }
319
+ if (executorId === "opencode") {
320
+ const source = executor?.opencode?.stepsSource || null;
321
+ return {
322
+ attemptTimeoutMinutes,
323
+ knownTurnLimit: executor?.opencode?.steps ?? null,
324
+ turnLimitSource: source,
325
+ notes:
326
+ source === "budget.turns"
327
+ ? ["Known turn limit was derived from generic budget.turns."]
328
+ : [],
329
+ };
330
+ }
331
+ if (executorId === "codex") {
332
+ const profileNote = executor?.codex?.profileName
333
+ ? ` via Codex profile ${executor.codex.profileName}`
334
+ : "";
335
+ return {
336
+ attemptTimeoutMinutes,
337
+ knownTurnLimit: null,
338
+ turnLimitSource: "not-set-by-wave",
339
+ notes: [
340
+ `Wave emits no Codex turn-limit flag; any effective ceiling may come from the selected Codex profile${profileNote} or the upstream Codex runtime.`,
341
+ ],
342
+ };
343
+ }
344
+ return {
345
+ attemptTimeoutMinutes,
346
+ knownTurnLimit: null,
347
+ turnLimitSource: "not-applicable",
348
+ notes: ["Local executor does not use model turn limits."],
349
+ };
350
+ }
351
+
302
352
  function buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection }) {
303
353
  const executor = agent.executorResolved;
304
354
  return {
@@ -329,16 +379,29 @@ function buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection }) {
329
379
  export function buildExecutorLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }) {
330
380
  const executorId = normalizeExecutorMode(agent?.executorResolved?.id || DEFAULT_EXECUTOR_MODE);
331
381
  ensureDirectory(overlayDir);
382
+ const limits = buildLaunchLimitsMetadata(agent);
332
383
  if (executorId === "local") {
333
- return buildLocalLaunchSpec({ promptPath, logPath });
384
+ return {
385
+ ...buildLocalLaunchSpec({ promptPath, logPath }),
386
+ limits,
387
+ };
334
388
  }
335
389
  if (executorId === "claude") {
336
- return buildClaudeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection });
390
+ return {
391
+ ...buildClaudeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }),
392
+ limits,
393
+ };
337
394
  }
338
395
  if (executorId === "opencode") {
339
- return buildOpenCodeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection });
396
+ return {
397
+ ...buildOpenCodeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }),
398
+ limits,
399
+ };
340
400
  }
341
- return buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection });
401
+ return {
402
+ ...buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection }),
403
+ limits,
404
+ };
342
405
  }
343
406
 
344
407
  export function commandForExecutor(executor, executorId = executor?.id) {
@@ -139,6 +139,7 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
139
139
  env: launchSpec.env || {},
140
140
  useRateLimitRetries: launchSpec.useRateLimitRetries === true,
141
141
  invocationLines: launchSpec.invocationLines,
142
+ limits: launchSpec.limits || null,
142
143
  skills: summarizeResolvedSkills(agent.skillsResolved),
143
144
  });
144
145
  return {