@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.
- package/CHANGELOG.md +15 -0
- package/README.md +75 -30
- package/docs/README.md +15 -3
- package/docs/concepts/context7-vs-skills.md +24 -0
- package/docs/concepts/runtime-agnostic-orchestration.md +17 -2
- package/docs/concepts/what-is-a-wave.md +28 -0
- package/docs/evals/README.md +4 -2
- package/docs/guides/terminal-surfaces.md +2 -0
- package/docs/plans/current-state.md +1 -1
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +4 -4
- package/docs/plans/wave-orchestrator.md +11 -3
- package/docs/reference/npmjs-trusted-publishing.md +2 -2
- package/docs/reference/runtime-config/README.md +4 -4
- package/docs/reference/runtime-config/claude.md +6 -1
- package/docs/reference/runtime-config/codex.md +2 -2
- package/docs/reference/runtime-config/opencode.md +1 -1
- package/docs/reference/sample-waves.md +4 -4
- package/docs/research/agent-context-sources.md +2 -0
- package/docs/research/coordination-failure-review.md +37 -13
- package/package.json +1 -1
- package/releases/manifest.json +33 -0
- package/scripts/wave-orchestrator/agent-state.mjs +10 -3
- package/scripts/wave-orchestrator/config.mjs +19 -0
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +150 -20
- package/scripts/wave-orchestrator/dashboard-state.mjs +8 -0
- package/scripts/wave-orchestrator/executors.mjs +67 -4
- package/scripts/wave-orchestrator/launcher-runtime.mjs +1 -0
- package/scripts/wave-orchestrator/launcher.mjs +245 -10
- package/scripts/wave-orchestrator/terminals.mjs +25 -0
- 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
|
|
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.
|
|
199
|
+
### 1. The benchmark vocabulary exists, but the empirical proof is still thin
|
|
179
200
|
|
|
180
|
-
|
|
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
|
-
- `
|
|
183
|
-
- `
|
|
184
|
-
- `
|
|
203
|
+
- `hidden-profile-pooling`
|
|
204
|
+
- `silo-escape`
|
|
205
|
+
- `simultaneous-coordination`
|
|
206
|
+
- `expertise-leverage`
|
|
207
|
+
- `blackboard-fidelity`
|
|
208
|
+
- `contradiction-recovery`
|
|
185
209
|
|
|
186
|
-
|
|
210
|
+
That is a real improvement because the repo now has a vocabulary for the exact MAS failures the research highlights.
|
|
187
211
|
|
|
188
|
-
|
|
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
|
-
|
|
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
package/releases/manifest.json
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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=${
|
|
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: ${
|
|
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)} ${
|
|
120
|
-
agent.
|
|
121
|
-
|
|
122
|
-
|
|
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=${
|
|
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",
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
384
|
+
return {
|
|
385
|
+
...buildLocalLaunchSpec({ promptPath, logPath }),
|
|
386
|
+
limits,
|
|
387
|
+
};
|
|
334
388
|
}
|
|
335
389
|
if (executorId === "claude") {
|
|
336
|
-
return
|
|
390
|
+
return {
|
|
391
|
+
...buildClaudeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }),
|
|
392
|
+
limits,
|
|
393
|
+
};
|
|
337
394
|
}
|
|
338
395
|
if (executorId === "opencode") {
|
|
339
|
-
return
|
|
396
|
+
return {
|
|
397
|
+
...buildOpenCodeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }),
|
|
398
|
+
limits,
|
|
399
|
+
};
|
|
340
400
|
}
|
|
341
|
-
return
|
|
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 {
|