@chllming/wave-orchestration 0.7.0 → 0.7.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 +40 -0
- package/README.md +9 -8
- package/docs/guides/planner.md +19 -0
- package/docs/guides/terminal-surfaces.md +12 -0
- package/docs/plans/component-cutover-matrix.json +50 -3
- package/docs/plans/current-state.md +1 -1
- package/docs/plans/end-state-architecture.md +927 -0
- 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/plans/waves/wave-1.md +376 -0
- package/docs/plans/waves/wave-2.md +292 -0
- package/docs/plans/waves/wave-3.md +342 -0
- package/docs/plans/waves/wave-4.md +391 -0
- package/docs/plans/waves/wave-5.md +382 -0
- package/docs/plans/waves/wave-6.md +321 -0
- 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 +43 -2
- package/scripts/wave-orchestrator/agent-state.mjs +458 -35
- package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
- package/scripts/wave-orchestrator/control-cli.mjs +119 -20
- package/scripts/wave-orchestrator/coordination.mjs +11 -10
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +82 -2
- package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
- package/scripts/wave-orchestrator/install.mjs +120 -3
- package/scripts/wave-orchestrator/launcher-derived-state.mjs +915 -0
- package/scripts/wave-orchestrator/launcher-gates.mjs +1061 -0
- package/scripts/wave-orchestrator/launcher-retry.mjs +873 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +9 -9
- package/scripts/wave-orchestrator/launcher-supervisor.mjs +704 -0
- package/scripts/wave-orchestrator/launcher.mjs +317 -2999
- package/scripts/wave-orchestrator/task-entity.mjs +557 -0
- package/scripts/wave-orchestrator/terminals.mjs +1 -1
- package/scripts/wave-orchestrator/wave-files.mjs +138 -20
- package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
- package/wave.config.json +1 -1
|
@@ -403,3 +403,84 @@ export function writeWaveControlDeliveryState(filePath, payload, defaults = {})
|
|
|
403
403
|
export function cloneArtifactPayload(value) {
|
|
404
404
|
return cloneJson(value);
|
|
405
405
|
}
|
|
406
|
+
|
|
407
|
+
// ── Wave 4: Surface class metadata and additional schema normalizers ──
|
|
408
|
+
|
|
409
|
+
export const WAVE_STATE_SCHEMA_VERSION = 1;
|
|
410
|
+
export const TASK_ENTITY_SCHEMA_VERSION = 1;
|
|
411
|
+
export const AGENT_RESULT_ENVELOPE_SCHEMA_VERSION = 1;
|
|
412
|
+
export const RESUME_PLAN_SCHEMA_VERSION = 1;
|
|
413
|
+
export const HUMAN_INPUT_WORKFLOW_SCHEMA_VERSION = 1;
|
|
414
|
+
|
|
415
|
+
export const SURFACE_CLASS_CANONICAL_EVENT = "canonical-event";
|
|
416
|
+
export const SURFACE_CLASS_CANONICAL_SNAPSHOT = "canonical-snapshot";
|
|
417
|
+
export const SURFACE_CLASS_CACHED_DERIVED = "cached-derived";
|
|
418
|
+
export const SURFACE_CLASS_HUMAN_PROJECTION = "human-projection";
|
|
419
|
+
export const SURFACE_CLASSES = new Set([
|
|
420
|
+
SURFACE_CLASS_CANONICAL_EVENT,
|
|
421
|
+
SURFACE_CLASS_CANONICAL_SNAPSHOT,
|
|
422
|
+
SURFACE_CLASS_CACHED_DERIVED,
|
|
423
|
+
SURFACE_CLASS_HUMAN_PROJECTION,
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
export const WAVE_STATE_KIND = "wave-state-snapshot";
|
|
427
|
+
export const TASK_ENTITY_KIND = "wave-task-entity";
|
|
428
|
+
export const AGENT_RESULT_ENVELOPE_KIND = "agent-result-envelope";
|
|
429
|
+
export const RESUME_PLAN_KIND = "wave-resume-plan";
|
|
430
|
+
export const HUMAN_INPUT_WORKFLOW_KIND = "human-input-workflow-state";
|
|
431
|
+
|
|
432
|
+
export function normalizeWaveStateSnapshot(payload, defaults = {}) {
|
|
433
|
+
const source = isPlainObject(payload) ? payload : {};
|
|
434
|
+
return {
|
|
435
|
+
schemaVersion: WAVE_STATE_SCHEMA_VERSION,
|
|
436
|
+
kind: WAVE_STATE_KIND,
|
|
437
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
438
|
+
lane: normalizeText(source.lane, normalizeText(defaults.lane, null)),
|
|
439
|
+
wave: normalizeInteger(source.wave, normalizeInteger(defaults.wave, null)),
|
|
440
|
+
...source,
|
|
441
|
+
schemaVersion: WAVE_STATE_SCHEMA_VERSION,
|
|
442
|
+
kind: WAVE_STATE_KIND,
|
|
443
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
444
|
+
generatedAt: normalizeText(source.generatedAt, toIsoTimestamp()),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function readWaveStateSnapshot(filePath, defaults = {}) {
|
|
449
|
+
const payload = readJsonOrNull(filePath);
|
|
450
|
+
if (!payload) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return normalizeWaveStateSnapshot(payload, defaults);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function writeWaveStateSnapshot(filePath, payload, defaults = {}) {
|
|
457
|
+
const normalized = normalizeWaveStateSnapshot(payload, defaults);
|
|
458
|
+
writeJsonAtomic(filePath, normalized);
|
|
459
|
+
return normalized;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function normalizeAgentResultEnvelope(payload) {
|
|
463
|
+
const source = isPlainObject(payload) ? payload : {};
|
|
464
|
+
return {
|
|
465
|
+
schemaVersion: AGENT_RESULT_ENVELOPE_SCHEMA_VERSION,
|
|
466
|
+
kind: AGENT_RESULT_ENVELOPE_KIND,
|
|
467
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
468
|
+
...source,
|
|
469
|
+
schemaVersion: AGENT_RESULT_ENVELOPE_SCHEMA_VERSION,
|
|
470
|
+
kind: AGENT_RESULT_ENVELOPE_KIND,
|
|
471
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function normalizeResumePlan(payload) {
|
|
476
|
+
const source = isPlainObject(payload) ? payload : {};
|
|
477
|
+
return {
|
|
478
|
+
schemaVersion: RESUME_PLAN_SCHEMA_VERSION,
|
|
479
|
+
kind: RESUME_PLAN_KIND,
|
|
480
|
+
_meta: { surfaceClass: SURFACE_CLASS_CACHED_DERIVED },
|
|
481
|
+
...source,
|
|
482
|
+
schemaVersion: RESUME_PLAN_SCHEMA_VERSION,
|
|
483
|
+
kind: RESUME_PLAN_KIND,
|
|
484
|
+
_meta: { surfaceClass: SURFACE_CLASS_CACHED_DERIVED },
|
|
485
|
+
};
|
|
486
|
+
}
|
|
@@ -270,16 +270,60 @@ 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) => {
|
|
278
316
|
const statusPath = statusPathForAgent(lanePaths, wave, agent);
|
|
279
317
|
const statusRecord = readStatusRecordIfPresent(statusPath);
|
|
318
|
+
const logPath = path.join(lanePaths.logsDir, `wave-${wave.wave}-${agent.slug}.log`);
|
|
280
319
|
const summary = augmentSummaryWithProofRegistry(
|
|
281
320
|
agent,
|
|
282
|
-
readAgentExecutionSummary(statusPath
|
|
321
|
+
readAgentExecutionSummary(statusPath, {
|
|
322
|
+
agent,
|
|
323
|
+
statusPath,
|
|
324
|
+
statusRecord,
|
|
325
|
+
logPath: fs.existsSync(logPath) ? logPath : null,
|
|
326
|
+
}),
|
|
283
327
|
proofRegistry || { entries: [] },
|
|
284
328
|
);
|
|
285
329
|
const proofValidation =
|
|
@@ -301,9 +345,15 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
|
|
|
301
345
|
const dependency = openInbound.find((record) => record.assignedAgentId === agent.agentId);
|
|
302
346
|
let state = "planned";
|
|
303
347
|
let reason = "";
|
|
304
|
-
if (
|
|
348
|
+
if (selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId)) {
|
|
349
|
+
state = "working";
|
|
350
|
+
reason = selection?.detail || "Selected by the active launcher attempt.";
|
|
351
|
+
} else if (selectedAgentIds.has(agent.agentId)) {
|
|
305
352
|
state = "needs-rerun";
|
|
306
|
-
reason =
|
|
353
|
+
reason =
|
|
354
|
+
selection?.source === "relaunch-plan"
|
|
355
|
+
? "Selected by the persisted relaunch plan."
|
|
356
|
+
: "Selected by active rerun request.";
|
|
307
357
|
} else if (targetedBlockingTasks.some((task) => task.state === "working")) {
|
|
308
358
|
state = "working";
|
|
309
359
|
reason = targetedBlockingTasks.find((task) => task.state === "working")?.title || "";
|
|
@@ -334,7 +384,8 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
|
|
|
334
384
|
state,
|
|
335
385
|
reason: reason || null,
|
|
336
386
|
taskIds: targetedTasks.map((task) => task.taskId),
|
|
337
|
-
selectedForRerun:
|
|
387
|
+
selectedForRerun: selectedAgentIds.has(agent.agentId) && selection?.source !== "active-attempt",
|
|
388
|
+
selectedForActiveAttempt: selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId),
|
|
338
389
|
activeProofBundleIds: (proofRegistry?.entries || [])
|
|
339
390
|
.filter(
|
|
340
391
|
(entry) =>
|
|
@@ -346,10 +397,25 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
|
|
|
346
397
|
});
|
|
347
398
|
}
|
|
348
399
|
|
|
349
|
-
function
|
|
350
|
-
|
|
400
|
+
function selectionTargetsAgent(agentId, selectionSet) {
|
|
401
|
+
return Boolean(agentId) && selectionSet.has(agentId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, activeAttempt, rerunRequest, relaunchPlan, agentId = "" }) {
|
|
405
|
+
const attemptSelection = new Set(activeAttempt?.selectedAgentIds || []);
|
|
406
|
+
const scopeToActiveAttempt = !agentId && attemptSelection.size > 0;
|
|
407
|
+
const scopedTasks = (agentId
|
|
351
408
|
? tasks.filter((task) => task.ownerAgentId === agentId || task.assigneeAgentId === agentId)
|
|
352
|
-
: tasks
|
|
409
|
+
: tasks
|
|
410
|
+
).filter((task) => {
|
|
411
|
+
if (!scopeToActiveAttempt) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
return (
|
|
415
|
+
selectionTargetsAgent(task.ownerAgentId, attemptSelection) ||
|
|
416
|
+
selectionTargetsAgent(task.assigneeAgentId, attemptSelection)
|
|
417
|
+
);
|
|
418
|
+
});
|
|
353
419
|
const pendingHuman = scopedTasks.find((task) => task.state === "input-required");
|
|
354
420
|
if (pendingHuman) {
|
|
355
421
|
return {
|
|
@@ -381,11 +447,19 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
381
447
|
detail: clarification.title,
|
|
382
448
|
};
|
|
383
449
|
}
|
|
384
|
-
const
|
|
450
|
+
const scopedAssignments = (capabilityAssignments || []).filter((assignment) => {
|
|
451
|
+
if (!scopeToActiveAttempt) {
|
|
452
|
+
return assignmentRelevantToAgent(assignment, agentId);
|
|
453
|
+
}
|
|
454
|
+
return (
|
|
455
|
+
selectionTargetsAgent(assignment.assignedAgentId, attemptSelection) ||
|
|
456
|
+
selectionTargetsAgent(assignment.sourceAgentId, attemptSelection)
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
const unresolvedAssignment = scopedAssignments.find(
|
|
385
460
|
(assignment) =>
|
|
386
461
|
assignment.blocking &&
|
|
387
|
-
!assignment.assignedAgentId
|
|
388
|
-
assignmentRelevantToAgent(assignment, agentId),
|
|
462
|
+
!assignment.assignedAgentId,
|
|
389
463
|
);
|
|
390
464
|
if (unresolvedAssignment) {
|
|
391
465
|
return {
|
|
@@ -395,10 +469,7 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
395
469
|
detail: unresolvedAssignment.assignmentDetail || unresolvedAssignment.summary || unresolvedAssignment.requestId,
|
|
396
470
|
};
|
|
397
471
|
}
|
|
398
|
-
const blockingAssignment = (
|
|
399
|
-
(assignment) =>
|
|
400
|
-
assignment.blocking && assignmentRelevantToAgent(assignment, agentId),
|
|
401
|
-
);
|
|
472
|
+
const blockingAssignment = scopedAssignments.find((assignment) => assignment.blocking);
|
|
402
473
|
if (blockingAssignment) {
|
|
403
474
|
return {
|
|
404
475
|
kind: "helper-assignment",
|
|
@@ -410,7 +481,18 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
410
481
|
const dependency = [
|
|
411
482
|
...(dependencySnapshot?.openInbound || []),
|
|
412
483
|
...(dependencySnapshot?.openOutbound || []),
|
|
413
|
-
].find((record) =>
|
|
484
|
+
].find((record) => {
|
|
485
|
+
if (agentId) {
|
|
486
|
+
return record.assignedAgentId === agentId || record.agentId === agentId;
|
|
487
|
+
}
|
|
488
|
+
if (!scopeToActiveAttempt) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return (
|
|
492
|
+
selectionTargetsAgent(record.assignedAgentId, attemptSelection) ||
|
|
493
|
+
selectionTargetsAgent(record.agentId, attemptSelection)
|
|
494
|
+
);
|
|
495
|
+
});
|
|
414
496
|
if (dependency) {
|
|
415
497
|
return {
|
|
416
498
|
kind: "dependency",
|
|
@@ -419,7 +501,7 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
419
501
|
detail: dependency.summary || dependency.detail || dependency.id,
|
|
420
502
|
};
|
|
421
503
|
}
|
|
422
|
-
if (rerunRequest) {
|
|
504
|
+
if (!scopeToActiveAttempt && rerunRequest) {
|
|
423
505
|
return {
|
|
424
506
|
kind: "rerun-request",
|
|
425
507
|
id: rerunRequest.requestId || "active-rerun",
|
|
@@ -427,6 +509,14 @@ function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, r
|
|
|
427
509
|
detail: rerunRequest.reason || "Active rerun request controls next attempt selection.",
|
|
428
510
|
};
|
|
429
511
|
}
|
|
512
|
+
if (!scopeToActiveAttempt && relaunchPlan) {
|
|
513
|
+
return {
|
|
514
|
+
kind: "relaunch-plan",
|
|
515
|
+
id: `wave-${relaunchPlan.wave ?? "unknown"}-relaunch-plan`,
|
|
516
|
+
agentId: null,
|
|
517
|
+
detail: "Persisted relaunch plan controls the next safe launcher selection.",
|
|
518
|
+
};
|
|
519
|
+
}
|
|
430
520
|
const blocker = scopedTasks.find(
|
|
431
521
|
(task) => task.taskType === "blocker" && ["open", "working"].includes(task.state),
|
|
432
522
|
);
|
|
@@ -485,6 +575,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
485
575
|
}).filter((task) => !agentId || task.ownerAgentId === agentId || task.assigneeAgentId === agentId);
|
|
486
576
|
const controlState = readWaveControlPlaneState(lanePaths, wave.wave);
|
|
487
577
|
const proofRegistry = readWaveProofRegistry(lanePaths, wave.wave) || { entries: [] };
|
|
578
|
+
const relaunchPlan = readWaveRelaunchPlanSnapshot(lanePaths, wave.wave);
|
|
488
579
|
const rerunRequest = controlState.activeRerunRequest
|
|
489
580
|
? {
|
|
490
581
|
...controlState.activeRerunRequest,
|
|
@@ -497,6 +588,11 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
497
588
|
}),
|
|
498
589
|
}
|
|
499
590
|
: null;
|
|
591
|
+
const selection = buildEffectiveSelection(lanePaths, wave, {
|
|
592
|
+
activeAttempt: controlState.activeAttempt,
|
|
593
|
+
rerunRequest,
|
|
594
|
+
relaunchPlan,
|
|
595
|
+
});
|
|
500
596
|
return {
|
|
501
597
|
lane: lanePaths.lane,
|
|
502
598
|
wave: wave.wave,
|
|
@@ -506,7 +602,9 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
506
602
|
tasks,
|
|
507
603
|
capabilityAssignments,
|
|
508
604
|
dependencySnapshot,
|
|
605
|
+
activeAttempt: controlState.activeAttempt,
|
|
509
606
|
rerunRequest,
|
|
607
|
+
relaunchPlan,
|
|
510
608
|
agentId,
|
|
511
609
|
}),
|
|
512
610
|
logicalAgents: buildLogicalAgents({
|
|
@@ -515,7 +613,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
515
613
|
tasks,
|
|
516
614
|
dependencySnapshot,
|
|
517
615
|
capabilityAssignments,
|
|
518
|
-
|
|
616
|
+
selection,
|
|
519
617
|
proofRegistry,
|
|
520
618
|
}).filter((agent) => !agentId || agent.agentId === agentId),
|
|
521
619
|
tasks,
|
|
@@ -533,8 +631,9 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
533
631
|
proofBundles: (proofRegistry?.entries || []).filter(
|
|
534
632
|
(entry) => !agentId || entry.agentId === agentId,
|
|
535
633
|
),
|
|
634
|
+
selectionSource: selection.source,
|
|
536
635
|
rerunRequest,
|
|
537
|
-
relaunchPlan
|
|
636
|
+
relaunchPlan,
|
|
538
637
|
nextTimer: nextTaskDeadline(tasks),
|
|
539
638
|
activeAttempt: controlState.activeAttempt,
|
|
540
639
|
};
|
|
@@ -269,6 +269,15 @@ export function buildExecutionPrompt({
|
|
|
269
269
|
"- Use `clear` only when no unresolved findings or approvals remain. Use `blocked` only when the wave must stop before integration.",
|
|
270
270
|
]
|
|
271
271
|
: [];
|
|
272
|
+
const coordinationCommand = [
|
|
273
|
+
"pnpm exec wave coord post",
|
|
274
|
+
`--lane ${lane}`,
|
|
275
|
+
`--wave ${wave}`,
|
|
276
|
+
`--agent ${agent.agentId}`,
|
|
277
|
+
'--kind "<request|ack|claim|evidence|decision|blocker|handoff|clarification-request|orchestrator-guidance|resolved-by-policy|human-escalation|human-feedback|integration-summary>"',
|
|
278
|
+
'--summary "<one-line summary>"',
|
|
279
|
+
'--detail "<short detail>"',
|
|
280
|
+
].join(" ");
|
|
272
281
|
const implementationRequirements =
|
|
273
282
|
![contQaAgentId, documentationAgentId].includes(agent.agentId) &&
|
|
274
283
|
!isSecurityReviewAgent(agent) &&
|
|
@@ -281,7 +290,8 @@ export function buildExecutionPrompt({
|
|
|
281
290
|
"- Emit one final structured component marker per owned component: `[wave-component] component=<id> level=<level> state=<met|gap> detail=<short-note>`.",
|
|
282
291
|
]
|
|
283
292
|
: []),
|
|
284
|
-
"- If
|
|
293
|
+
"- If the work is incomplete, keep the required proof/doc/component markers and set `state=gap` on the relevant final marker instead of narrating completion.",
|
|
294
|
+
`- Route unresolved architecture, integration, durability, ops, or docs issues through \`${coordinationCommand}\`. Do not append \`[wave-gap]\` lines after the final implementation markers.`,
|
|
285
295
|
]
|
|
286
296
|
: [];
|
|
287
297
|
const exitContractLines = agent.exitContract
|
|
@@ -305,15 +315,6 @@ export function buildExecutionPrompt({
|
|
|
305
315
|
'--context "<what you tried, options, and impact>"',
|
|
306
316
|
"--timeout-seconds 30",
|
|
307
317
|
].join(" ");
|
|
308
|
-
const coordinationCommand = [
|
|
309
|
-
"pnpm exec wave coord post",
|
|
310
|
-
`--lane ${lane}`,
|
|
311
|
-
`--wave ${wave}`,
|
|
312
|
-
`--agent ${agent.agentId}`,
|
|
313
|
-
'--kind "<request|ack|claim|evidence|decision|blocker|handoff|clarification-request|orchestrator-guidance|resolved-by-policy|human-escalation|human-feedback|integration-summary>"',
|
|
314
|
-
'--summary "<one-line summary>"',
|
|
315
|
-
'--detail "<short detail>"',
|
|
316
|
-
].join(" ");
|
|
317
318
|
const context7Selection = context7?.selection || agent?.context7Resolved || null;
|
|
318
319
|
const executorId = agent?.executorResolved?.id || "default";
|
|
319
320
|
const context7LibrarySummary =
|
|
@@ -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)
|