@chllming/wave-orchestration 0.7.0 → 0.7.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.
- package/CHANGELOG.md +25 -0
- package/README.md +9 -8
- package/docs/guides/planner.md +19 -0
- package/docs/guides/terminal-surfaces.md +12 -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 +26 -0
- package/docs/plans/wave-orchestrator.md +4 -7
- package/docs/reference/cli-reference.md +547 -0
- package/docs/reference/coordination-and-closure.md +1 -1
- package/docs/reference/npmjs-trusted-publishing.md +2 -2
- package/docs/reference/runtime-config/README.md +2 -2
- package/docs/reference/runtime-config/codex.md +2 -1
- package/docs/reference/sample-waves.md +4 -4
- package/package.json +1 -1
- package/releases/manifest.json +24 -2
- package/scripts/wave-orchestrator/agent-state.mjs +11 -2
- package/scripts/wave-orchestrator/control-cli.mjs +112 -19
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +82 -2
- package/scripts/wave-orchestrator/install.mjs +98 -3
- package/scripts/wave-orchestrator/launcher-runtime.mjs +9 -9
- package/scripts/wave-orchestrator/launcher.mjs +88 -1
- package/scripts/wave-orchestrator/terminals.mjs +1 -1
- package/scripts/wave-orchestrator/wave-files.mjs +127 -18
package/releases/manifest.json
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "@chllming/wave-orchestration",
|
|
4
4
|
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.7.1",
|
|
7
|
+
"date": "2026-03-23",
|
|
8
|
+
"summary": "Run-control hardening, completed-with-drift reconcile preservation, live Codex ceiling visibility, and 0.7.1 release-surface alignment.",
|
|
9
|
+
"features": [
|
|
10
|
+
"Fresh live launches now clear stale auto-generated relaunch plans by default, so explicit wave starts recompute the implementation fan-out unless `--resume-control-state` is passed.",
|
|
11
|
+
"`wave control status` now treats the active attempt as the authoritative live fan-out instead of replaying stale rerun intent or unrelated closure blockers.",
|
|
12
|
+
"Historical `reconcile-status` now preserves previously authoritative completed waves as `completed_with_drift` when the only mismatch is prompt-hash drift.",
|
|
13
|
+
"Live executor overlays now always write `launch-preview.json`, and Codex summaries record an observed turn ceiling when the runtime reports one.",
|
|
14
|
+
"Shipped package docs, migration guidance, sample-wave references, and npm publishing instructions now point at the `0.7.1` release surface."
|
|
15
|
+
],
|
|
16
|
+
"manualSteps": [
|
|
17
|
+
"If you intentionally want to reuse a prior auto-generated relaunch selection on a fresh live start, pass `--resume-control-state` explicitly.",
|
|
18
|
+
"Use `pnpm exec wave dashboard --lane <lane> --attach current` or `--attach global` to reattach to live tmux-backed dashboards without resolving sockets or session names by hand.",
|
|
19
|
+
"If an adopted `0.6.x` repo fails `wave doctor` after the `0.7.x` upgrade, sync the repo-owned planner starter surface (`docs/agents/wave-planner-role.md`, `skills/role-planner/`, `docs/context7/planner-agent/`, `docs/reference/wave-planning-lessons.md`, and the `planner-agentic` bundle entry) before relying on planner-aware validation."
|
|
20
|
+
],
|
|
21
|
+
"breaking": false
|
|
22
|
+
},
|
|
5
23
|
{
|
|
6
24
|
"version": "0.7.0",
|
|
7
25
|
"date": "2026-03-23",
|
|
@@ -12,11 +30,15 @@
|
|
|
12
30
|
"Wave Control telemetry: local-first event queueing with best-effort batch delivery, configurable report modes, selective artifact upload, and per-category capture toggles.",
|
|
13
31
|
"Live-wave orchestration refresh that keeps coordination surfaces, clarification triage, and dashboard metrics current during active execution.",
|
|
14
32
|
"Resident orchestrator support via `--resident-orchestrator` for long-running non-owning monitoring sessions.",
|
|
15
|
-
"Native and external benchmark telemetry with failure-review validity classification and config attestation hashing."
|
|
33
|
+
"Native and external benchmark telemetry with failure-review validity classification and config attestation hashing.",
|
|
34
|
+
"Stable dashboard reattach via `wave dashboard --attach current|global`, plus live `launch-preview.json` artifacts that preserve observed Codex turn ceilings without pretending Wave set them.",
|
|
35
|
+
"Historical `reconcile-status` now preserves previously authoritative completed waves as completed-with-drift when the only mismatch is prompt-hash drift.",
|
|
36
|
+
"Fresh live launches now clear stale auto-generated relaunch plans by default, while `wave control status` treats the active attempt as the authoritative fan-out instead of replaying stale relaunch state."
|
|
16
37
|
],
|
|
17
38
|
"manualSteps": [
|
|
18
39
|
"Existing `wave coord`, `wave retry`, and `wave proof` commands remain available as compatibility surfaces. No migration required, but new operator docs prefer `wave control`.",
|
|
19
|
-
"To enable Wave Control telemetry, add a `waveControl` section to `wave.config.json` with at minimum an `endpoint` and `workspaceId`. Pass `--no-telemetry` to disable for a single run."
|
|
40
|
+
"To enable Wave Control telemetry, add a `waveControl` section to `wave.config.json` with at minimum an `endpoint` and `workspaceId`. Pass `--no-telemetry` to disable for a single run.",
|
|
41
|
+
"If an adopted `0.6.x` repo fails `wave doctor` after the `0.7.x` upgrade, sync the repo-owned planner starter surface (`docs/agents/wave-planner-role.md`, `skills/role-planner/`, `docs/context7/planner-agent/`, `docs/reference/wave-planning-lessons.md`, and the `planner-agentic` bundle entry) before relying on planner-aware validation."
|
|
20
42
|
],
|
|
21
43
|
"breaking": false
|
|
22
44
|
},
|
|
@@ -185,23 +185,27 @@ function findLatestComponentMatches(text) {
|
|
|
185
185
|
|
|
186
186
|
function detectTermination(agent, logText, statusRecord) {
|
|
187
187
|
const patterns = [
|
|
188
|
-
{ reason: "max-turns", regex: /
|
|
188
|
+
{ reason: "max-turns", regex: /Reached max turns \((\d+)\)/i },
|
|
189
189
|
{ reason: "timeout", regex: /(timed out(?: after [^\n.]+)?)/i },
|
|
190
190
|
{ reason: "session-missing", regex: /(session [^\n]+ disappeared before [^\n]+ was written)/i },
|
|
191
191
|
];
|
|
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[
|
|
195
|
+
const baseHint = cleanText(match[0]);
|
|
196
|
+
const observedTurnLimit =
|
|
197
|
+
pattern.reason === "max-turns" && Number.isFinite(Number(match[1])) ? Number(match[1]) : null;
|
|
196
198
|
if (pattern.reason === "max-turns" && agent?.executorResolved?.id === "codex") {
|
|
197
199
|
return {
|
|
198
200
|
reason: pattern.reason,
|
|
199
201
|
hint: `${baseHint}. Wave does not set a Codex turn-limit flag; inspect launch-preview.json limits for any profile or upstream-runtime ceiling notes.`,
|
|
202
|
+
observedTurnLimit,
|
|
200
203
|
};
|
|
201
204
|
}
|
|
202
205
|
return {
|
|
203
206
|
reason: pattern.reason,
|
|
204
207
|
hint: baseHint,
|
|
208
|
+
observedTurnLimit,
|
|
205
209
|
};
|
|
206
210
|
}
|
|
207
211
|
}
|
|
@@ -212,6 +216,7 @@ function detectTermination(agent, logText, statusRecord) {
|
|
|
212
216
|
return {
|
|
213
217
|
reason: "status-detail",
|
|
214
218
|
hint: statusHint,
|
|
219
|
+
observedTurnLimit: null,
|
|
215
220
|
};
|
|
216
221
|
}
|
|
217
222
|
const exitCode = Number.isFinite(Number(statusRecord?.code)) ? Number(statusRecord.code) : null;
|
|
@@ -219,11 +224,13 @@ function detectTermination(agent, logText, statusRecord) {
|
|
|
219
224
|
return {
|
|
220
225
|
reason: "exit-code",
|
|
221
226
|
hint: `Exit code ${exitCode}.`,
|
|
227
|
+
observedTurnLimit: null,
|
|
222
228
|
};
|
|
223
229
|
}
|
|
224
230
|
return {
|
|
225
231
|
reason: null,
|
|
226
232
|
hint: "",
|
|
233
|
+
observedTurnLimit: null,
|
|
227
234
|
};
|
|
228
235
|
}
|
|
229
236
|
|
|
@@ -437,6 +444,8 @@ export function buildAgentExecutionSummary({ agent, statusRecord, logPath, repor
|
|
|
437
444
|
: null,
|
|
438
445
|
terminationReason: termination.reason,
|
|
439
446
|
terminationHint: termination.hint,
|
|
447
|
+
terminationObservedTurnLimit:
|
|
448
|
+
Number.isFinite(Number(termination.observedTurnLimit)) ? Number(termination.observedTurnLimit) : null,
|
|
440
449
|
logPath: path.relative(REPO_ROOT, logPath),
|
|
441
450
|
reportPath: reportPath ? path.relative(REPO_ROOT, reportPath) : null,
|
|
442
451
|
};
|
|
@@ -270,8 +270,46 @@ function assignmentRelevantToAgent(assignment, agentId = "") {
|
|
|
270
270
|
);
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
function
|
|
274
|
-
const
|
|
273
|
+
function buildEffectiveSelection(lanePaths, wave, { activeAttempt = null, rerunRequest = null, relaunchPlan = null } = {}) {
|
|
274
|
+
const activeAttemptSelected = Array.isArray(activeAttempt?.selectedAgentIds)
|
|
275
|
+
? Array.from(new Set(activeAttempt.selectedAgentIds.filter(Boolean)))
|
|
276
|
+
: [];
|
|
277
|
+
if (activeAttemptSelected.length > 0) {
|
|
278
|
+
return {
|
|
279
|
+
source: "active-attempt",
|
|
280
|
+
selectedAgentIds: activeAttemptSelected,
|
|
281
|
+
detail: activeAttempt?.detail || null,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const rerunSelected = rerunRequest?.selectedAgentIds?.length
|
|
285
|
+
? rerunRequest.selectedAgentIds
|
|
286
|
+
: resolveRetryOverrideAgentIds(wave, lanePaths, rerunRequest);
|
|
287
|
+
if (rerunSelected.length > 0) {
|
|
288
|
+
return {
|
|
289
|
+
source: "rerun-request",
|
|
290
|
+
selectedAgentIds: rerunSelected,
|
|
291
|
+
detail: rerunRequest?.reason || null,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const relaunchSelected = Array.isArray(relaunchPlan?.selectedAgentIds)
|
|
295
|
+
? Array.from(new Set(relaunchPlan.selectedAgentIds.filter(Boolean)))
|
|
296
|
+
: [];
|
|
297
|
+
if (relaunchSelected.length > 0) {
|
|
298
|
+
return {
|
|
299
|
+
source: "relaunch-plan",
|
|
300
|
+
selectedAgentIds: relaunchSelected,
|
|
301
|
+
detail: null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
source: "none",
|
|
306
|
+
selectedAgentIds: [],
|
|
307
|
+
detail: null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabilityAssignments, selection, proofRegistry }) {
|
|
312
|
+
const selectedAgentIds = new Set(selection?.selectedAgentIds || []);
|
|
275
313
|
const helperAssignments = Array.isArray(capabilityAssignments) ? capabilityAssignments : [];
|
|
276
314
|
const openInbound = dependencySnapshot?.openInbound || [];
|
|
277
315
|
return wave.agents.map((agent) => {
|
|
@@ -301,9 +339,15 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
|
|
|
301
339
|
const dependency = openInbound.find((record) => record.assignedAgentId === agent.agentId);
|
|
302
340
|
let state = "planned";
|
|
303
341
|
let reason = "";
|
|
304
|
-
if (
|
|
342
|
+
if (selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId)) {
|
|
343
|
+
state = "working";
|
|
344
|
+
reason = selection?.detail || "Selected by the active launcher attempt.";
|
|
345
|
+
} else if (selectedAgentIds.has(agent.agentId)) {
|
|
305
346
|
state = "needs-rerun";
|
|
306
|
-
reason =
|
|
347
|
+
reason =
|
|
348
|
+
selection?.source === "relaunch-plan"
|
|
349
|
+
? "Selected by the persisted relaunch plan."
|
|
350
|
+
: "Selected by active rerun request.";
|
|
307
351
|
} else if (targetedBlockingTasks.some((task) => task.state === "working")) {
|
|
308
352
|
state = "working";
|
|
309
353
|
reason = targetedBlockingTasks.find((task) => task.state === "working")?.title || "";
|
|
@@ -334,7 +378,8 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
|
|
|
334
378
|
state,
|
|
335
379
|
reason: reason || null,
|
|
336
380
|
taskIds: targetedTasks.map((task) => task.taskId),
|
|
337
|
-
selectedForRerun:
|
|
381
|
+
selectedForRerun: selectedAgentIds.has(agent.agentId) && selection?.source !== "active-attempt",
|
|
382
|
+
selectedForActiveAttempt: selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId),
|
|
338
383
|
activeProofBundleIds: (proofRegistry?.entries || [])
|
|
339
384
|
.filter(
|
|
340
385
|
(entry) =>
|
|
@@ -346,10 +391,25 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
|
|
|
346
391
|
});
|
|
347
392
|
}
|
|
348
393
|
|
|
349
|
-
function
|
|
350
|
-
|
|
394
|
+
function selectionTargetsAgent(agentId, selectionSet) {
|
|
395
|
+
return Boolean(agentId) && selectionSet.has(agentId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, activeAttempt, rerunRequest, relaunchPlan, agentId = "" }) {
|
|
399
|
+
const attemptSelection = new Set(activeAttempt?.selectedAgentIds || []);
|
|
400
|
+
const scopeToActiveAttempt = !agentId && attemptSelection.size > 0;
|
|
401
|
+
const scopedTasks = (agentId
|
|
351
402
|
? tasks.filter((task) => task.ownerAgentId === agentId || task.assigneeAgentId === agentId)
|
|
352
|
-
: tasks
|
|
403
|
+
: tasks
|
|
404
|
+
).filter((task) => {
|
|
405
|
+
if (!scopeToActiveAttempt) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return (
|
|
409
|
+
selectionTargetsAgent(task.ownerAgentId, attemptSelection) ||
|
|
410
|
+
selectionTargetsAgent(task.assigneeAgentId, attemptSelection)
|
|
411
|
+
);
|
|
412
|
+
});
|
|
353
413
|
const pendingHuman = scopedTasks.find((task) => task.state === "input-required");
|
|
354
414
|
if (pendingHuman) {
|
|
355
415
|
return {
|
|
@@ -381,11 +441,19 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
381
441
|
detail: clarification.title,
|
|
382
442
|
};
|
|
383
443
|
}
|
|
384
|
-
const
|
|
444
|
+
const scopedAssignments = (capabilityAssignments || []).filter((assignment) => {
|
|
445
|
+
if (!scopeToActiveAttempt) {
|
|
446
|
+
return assignmentRelevantToAgent(assignment, agentId);
|
|
447
|
+
}
|
|
448
|
+
return (
|
|
449
|
+
selectionTargetsAgent(assignment.assignedAgentId, attemptSelection) ||
|
|
450
|
+
selectionTargetsAgent(assignment.sourceAgentId, attemptSelection)
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
const unresolvedAssignment = scopedAssignments.find(
|
|
385
454
|
(assignment) =>
|
|
386
455
|
assignment.blocking &&
|
|
387
|
-
!assignment.assignedAgentId
|
|
388
|
-
assignmentRelevantToAgent(assignment, agentId),
|
|
456
|
+
!assignment.assignedAgentId,
|
|
389
457
|
);
|
|
390
458
|
if (unresolvedAssignment) {
|
|
391
459
|
return {
|
|
@@ -395,10 +463,7 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
395
463
|
detail: unresolvedAssignment.assignmentDetail || unresolvedAssignment.summary || unresolvedAssignment.requestId,
|
|
396
464
|
};
|
|
397
465
|
}
|
|
398
|
-
const blockingAssignment = (
|
|
399
|
-
(assignment) =>
|
|
400
|
-
assignment.blocking && assignmentRelevantToAgent(assignment, agentId),
|
|
401
|
-
);
|
|
466
|
+
const blockingAssignment = scopedAssignments.find((assignment) => assignment.blocking);
|
|
402
467
|
if (blockingAssignment) {
|
|
403
468
|
return {
|
|
404
469
|
kind: "helper-assignment",
|
|
@@ -410,7 +475,18 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
410
475
|
const dependency = [
|
|
411
476
|
...(dependencySnapshot?.openInbound || []),
|
|
412
477
|
...(dependencySnapshot?.openOutbound || []),
|
|
413
|
-
].find((record) =>
|
|
478
|
+
].find((record) => {
|
|
479
|
+
if (agentId) {
|
|
480
|
+
return record.assignedAgentId === agentId || record.agentId === agentId;
|
|
481
|
+
}
|
|
482
|
+
if (!scopeToActiveAttempt) {
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
return (
|
|
486
|
+
selectionTargetsAgent(record.assignedAgentId, attemptSelection) ||
|
|
487
|
+
selectionTargetsAgent(record.agentId, attemptSelection)
|
|
488
|
+
);
|
|
489
|
+
});
|
|
414
490
|
if (dependency) {
|
|
415
491
|
return {
|
|
416
492
|
kind: "dependency",
|
|
@@ -419,7 +495,7 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
419
495
|
detail: dependency.summary || dependency.detail || dependency.id,
|
|
420
496
|
};
|
|
421
497
|
}
|
|
422
|
-
if (rerunRequest) {
|
|
498
|
+
if (!scopeToActiveAttempt && rerunRequest) {
|
|
423
499
|
return {
|
|
424
500
|
kind: "rerun-request",
|
|
425
501
|
id: rerunRequest.requestId || "active-rerun",
|
|
@@ -427,6 +503,14 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
427
503
|
detail: rerunRequest.reason || "Active rerun request controls next attempt selection.",
|
|
428
504
|
};
|
|
429
505
|
}
|
|
506
|
+
if (!scopeToActiveAttempt && relaunchPlan) {
|
|
507
|
+
return {
|
|
508
|
+
kind: "relaunch-plan",
|
|
509
|
+
id: `wave-${relaunchPlan.wave ?? "unknown"}-relaunch-plan`,
|
|
510
|
+
agentId: null,
|
|
511
|
+
detail: "Persisted relaunch plan controls the next safe launcher selection.",
|
|
512
|
+
};
|
|
513
|
+
}
|
|
430
514
|
const blocker = scopedTasks.find(
|
|
431
515
|
(task) => task.taskType === "blocker" && ["open", "working"].includes(task.state),
|
|
432
516
|
);
|
|
@@ -485,6 +569,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
485
569
|
}).filter((task) => !agentId || task.ownerAgentId === agentId || task.assigneeAgentId === agentId);
|
|
486
570
|
const controlState = readWaveControlPlaneState(lanePaths, wave.wave);
|
|
487
571
|
const proofRegistry = readWaveProofRegistry(lanePaths, wave.wave) || { entries: [] };
|
|
572
|
+
const relaunchPlan = readWaveRelaunchPlanSnapshot(lanePaths, wave.wave);
|
|
488
573
|
const rerunRequest = controlState.activeRerunRequest
|
|
489
574
|
? {
|
|
490
575
|
...controlState.activeRerunRequest,
|
|
@@ -497,6 +582,11 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
497
582
|
}),
|
|
498
583
|
}
|
|
499
584
|
: null;
|
|
585
|
+
const selection = buildEffectiveSelection(lanePaths, wave, {
|
|
586
|
+
activeAttempt: controlState.activeAttempt,
|
|
587
|
+
rerunRequest,
|
|
588
|
+
relaunchPlan,
|
|
589
|
+
});
|
|
500
590
|
return {
|
|
501
591
|
lane: lanePaths.lane,
|
|
502
592
|
wave: wave.wave,
|
|
@@ -506,7 +596,9 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
506
596
|
tasks,
|
|
507
597
|
capabilityAssignments,
|
|
508
598
|
dependencySnapshot,
|
|
599
|
+
activeAttempt: controlState.activeAttempt,
|
|
509
600
|
rerunRequest,
|
|
601
|
+
relaunchPlan,
|
|
510
602
|
agentId,
|
|
511
603
|
}),
|
|
512
604
|
logicalAgents: buildLogicalAgents({
|
|
@@ -515,7 +607,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
515
607
|
tasks,
|
|
516
608
|
dependencySnapshot,
|
|
517
609
|
capabilityAssignments,
|
|
518
|
-
|
|
610
|
+
selection,
|
|
519
611
|
proofRegistry,
|
|
520
612
|
}).filter((agent) => !agentId || agent.agentId === agentId),
|
|
521
613
|
tasks,
|
|
@@ -533,8 +625,9 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
533
625
|
proofBundles: (proofRegistry?.entries || []).filter(
|
|
534
626
|
(entry) => !agentId || entry.agentId === agentId,
|
|
535
627
|
),
|
|
628
|
+
selectionSource: selection.source,
|
|
536
629
|
rerunRequest,
|
|
537
|
-
relaunchPlan
|
|
630
|
+
relaunchPlan,
|
|
538
631
|
nextTimer: nextTaskDeadline(tasks),
|
|
539
632
|
activeAttempt: controlState.activeAttempt,
|
|
540
633
|
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { loadWaveConfig } from "./config.mjs";
|
|
3
5
|
import { analyzeMessageBoardCommunication } from "./coordination.mjs";
|
|
4
6
|
import { commsAgeSummary, deploymentSummary } from "./dashboard-state.mjs";
|
|
5
7
|
import {
|
|
8
|
+
buildLanePaths,
|
|
6
9
|
DEFAULT_REFRESH_MS,
|
|
7
10
|
DEFAULT_WAVE_LANE,
|
|
8
11
|
FINAL_EXIT_DELAY_MS,
|
|
@@ -14,12 +17,29 @@ import {
|
|
|
14
17
|
sleep,
|
|
15
18
|
truncate,
|
|
16
19
|
} from "./shared.mjs";
|
|
20
|
+
import {
|
|
21
|
+
createCurrentWaveDashboardTerminalEntry,
|
|
22
|
+
createGlobalDashboardTerminalEntry,
|
|
23
|
+
} from "./terminals.mjs";
|
|
24
|
+
|
|
25
|
+
const DASHBOARD_ATTACH_TARGETS = ["current", "global"];
|
|
26
|
+
|
|
27
|
+
function normalizeDashboardAttachTarget(value) {
|
|
28
|
+
const normalized = String(value || "")
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
if (!DASHBOARD_ATTACH_TARGETS.includes(normalized)) {
|
|
32
|
+
throw new Error(`--attach must be one of: ${DASHBOARD_ATTACH_TARGETS.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
17
36
|
|
|
18
37
|
export function parseDashboardArgs(argv) {
|
|
19
38
|
const options = {
|
|
20
39
|
lane: DEFAULT_WAVE_LANE,
|
|
21
40
|
dashboardFile: null,
|
|
22
41
|
messageBoard: null,
|
|
42
|
+
attach: null,
|
|
23
43
|
watch: false,
|
|
24
44
|
refreshMs: DEFAULT_REFRESH_MS,
|
|
25
45
|
};
|
|
@@ -39,6 +59,8 @@ export function parseDashboardArgs(argv) {
|
|
|
39
59
|
options.dashboardFile = path.resolve(REPO_ROOT, argv[++i] || "");
|
|
40
60
|
} else if (arg === "--message-board") {
|
|
41
61
|
options.messageBoard = path.resolve(REPO_ROOT, argv[++i] || "");
|
|
62
|
+
} else if (arg === "--attach") {
|
|
63
|
+
options.attach = normalizeDashboardAttachTarget(argv[++i] || "");
|
|
42
64
|
} else if (arg === "--refresh-ms") {
|
|
43
65
|
options.refreshMs = Number.parseInt(String(argv[++i] || ""), 10);
|
|
44
66
|
} else if (arg === "--help" || arg === "-h") {
|
|
@@ -47,12 +69,63 @@ export function parseDashboardArgs(argv) {
|
|
|
47
69
|
throw new Error(`Unknown argument: ${arg}`);
|
|
48
70
|
}
|
|
49
71
|
}
|
|
50
|
-
if (!options.dashboardFile) {
|
|
51
|
-
throw new Error("--dashboard-file is required");
|
|
72
|
+
if (!options.dashboardFile && !options.attach) {
|
|
73
|
+
throw new Error("--dashboard-file is required unless --attach is used");
|
|
52
74
|
}
|
|
53
75
|
return { help: false, options };
|
|
54
76
|
}
|
|
55
77
|
|
|
78
|
+
function tmuxSessionExists(socketName, sessionName) {
|
|
79
|
+
const result = spawnSync("tmux", ["-L", socketName, "has-session", "-t", sessionName], {
|
|
80
|
+
cwd: REPO_ROOT,
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
env: { ...process.env, TMUX: "" },
|
|
83
|
+
});
|
|
84
|
+
if (result.error) {
|
|
85
|
+
throw new Error(`tmux session lookup failed: ${result.error.message}`);
|
|
86
|
+
}
|
|
87
|
+
if (result.status === 0) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const combined = `${String(result.stderr || "").toLowerCase()}\n${String(result.stdout || "").toLowerCase()}`;
|
|
91
|
+
if (
|
|
92
|
+
combined.includes("can't find session") ||
|
|
93
|
+
combined.includes("no server running") ||
|
|
94
|
+
combined.includes("error connecting")
|
|
95
|
+
) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
throw new Error((result.stderr || result.stdout || "tmux has-session failed").trim());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function attachDashboardSession(lane, target) {
|
|
102
|
+
const config = loadWaveConfig();
|
|
103
|
+
const lanePaths = buildLanePaths(lane, { config });
|
|
104
|
+
const entry =
|
|
105
|
+
target === "global"
|
|
106
|
+
? createGlobalDashboardTerminalEntry(lanePaths, "current")
|
|
107
|
+
: createCurrentWaveDashboardTerminalEntry(lanePaths);
|
|
108
|
+
if (!tmuxSessionExists(lanePaths.tmuxSocketName, entry.sessionName)) {
|
|
109
|
+
const dashboardsRel = path.relative(REPO_ROOT, path.dirname(lanePaths.globalDashboardPath));
|
|
110
|
+
throw new Error(
|
|
111
|
+
`No ${target} dashboard session is live for lane ${lanePaths.lane}. Launch a dashboarded run on that lane, then inspect ${dashboardsRel} if you need the last written dashboard state.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const result = spawnSync("tmux", ["-L", lanePaths.tmuxSocketName, "attach", "-t", entry.sessionName], {
|
|
115
|
+
cwd: REPO_ROOT,
|
|
116
|
+
stdio: "inherit",
|
|
117
|
+
env: { ...process.env, TMUX: "" },
|
|
118
|
+
});
|
|
119
|
+
if (result.error) {
|
|
120
|
+
throw new Error(`tmux attach failed: ${result.error.message}`);
|
|
121
|
+
}
|
|
122
|
+
if (result.status !== 0) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`tmux attach exited ${result.status} for lane ${lanePaths.lane} ${target} dashboard session ${entry.sessionName}.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
56
129
|
function readMessageBoardTail(messageBoardPath, maxLines = 24) {
|
|
57
130
|
if (!messageBoardPath) {
|
|
58
131
|
return ["(message board path unavailable)"];
|
|
@@ -379,12 +452,19 @@ Options:
|
|
|
379
452
|
--lane <name> Wave lane name (default: ${DEFAULT_WAVE_LANE})
|
|
380
453
|
--dashboard-file <path> Path to wave/global dashboard JSON
|
|
381
454
|
--message-board <path> Optional message board path override
|
|
455
|
+
--attach <current|global>
|
|
456
|
+
Attach to the stable tmux-backed dashboard session for the lane
|
|
382
457
|
--watch Refresh continuously
|
|
383
458
|
--refresh-ms <n> Refresh interval in ms (default: ${DEFAULT_REFRESH_MS})
|
|
384
459
|
`);
|
|
385
460
|
return;
|
|
386
461
|
}
|
|
387
462
|
|
|
463
|
+
if (options.attach) {
|
|
464
|
+
attachDashboardSession(options.lane, options.attach);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
388
468
|
let terminalStateReachedAt = null;
|
|
389
469
|
while (true) {
|
|
390
470
|
const raw = fs.existsSync(options.dashboardFile)
|
|
@@ -92,6 +92,33 @@ const REQUIRED_GITIGNORE_ENTRIES = [
|
|
|
92
92
|
"docs/research/papers/",
|
|
93
93
|
"docs/research/articles/",
|
|
94
94
|
];
|
|
95
|
+
const PLANNER_MIGRATION_REQUIRED_SURFACES = [
|
|
96
|
+
{
|
|
97
|
+
id: "planner-role",
|
|
98
|
+
label: "docs/agents/wave-planner-role.md",
|
|
99
|
+
path: "docs/agents/wave-planner-role.md",
|
|
100
|
+
kind: "file",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "planner-skill",
|
|
104
|
+
label: "skills/role-planner/",
|
|
105
|
+
path: "skills/role-planner",
|
|
106
|
+
kind: "dir",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "planner-context7",
|
|
110
|
+
label: "docs/context7/planner-agent/",
|
|
111
|
+
path: "docs/context7/planner-agent",
|
|
112
|
+
kind: "dir",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "planner-lessons",
|
|
116
|
+
label: "docs/reference/wave-planning-lessons.md",
|
|
117
|
+
path: "docs/reference/wave-planning-lessons.md",
|
|
118
|
+
kind: "file",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
const PLANNER_REQUIRED_BUNDLE_ID = "planner-agentic";
|
|
95
122
|
|
|
96
123
|
function collectDeclaredDeployKinds(waves = []) {
|
|
97
124
|
return Array.from(
|
|
@@ -222,6 +249,8 @@ function slugifyVersion(value) {
|
|
|
222
249
|
}
|
|
223
250
|
|
|
224
251
|
function formatUpgradeReport(report) {
|
|
252
|
+
const plannerMigrationErrors = report.doctor.errors.filter((issue) => isPlannerMigrationIssue(issue));
|
|
253
|
+
const otherDoctorErrors = report.doctor.errors.filter((issue) => !isPlannerMigrationIssue(issue));
|
|
225
254
|
return [
|
|
226
255
|
`# Wave Upgrade Report`,
|
|
227
256
|
"",
|
|
@@ -238,12 +267,27 @@ function formatUpgradeReport(report) {
|
|
|
238
267
|
"",
|
|
239
268
|
"- No repo-owned plans, waves, role prompts, or config files were overwritten.",
|
|
240
269
|
"- New runtime behavior comes from the installed package version.",
|
|
270
|
+
...(report.initMode === "adopt-existing" && plannerMigrationErrors.length > 0
|
|
271
|
+
? [
|
|
272
|
+
"",
|
|
273
|
+
"## Adopted Repo Follow-Up",
|
|
274
|
+
"",
|
|
275
|
+
"- This workspace was adopted from an existing repo-owned Wave surface.",
|
|
276
|
+
"- `wave upgrade` does not copy new planner starter docs, skills, or Context7 bundle entries into adopted repos.",
|
|
277
|
+
...plannerMigrationErrors.map((issue) => `- Error: ${issue}`),
|
|
278
|
+
]
|
|
279
|
+
: []),
|
|
280
|
+
...(report.initMode === "adopt-existing" && plannerMigrationErrors.length > 0
|
|
281
|
+
? [
|
|
282
|
+
"- After syncing that planner surface, rerun `pnpm exec wave doctor` before relying on `wave draft --agentic` or planner-aware validation.",
|
|
283
|
+
]
|
|
284
|
+
: []),
|
|
241
285
|
...(report.doctor.errors.length > 0 || report.doctor.warnings.length > 0
|
|
242
286
|
? [
|
|
243
287
|
"",
|
|
244
288
|
"## Follow-Up",
|
|
245
289
|
"",
|
|
246
|
-
...
|
|
290
|
+
...otherDoctorErrors.map((issue) => `- Error: ${issue}`),
|
|
247
291
|
...report.doctor.warnings.map((issue) => `- Warning: ${issue}`),
|
|
248
292
|
]
|
|
249
293
|
: []),
|
|
@@ -275,6 +319,48 @@ function plannerRequiredPaths() {
|
|
|
275
319
|
).sort();
|
|
276
320
|
}
|
|
277
321
|
|
|
322
|
+
function isPlannerMigrationIssue(issue) {
|
|
323
|
+
return String(issue || "").startsWith("Planner starter surface is incomplete");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function missingPlannerMigrationSurfaceLabels() {
|
|
327
|
+
const missing = [];
|
|
328
|
+
for (const surface of PLANNER_MIGRATION_REQUIRED_SURFACES) {
|
|
329
|
+
const targetPath = path.join(REPO_ROOT, surface.path);
|
|
330
|
+
if (!fs.existsSync(targetPath)) {
|
|
331
|
+
missing.push(surface.label);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (surface.kind === "dir") {
|
|
335
|
+
try {
|
|
336
|
+
if (fs.readdirSync(targetPath).length === 0) {
|
|
337
|
+
missing.push(surface.label);
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
missing.push(surface.label);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return missing;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function plannerMigrationIssue(config, context7BundleIndex) {
|
|
348
|
+
const missing = missingPlannerMigrationSurfaceLabels();
|
|
349
|
+
const bundleId = String(config?.planner?.agentic?.context7Bundle || "").trim();
|
|
350
|
+
const bundleEntryMissing =
|
|
351
|
+
bundleId === "" || bundleId === PLANNER_REQUIRED_BUNDLE_ID
|
|
352
|
+
? !context7BundleIndex?.bundles?.[PLANNER_REQUIRED_BUNDLE_ID]
|
|
353
|
+
: false;
|
|
354
|
+
if (missing.length === 0 && !bundleEntryMissing) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const remediationItems = missing.slice();
|
|
358
|
+
if (bundleEntryMissing) {
|
|
359
|
+
remediationItems.push(`docs/context7/bundles.json#${PLANNER_REQUIRED_BUNDLE_ID}`);
|
|
360
|
+
}
|
|
361
|
+
return `Planner starter surface is incomplete for 0.7.x workspaces. Sync ${remediationItems.join(", ")} from the packaged release, then rerun \`pnpm exec wave doctor\`.`;
|
|
362
|
+
}
|
|
363
|
+
|
|
278
364
|
export function runDoctor() {
|
|
279
365
|
const errors = [];
|
|
280
366
|
const warnings = [];
|
|
@@ -320,14 +406,22 @@ export function runDoctor() {
|
|
|
320
406
|
}
|
|
321
407
|
}
|
|
322
408
|
const context7BundleIndex = loadContext7BundleIndex(lanePaths.context7BundleIndexPath);
|
|
409
|
+
const plannerMigration = plannerMigrationIssue(config, context7BundleIndex);
|
|
410
|
+
if (plannerMigration) {
|
|
411
|
+
errors.push(plannerMigration);
|
|
412
|
+
}
|
|
323
413
|
const plannerPaths = plannerRequiredPaths();
|
|
324
414
|
for (const relPath of plannerPaths) {
|
|
325
|
-
if (!fs.existsSync(path.join(REPO_ROOT, relPath))) {
|
|
415
|
+
if (!fs.existsSync(path.join(REPO_ROOT, relPath)) && !plannerMigration) {
|
|
326
416
|
errors.push(`Missing planner file: ${relPath}`);
|
|
327
417
|
}
|
|
328
418
|
}
|
|
329
419
|
const plannerBundleId = String(config.planner?.agentic?.context7Bundle || "").trim();
|
|
330
|
-
if (
|
|
420
|
+
if (
|
|
421
|
+
plannerBundleId &&
|
|
422
|
+
!context7BundleIndex.bundles[plannerBundleId] &&
|
|
423
|
+
!(plannerMigration && plannerBundleId === PLANNER_REQUIRED_BUNDLE_ID)
|
|
424
|
+
) {
|
|
331
425
|
errors.push(
|
|
332
426
|
`planner.agentic.context7Bundle references unknown bundle "${plannerBundleId}".`,
|
|
333
427
|
);
|
|
@@ -484,6 +578,7 @@ export function upgradeWorkspace() {
|
|
|
484
578
|
previousVersion,
|
|
485
579
|
currentVersion: metadata.version,
|
|
486
580
|
generatedAt,
|
|
581
|
+
initMode: existingState.initMode || null,
|
|
487
582
|
releases,
|
|
488
583
|
doctor,
|
|
489
584
|
};
|
|
@@ -135,16 +135,16 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
|
|
|
135
135
|
skillProjection: agent.skillsResolved,
|
|
136
136
|
});
|
|
137
137
|
const resolvedExecutorMode = launchSpec.executorId || agent.executorResolved?.id || "codex";
|
|
138
|
+
writeJsonAtomic(path.join(overlayDir, "launch-preview.json"), {
|
|
139
|
+
executorId: resolvedExecutorMode,
|
|
140
|
+
command: launchSpec.command,
|
|
141
|
+
env: launchSpec.env || {},
|
|
142
|
+
useRateLimitRetries: launchSpec.useRateLimitRetries === true,
|
|
143
|
+
invocationLines: launchSpec.invocationLines,
|
|
144
|
+
limits: launchSpec.limits || null,
|
|
145
|
+
skills: summarizeResolvedSkills(agent.skillsResolved),
|
|
146
|
+
});
|
|
138
147
|
if (dryRun) {
|
|
139
|
-
writeJsonAtomic(path.join(overlayDir, "launch-preview.json"), {
|
|
140
|
-
executorId: resolvedExecutorMode,
|
|
141
|
-
command: launchSpec.command,
|
|
142
|
-
env: launchSpec.env || {},
|
|
143
|
-
useRateLimitRetries: launchSpec.useRateLimitRetries === true,
|
|
144
|
-
invocationLines: launchSpec.invocationLines,
|
|
145
|
-
limits: launchSpec.limits || null,
|
|
146
|
-
skills: summarizeResolvedSkills(agent.skillsResolved),
|
|
147
|
-
});
|
|
148
148
|
return {
|
|
149
149
|
promptHash,
|
|
150
150
|
context7,
|