@chllming/wave-orchestration 0.9.0 → 0.9.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 +28 -0
- package/README.md +119 -18
- package/docs/README.md +7 -3
- package/docs/architecture/README.md +1498 -0
- package/docs/concepts/operating-modes.md +2 -2
- package/docs/guides/author-and-run-waves.md +14 -4
- package/docs/guides/planner.md +2 -2
- package/docs/guides/{recommendations-0.9.0.md → recommendations-0.9.1.md} +8 -7
- package/docs/guides/sandboxed-environments.md +158 -0
- package/docs/guides/terminal-surfaces.md +14 -12
- package/docs/plans/current-state.md +5 -3
- package/docs/plans/end-state-architecture.md +3 -1
- package/docs/plans/examples/wave-example-design-handoff.md +1 -1
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +46 -19
- package/docs/plans/sandbox-end-state-architecture.md +153 -0
- package/docs/reference/cli-reference.md +71 -7
- package/docs/reference/coordination-and-closure.md +1 -1
- package/docs/reference/github-packages-setup.md +1 -1
- package/docs/reference/migration-0.2-to-0.5.md +9 -7
- package/docs/reference/npmjs-token-publishing.md +53 -0
- package/docs/reference/npmjs-trusted-publishing.md +4 -50
- package/docs/reference/package-publishing-flow.md +272 -0
- package/docs/reference/runtime-config/README.md +2 -2
- package/docs/reference/sample-waves.md +5 -5
- package/docs/reference/skills.md +1 -1
- package/docs/roadmap.md +43 -201
- package/package.json +1 -1
- package/releases/manifest.json +19 -0
- package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
- package/scripts/wave-orchestrator/agent-state.mjs +0 -1
- package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
- package/scripts/wave-orchestrator/autonomous.mjs +47 -14
- package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
- package/scripts/wave-orchestrator/control-cli.mjs +42 -5
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +115 -43
- package/scripts/wave-orchestrator/derived-state-engine.mjs +6 -3
- package/scripts/wave-orchestrator/gate-engine.mjs +106 -38
- package/scripts/wave-orchestrator/install.mjs +13 -0
- package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +179 -68
- package/scripts/wave-orchestrator/launcher.mjs +201 -53
- package/scripts/wave-orchestrator/ledger.mjs +7 -2
- package/scripts/wave-orchestrator/projection-writer.mjs +13 -1
- package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
- package/scripts/wave-orchestrator/retry-control.mjs +3 -3
- package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
- package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
- package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
- package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
- package/scripts/wave-orchestrator/terminals.mjs +12 -32
- package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
- package/scripts/wave-orchestrator/wave-files.mjs +38 -5
- package/scripts/wave.mjs +13 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { appendCoordinationRecord } from "./coordination-store.mjs";
|
|
2
3
|
import {
|
|
3
4
|
parseStructuredSignalsFromLog,
|
|
4
5
|
refreshWaveDashboardAgentStates,
|
|
@@ -14,8 +15,13 @@ import {
|
|
|
14
15
|
readWaveIntegrationBarrier as readWaveIntegrationBarrierDefault,
|
|
15
16
|
readWaveSecurityGate as readWaveSecurityGateDefault,
|
|
16
17
|
} from "./gate-engine.mjs";
|
|
18
|
+
import { applyLaunchResultToRun } from "./launcher-runtime.mjs";
|
|
17
19
|
import { REPO_ROOT, toIsoTimestamp } from "./shared.mjs";
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isSecurityReviewAgentForLane,
|
|
22
|
+
resolveAgentClosureRoleKeys,
|
|
23
|
+
resolveWaveRoleBindings,
|
|
24
|
+
} from "./role-helpers.mjs";
|
|
19
25
|
import { summarizeResolvedSkills } from "./skills.mjs";
|
|
20
26
|
|
|
21
27
|
function failureResultFromGate(gate, fallbackLogPath) {
|
|
@@ -57,6 +63,64 @@ function recordClosureGateFailure({
|
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
function isForwardableClosureGap(gate) {
|
|
67
|
+
return gate?.statusCode === "wave-proof-gap";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function forwardedClosureGapRecord({
|
|
71
|
+
stage,
|
|
72
|
+
wave,
|
|
73
|
+
lanePaths,
|
|
74
|
+
gate,
|
|
75
|
+
attempt,
|
|
76
|
+
targetAgentIds = [],
|
|
77
|
+
}) {
|
|
78
|
+
return {
|
|
79
|
+
id: `wave-${wave.wave}-closure-gap-${stage.key}-${gate.agentId}-attempt-${attempt || 1}`,
|
|
80
|
+
kind: "blocker",
|
|
81
|
+
lane: lanePaths.lane,
|
|
82
|
+
wave: wave.wave,
|
|
83
|
+
agentId: gate.agentId,
|
|
84
|
+
status: "open",
|
|
85
|
+
priority: "high",
|
|
86
|
+
blocking: true,
|
|
87
|
+
blockerSeverity: "closure-critical",
|
|
88
|
+
summary: `${stage.label} reported a proof gap and was forwarded to later closure stages.`,
|
|
89
|
+
detail: gate.detail,
|
|
90
|
+
artifactRefs: gate.logPath ? [gate.logPath] : [],
|
|
91
|
+
targets: targetAgentIds.map((agentId) => `agent:${agentId}`),
|
|
92
|
+
attempt: attempt || 1,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stageRequiresRun(stage, wave, lanePaths) {
|
|
97
|
+
switch (stage.key) {
|
|
98
|
+
case "integration":
|
|
99
|
+
case "documentation":
|
|
100
|
+
case "cont-qa":
|
|
101
|
+
return true;
|
|
102
|
+
case "cont-eval":
|
|
103
|
+
return Array.isArray(wave?.agents) && wave.agents.some((agent) => agent?.agentId === stage.agentId);
|
|
104
|
+
case "security-review":
|
|
105
|
+
return (
|
|
106
|
+
Array.isArray(wave?.agents) &&
|
|
107
|
+
wave.agents.some((agent) => isSecurityReviewAgentForLane(agent, lanePaths))
|
|
108
|
+
);
|
|
109
|
+
default:
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function missingClosureRunGate(stage) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
agentId: stage.agentId,
|
|
118
|
+
statusCode: "missing-closure-run",
|
|
119
|
+
detail: `${stage.label} is required for this wave but no matching closure run was provided.`,
|
|
120
|
+
logPath: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
60
124
|
export async function runClosureSweepPhase({
|
|
61
125
|
lanePaths,
|
|
62
126
|
wave,
|
|
@@ -113,10 +177,24 @@ export async function runClosureSweepPhase({
|
|
|
113
177
|
? readWaveContQaGateFn
|
|
114
178
|
: readWaveContQaGateDefault;
|
|
115
179
|
const stagedRuns = planClosureStages({ lanePaths, wave, closureRuns });
|
|
180
|
+
const forwardedFailures = [];
|
|
116
181
|
const { contQaAgentId, contEvalAgentId, integrationAgentId, documentationAgentId } =
|
|
117
182
|
resolveWaveRoleBindings(wave, lanePaths);
|
|
118
|
-
for (const stage of stagedRuns) {
|
|
183
|
+
for (const [stageIndex, stage] of stagedRuns.entries()) {
|
|
119
184
|
if (stage.runs.length === 0) {
|
|
185
|
+
if (stageRequiresRun(stage, wave, lanePaths)) {
|
|
186
|
+
const gate = missingClosureRunGate(stage);
|
|
187
|
+
recordClosureGateFailure({
|
|
188
|
+
wave,
|
|
189
|
+
lanePaths,
|
|
190
|
+
gate,
|
|
191
|
+
label: stage.label,
|
|
192
|
+
recordCombinedEvent,
|
|
193
|
+
appendCoordination,
|
|
194
|
+
actionRequested: stage.actionRequested,
|
|
195
|
+
});
|
|
196
|
+
return failureResultFromGate(gate, null);
|
|
197
|
+
}
|
|
120
198
|
continue;
|
|
121
199
|
}
|
|
122
200
|
for (const runInfo of stage.runs) {
|
|
@@ -138,6 +216,7 @@ export async function runClosureSweepPhase({
|
|
|
138
216
|
promptPath: runInfo.promptPath,
|
|
139
217
|
logPath: runInfo.logPath,
|
|
140
218
|
statusPath: runInfo.statusPath,
|
|
219
|
+
runtimePath: runInfo.runtimePath,
|
|
141
220
|
messageBoardPath: runInfo.messageBoardPath,
|
|
142
221
|
messageBoardSnapshot: runInfo.messageBoardSnapshot || "",
|
|
143
222
|
sharedSummaryPath: runInfo.sharedSummaryPath,
|
|
@@ -157,19 +236,18 @@ export async function runClosureSweepPhase({
|
|
|
157
236
|
attempt: dashboardState?.attempt || 1,
|
|
158
237
|
},
|
|
159
238
|
});
|
|
160
|
-
runInfo
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
launchResult?.skills || summarizeResolvedSkills(runInfo.agent.skillsResolved);
|
|
239
|
+
applyLaunchResultToRun(runInfo, launchResult, {
|
|
240
|
+
attempt: dashboardState?.attempt || null,
|
|
241
|
+
fallbackExecutorId: runInfo.agent.executorResolved?.id || null,
|
|
242
|
+
fallbackSkills: summarizeResolvedSkills(runInfo.agent.skillsResolved),
|
|
243
|
+
});
|
|
166
244
|
setWaveDashboardAgent(dashboardState, runInfo.agent.agentId, {
|
|
167
245
|
state: "running",
|
|
168
246
|
detail: `Closure sweep launched${launchResult?.context7?.mode ? ` (${launchResult.context7.mode})` : ""}`,
|
|
169
247
|
});
|
|
170
248
|
recordCombinedEvent({
|
|
171
249
|
agentId: runInfo.agent.agentId,
|
|
172
|
-
message: `Closure sweep launched
|
|
250
|
+
message: `Closure sweep launched via ${launchResult?.sessionBackend || "process"} backend`,
|
|
173
251
|
});
|
|
174
252
|
flushDashboards();
|
|
175
253
|
const result = await waitForWaveCompletionFn(
|
|
@@ -228,6 +306,43 @@ export async function runClosureSweepPhase({
|
|
|
228
306
|
contQaAgentId,
|
|
229
307
|
});
|
|
230
308
|
if (!gate.ok) {
|
|
309
|
+
if (isForwardableClosureGap(gate)) {
|
|
310
|
+
const targetAgentIds = stagedRuns
|
|
311
|
+
.slice(stageIndex + 1)
|
|
312
|
+
.flatMap((candidate) => candidate.runs.map((run) => run.agent.agentId))
|
|
313
|
+
.filter(Boolean);
|
|
314
|
+
forwardedFailures.push({
|
|
315
|
+
agentId: gate.agentId,
|
|
316
|
+
statusCode: gate.statusCode,
|
|
317
|
+
logPath: gate.logPath || (stage.runs[0]?.logPath ? path.relative(REPO_ROOT, stage.runs[0].logPath) : null),
|
|
318
|
+
detail: gate.detail,
|
|
319
|
+
});
|
|
320
|
+
appendCoordinationRecord(
|
|
321
|
+
coordinationLogPath,
|
|
322
|
+
forwardedClosureGapRecord({
|
|
323
|
+
stage,
|
|
324
|
+
wave,
|
|
325
|
+
lanePaths,
|
|
326
|
+
gate,
|
|
327
|
+
attempt: dashboardState?.attempt || 1,
|
|
328
|
+
targetAgentIds,
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
recordCombinedEvent({
|
|
332
|
+
level: "warn",
|
|
333
|
+
agentId: gate.agentId,
|
|
334
|
+
message: `${stage.label} reported a proof gap; continuing later closure stages with the gap as input.`,
|
|
335
|
+
});
|
|
336
|
+
appendCoordination({
|
|
337
|
+
event: "closure_gap_forwarded",
|
|
338
|
+
waves: [wave.wave],
|
|
339
|
+
status: "blocked",
|
|
340
|
+
details: `agent=${gate.agentId}; reason=${gate.statusCode}; ${gate.detail}`,
|
|
341
|
+
actionRequested: `Lane ${lanePaths.lane} owners should resolve the forwarded closure proof gap after downstream closure evidence is collected.`,
|
|
342
|
+
});
|
|
343
|
+
refreshDerivedState?.(dashboardState?.attempt || 0);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
231
346
|
recordClosureGateFailure({
|
|
232
347
|
wave,
|
|
233
348
|
lanePaths,
|
|
@@ -243,18 +358,21 @@ export async function runClosureSweepPhase({
|
|
|
243
358
|
);
|
|
244
359
|
}
|
|
245
360
|
}
|
|
246
|
-
return { failures:
|
|
361
|
+
return { failures: forwardedFailures, timedOut: false };
|
|
247
362
|
}
|
|
248
363
|
|
|
249
364
|
export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
365
|
+
const roleBindings = resolveWaveRoleBindings(wave, lanePaths);
|
|
250
366
|
const { contQaAgentId, contEvalAgentId, integrationAgentId, documentationAgentId } =
|
|
251
|
-
|
|
367
|
+
roleBindings;
|
|
368
|
+
const runHasRole = (run, roleKey) =>
|
|
369
|
+
resolveAgentClosureRoleKeys(run.agent, roleBindings, lanePaths).includes(roleKey);
|
|
252
370
|
return [
|
|
253
371
|
{
|
|
254
372
|
key: "cont-eval",
|
|
255
373
|
agentId: contEvalAgentId,
|
|
256
374
|
label: "cont-EVAL gate",
|
|
257
|
-
runs: closureRuns.filter((run) => run
|
|
375
|
+
runs: closureRuns.filter((run) => runHasRole(run, "cont-eval")),
|
|
258
376
|
actionRequested:
|
|
259
377
|
`Lane ${lanePaths.lane} owners should resolve cont-EVAL tuning gaps before integration closure.`,
|
|
260
378
|
},
|
|
@@ -262,7 +380,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
262
380
|
key: "security-review",
|
|
263
381
|
agentId: "security",
|
|
264
382
|
label: "Security review",
|
|
265
|
-
runs: closureRuns.filter((run) =>
|
|
383
|
+
runs: closureRuns.filter((run) => runHasRole(run, "security-review")),
|
|
266
384
|
actionRequested:
|
|
267
385
|
`Lane ${lanePaths.lane} owners should resolve blocked security findings or missing approvals before integration closure.`,
|
|
268
386
|
},
|
|
@@ -270,7 +388,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
270
388
|
key: "integration",
|
|
271
389
|
agentId: integrationAgentId,
|
|
272
390
|
label: "Integration gate",
|
|
273
|
-
runs: closureRuns.filter((run) => run
|
|
391
|
+
runs: closureRuns.filter((run) => runHasRole(run, "integration")),
|
|
274
392
|
actionRequested:
|
|
275
393
|
`Lane ${lanePaths.lane} owners should resolve integration contradictions or blockers before documentation and cont-QA closure.`,
|
|
276
394
|
},
|
|
@@ -278,7 +396,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
278
396
|
key: "documentation",
|
|
279
397
|
agentId: documentationAgentId,
|
|
280
398
|
label: "Documentation closure",
|
|
281
|
-
runs: closureRuns.filter((run) => run
|
|
399
|
+
runs: closureRuns.filter((run) => runHasRole(run, "documentation")),
|
|
282
400
|
actionRequested:
|
|
283
401
|
`Lane ${lanePaths.lane} owners should resolve the shared-plan or component-matrix closure state before cont-QA progression.`,
|
|
284
402
|
},
|
|
@@ -286,7 +404,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
286
404
|
key: "cont-qa",
|
|
287
405
|
agentId: contQaAgentId,
|
|
288
406
|
label: "cont-QA gate",
|
|
289
|
-
runs: closureRuns.filter((run) => run
|
|
407
|
+
runs: closureRuns.filter((run) => runHasRole(run, "cont-qa")),
|
|
290
408
|
actionRequested:
|
|
291
409
|
`Lane ${lanePaths.lane} owners should resolve the cont-QA gate before wave progression.`,
|
|
292
410
|
},
|
|
@@ -320,7 +438,10 @@ function evaluateClosureStage({
|
|
|
320
438
|
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
321
439
|
});
|
|
322
440
|
case "security-review":
|
|
323
|
-
return readWaveSecurityGateFn(wave, closureRuns, {
|
|
441
|
+
return readWaveSecurityGateFn(wave, closureRuns, {
|
|
442
|
+
mode: "live",
|
|
443
|
+
securityRolePromptPath: lanePaths?.securityRolePromptPath,
|
|
444
|
+
});
|
|
324
445
|
case "integration":
|
|
325
446
|
return readWaveIntegrationBarrierFn(
|
|
326
447
|
wave,
|
|
@@ -42,11 +42,12 @@ import {
|
|
|
42
42
|
import { readWaveRelaunchPlanSnapshot, readWaveRetryOverride, resolveRetryOverrideAgentIds, writeWaveRetryOverride, clearWaveRetryOverride } from "./retry-control.mjs";
|
|
43
43
|
import { flushWaveControlQueue, readWaveControlQueueState } from "./wave-control-client.mjs";
|
|
44
44
|
import { readAgentExecutionSummary, validateImplementationSummary } from "./agent-state.mjs";
|
|
45
|
-
import { isContEvalReportOnlyAgent,
|
|
45
|
+
import { isContEvalReportOnlyAgent, isSecurityReviewAgentForLane } from "./role-helpers.mjs";
|
|
46
46
|
import {
|
|
47
47
|
buildSignalStatusLine,
|
|
48
48
|
syncWaveSignalProjections,
|
|
49
49
|
} from "./signals.mjs";
|
|
50
|
+
import { summarizeSupervisorStateForWave } from "./supervisor-cli.mjs";
|
|
50
51
|
|
|
51
52
|
function printUsage() {
|
|
52
53
|
console.log(`Usage:
|
|
@@ -366,7 +367,8 @@ function buildLogicalAgents({
|
|
|
366
367
|
proofRegistry || { entries: [] },
|
|
367
368
|
);
|
|
368
369
|
const proofValidation =
|
|
369
|
-
!
|
|
370
|
+
!isSecurityReviewAgentForLane(agent, lanePaths) &&
|
|
371
|
+
!isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId })
|
|
370
372
|
? validateImplementationSummary(agent, summary ? summary : null)
|
|
371
373
|
: { ok: statusRecord?.code === 0, statusCode: statusRecord?.code === 0 ? "pass" : "pending" };
|
|
372
374
|
const targetedTasks = tasks.filter(
|
|
@@ -385,7 +387,7 @@ function buildLogicalAgents({
|
|
|
385
387
|
const satisfiedByStatus =
|
|
386
388
|
statusRecord?.code === 0 &&
|
|
387
389
|
(proofValidation.ok ||
|
|
388
|
-
|
|
390
|
+
isSecurityReviewAgentForLane(agent, lanePaths) ||
|
|
389
391
|
isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId }));
|
|
390
392
|
let state = "planned";
|
|
391
393
|
let reason = "";
|
|
@@ -404,7 +406,7 @@ function buildLogicalAgents({
|
|
|
404
406
|
lanePaths.integrationAgentId || "A8",
|
|
405
407
|
lanePaths.documentationAgentId || "A9",
|
|
406
408
|
lanePaths.contQaAgentId || "A0",
|
|
407
|
-
].includes(agent.agentId) ||
|
|
409
|
+
].includes(agent.agentId) || isSecurityReviewAgentForLane(agent, lanePaths)
|
|
408
410
|
? "closed"
|
|
409
411
|
: "satisfied";
|
|
410
412
|
reason = "Completed wave preserves the latest satisfied agent state.";
|
|
@@ -425,7 +427,7 @@ function buildLogicalAgents({
|
|
|
425
427
|
lanePaths.integrationAgentId || "A8",
|
|
426
428
|
lanePaths.documentationAgentId || "A9",
|
|
427
429
|
lanePaths.contQaAgentId || "A0",
|
|
428
|
-
].includes(agent.agentId) ||
|
|
430
|
+
].includes(agent.agentId) || isSecurityReviewAgentForLane(agent, lanePaths)
|
|
429
431
|
? "closed"
|
|
430
432
|
: "satisfied";
|
|
431
433
|
reason = "Latest attempt satisfied current control-plane state.";
|
|
@@ -657,6 +659,12 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
657
659
|
const controlState = readWaveControlPlaneState(lanePaths, wave.wave);
|
|
658
660
|
const proofRegistry = readWaveProofRegistry(lanePaths, wave.wave) || { entries: [] };
|
|
659
661
|
const relaunchPlan = readWaveRelaunchPlanSnapshot(lanePaths, wave.wave);
|
|
662
|
+
const supervisor = summarizeSupervisorStateForWave(lanePaths, wave.wave, {
|
|
663
|
+
agentId,
|
|
664
|
+
});
|
|
665
|
+
const forwardedClosureGaps = Array.isArray(relaunchPlan?.forwardedClosureGaps)
|
|
666
|
+
? relaunchPlan.forwardedClosureGaps
|
|
667
|
+
: [];
|
|
660
668
|
const rerunRequest = controlState.activeRerunRequest
|
|
661
669
|
? {
|
|
662
670
|
...controlState.activeRerunRequest,
|
|
@@ -718,6 +726,8 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
|
|
|
718
726
|
selectionSource: selection.source,
|
|
719
727
|
rerunRequest,
|
|
720
728
|
relaunchPlan,
|
|
729
|
+
forwardedClosureGaps,
|
|
730
|
+
supervisor,
|
|
721
731
|
nextTimer: isCompletedPhase(phase) ? null : nextTaskDeadline(tasks),
|
|
722
732
|
activeAttempt: controlState.activeAttempt,
|
|
723
733
|
};
|
|
@@ -758,6 +768,33 @@ function printStatus(payload) {
|
|
|
758
768
|
console.log(buildSignalStatusLine(payload.signals.wave, payload));
|
|
759
769
|
}
|
|
760
770
|
console.log(`blocking=${blocking}`);
|
|
771
|
+
if (payload.supervisor) {
|
|
772
|
+
console.log(
|
|
773
|
+
`supervisor=${payload.supervisor.terminalDisposition || payload.supervisor.status} run_id=${payload.supervisor.runId} launcher_pid=${payload.supervisor.launcherPid || "none"}`,
|
|
774
|
+
);
|
|
775
|
+
if (payload.supervisor.sessionBackend || payload.supervisor.recoveryState || payload.supervisor.resumeAction) {
|
|
776
|
+
console.log(
|
|
777
|
+
`supervisor-backend=${payload.supervisor.sessionBackend || "unknown"} recovery=${payload.supervisor.recoveryState || "unknown"} resume=${payload.supervisor.resumeAction || "none"}`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
if ((payload.supervisor.agentRuntimeSummary || []).length > 0) {
|
|
781
|
+
console.log("supervisor-runtime:");
|
|
782
|
+
for (const record of payload.supervisor.agentRuntimeSummary) {
|
|
783
|
+
console.log(
|
|
784
|
+
`- ${record.agentId || "unknown"} ${record.terminalDisposition || "unknown"} pid=${record.pid || "none"} backend=${record.sessionBackend || "process"} attach=${record.attachMode || "log-tail"} heartbeat=${record.lastHeartbeatAt || "n/a"}`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if ((payload.forwardedClosureGaps || []).length > 0) {
|
|
790
|
+
console.log("forwarded-closure-gaps:");
|
|
791
|
+
for (const gap of payload.forwardedClosureGaps) {
|
|
792
|
+
const targets = Array.isArray(gap.targets) && gap.targets.length > 0 ? gap.targets.join(",") : "none";
|
|
793
|
+
console.log(
|
|
794
|
+
`- ${gap.stageKey} agent=${gap.agentId || "unknown"} attempt=${gap.attempt ?? "n/a"} targets=${targets}${gap.detail ? ` detail=${gap.detail}` : ""}`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
761
798
|
if (payload.nextTimer) {
|
|
762
799
|
console.log(`next-timer=${payload.nextTimer.kind} ${payload.nextTimer.taskId} at ${payload.nextTimer.at}`);
|
|
763
800
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { loadWaveConfig } from "./config.mjs";
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
formatAgeFromTimestamp,
|
|
15
14
|
formatElapsed,
|
|
16
15
|
pad,
|
|
16
|
+
readJsonOrNull,
|
|
17
17
|
sleep,
|
|
18
18
|
truncate,
|
|
19
19
|
} from "./shared.mjs";
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
createCurrentWaveDashboardTerminalEntry,
|
|
22
22
|
createGlobalDashboardTerminalEntry,
|
|
23
23
|
} from "./terminals.mjs";
|
|
24
|
+
import {
|
|
25
|
+
attachSession as attachTmuxSession,
|
|
26
|
+
hasSession as hasTmuxSession,
|
|
27
|
+
} from "./tmux-adapter.mjs";
|
|
24
28
|
|
|
25
29
|
const DASHBOARD_ATTACH_TARGETS = ["current", "global"];
|
|
26
30
|
|
|
@@ -78,30 +82,7 @@ export function parseDashboardArgs(argv) {
|
|
|
78
82
|
return { help: false, options };
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
function
|
|
82
|
-
const result = spawnSync("tmux", ["-L", socketName, "has-session", "-t", sessionName], {
|
|
83
|
-
cwd: REPO_ROOT,
|
|
84
|
-
encoding: "utf8",
|
|
85
|
-
env: { ...process.env, TMUX: "" },
|
|
86
|
-
});
|
|
87
|
-
if (result.error) {
|
|
88
|
-
throw new Error(`tmux session lookup failed: ${result.error.message}`);
|
|
89
|
-
}
|
|
90
|
-
if (result.status === 0) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
const combined = `${String(result.stderr || "").toLowerCase()}\n${String(result.stdout || "").toLowerCase()}`;
|
|
94
|
-
if (
|
|
95
|
-
combined.includes("can't find session") ||
|
|
96
|
-
combined.includes("no server running") ||
|
|
97
|
-
combined.includes("error connecting")
|
|
98
|
-
) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
throw new Error((result.stderr || result.stdout || "tmux has-session failed").trim());
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function attachDashboardSession(project, lane, target) {
|
|
85
|
+
async function attachDashboardSession(project, lane, target) {
|
|
105
86
|
const config = loadWaveConfig();
|
|
106
87
|
const lanePaths = buildLanePaths(lane, {
|
|
107
88
|
config,
|
|
@@ -111,25 +92,112 @@ function attachDashboardSession(project, lane, target) {
|
|
|
111
92
|
target === "global"
|
|
112
93
|
? createGlobalDashboardTerminalEntry(lanePaths, "current")
|
|
113
94
|
: createCurrentWaveDashboardTerminalEntry(lanePaths);
|
|
114
|
-
if (!
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
if (!await hasTmuxSession(lanePaths.tmuxSocketName, entry.sessionName, { allowMissingBinary: false })) {
|
|
96
|
+
const fallback = resolveDashboardAttachFallback(lanePaths, target);
|
|
97
|
+
if (fallback) {
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(buildMissingDashboardAttachError(lanePaths, target));
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await attachTmuxSession(lanePaths.tmuxSocketName, entry.sessionName);
|
|
104
|
+
return null;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.tmuxMissingSession) {
|
|
107
|
+
const fallback = resolveDashboardAttachFallback(lanePaths, target);
|
|
108
|
+
if (fallback) {
|
|
109
|
+
return fallback;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(buildMissingDashboardAttachError(lanePaths, target));
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildMissingDashboardAttachError(lanePaths, target) {
|
|
118
|
+
const dashboardsRel = path.relative(REPO_ROOT, path.dirname(lanePaths.globalDashboardPath));
|
|
119
|
+
return `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.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function waveDashboardPathForNumber(lanePaths, waveNumber) {
|
|
123
|
+
if (!Number.isFinite(Number(waveNumber))) {
|
|
124
|
+
return null;
|
|
119
125
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
const candidate = path.join(lanePaths.dashboardsDir, `wave-${Number(waveNumber)}.json`);
|
|
127
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function selectCurrentWaveFromGlobalDashboard(globalState) {
|
|
131
|
+
const waves = Array.isArray(globalState?.waves) ? globalState.waves : [];
|
|
132
|
+
const candidates = waves
|
|
133
|
+
.map((wave) => ({
|
|
134
|
+
waveNumber: Number.parseInt(String(wave?.wave ?? ""), 10),
|
|
135
|
+
status: String(wave?.status || "").trim().toLowerCase(),
|
|
136
|
+
updatedAt: Date.parse(
|
|
137
|
+
String(wave?.updatedAt || wave?.completedAt || wave?.startedAt || ""),
|
|
138
|
+
),
|
|
139
|
+
}))
|
|
140
|
+
.filter((entry) => Number.isFinite(entry.waveNumber));
|
|
141
|
+
if (candidates.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
candidates.sort((left, right) => {
|
|
145
|
+
const leftTerminal = TERMINAL_STATES.has(left.status);
|
|
146
|
+
const rightTerminal = TERMINAL_STATES.has(right.status);
|
|
147
|
+
if (leftTerminal !== rightTerminal) {
|
|
148
|
+
return leftTerminal ? 1 : -1;
|
|
149
|
+
}
|
|
150
|
+
const leftUpdatedAt = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
|
|
151
|
+
const rightUpdatedAt = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
|
|
152
|
+
if (leftUpdatedAt !== rightUpdatedAt) {
|
|
153
|
+
return rightUpdatedAt - leftUpdatedAt;
|
|
154
|
+
}
|
|
155
|
+
return right.waveNumber - left.waveNumber;
|
|
124
156
|
});
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
return candidates[0].waveNumber;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveDashboardAttachFallback(lanePaths, target) {
|
|
161
|
+
if (target === "global") {
|
|
162
|
+
return fs.existsSync(lanePaths.globalDashboardPath)
|
|
163
|
+
? { dashboardFile: lanePaths.globalDashboardPath }
|
|
164
|
+
: null;
|
|
127
165
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
const globalState = readJsonOrNull(lanePaths.globalDashboardPath);
|
|
167
|
+
const preferredWaveNumber = selectCurrentWaveFromGlobalDashboard(globalState);
|
|
168
|
+
const preferredWavePath = waveDashboardPathForNumber(lanePaths, preferredWaveNumber);
|
|
169
|
+
if (preferredWavePath) {
|
|
170
|
+
return { dashboardFile: preferredWavePath };
|
|
132
171
|
}
|
|
172
|
+
if (!fs.existsSync(lanePaths.dashboardsDir)) {
|
|
173
|
+
return fs.existsSync(lanePaths.globalDashboardPath)
|
|
174
|
+
? { dashboardFile: lanePaths.globalDashboardPath }
|
|
175
|
+
: null;
|
|
176
|
+
}
|
|
177
|
+
const candidates = fs.readdirSync(lanePaths.dashboardsDir, { withFileTypes: true })
|
|
178
|
+
.filter((entry) => entry.isFile())
|
|
179
|
+
.map((entry) => ({
|
|
180
|
+
filePath: path.join(lanePaths.dashboardsDir, entry.name),
|
|
181
|
+
match: entry.name.match(/^wave-(\d+)\.json$/),
|
|
182
|
+
}))
|
|
183
|
+
.filter((entry) => entry.match)
|
|
184
|
+
.map((entry) => ({
|
|
185
|
+
dashboardFile: entry.filePath,
|
|
186
|
+
waveNumber: Number.parseInt(entry.match[1], 10),
|
|
187
|
+
mtimeMs: fs.statSync(entry.filePath).mtimeMs,
|
|
188
|
+
}))
|
|
189
|
+
.sort((left, right) => {
|
|
190
|
+
if (left.mtimeMs !== right.mtimeMs) {
|
|
191
|
+
return right.mtimeMs - left.mtimeMs;
|
|
192
|
+
}
|
|
193
|
+
return right.waveNumber - left.waveNumber;
|
|
194
|
+
});
|
|
195
|
+
if (candidates.length > 0) {
|
|
196
|
+
return { dashboardFile: candidates[0].dashboardFile };
|
|
197
|
+
}
|
|
198
|
+
return fs.existsSync(lanePaths.globalDashboardPath)
|
|
199
|
+
? { dashboardFile: lanePaths.globalDashboardPath }
|
|
200
|
+
: null;
|
|
133
201
|
}
|
|
134
202
|
|
|
135
203
|
function readMessageBoardTail(messageBoardPath, maxLines = 24) {
|
|
@@ -460,7 +528,7 @@ Options:
|
|
|
460
528
|
--dashboard-file <path> Path to wave/global dashboard JSON
|
|
461
529
|
--message-board <path> Optional message board path override
|
|
462
530
|
--attach <current|global>
|
|
463
|
-
Attach to the stable
|
|
531
|
+
Attach to the stable dashboard session for the lane, or follow the last written dashboard file when no live session exists
|
|
464
532
|
--watch Refresh continuously
|
|
465
533
|
--refresh-ms <n> Refresh interval in ms (default: ${DEFAULT_REFRESH_MS})
|
|
466
534
|
`);
|
|
@@ -468,8 +536,12 @@ Options:
|
|
|
468
536
|
}
|
|
469
537
|
|
|
470
538
|
if (options.attach) {
|
|
471
|
-
attachDashboardSession(options.project, options.lane, options.attach);
|
|
472
|
-
|
|
539
|
+
const fallback = await attachDashboardSession(options.project, options.lane, options.attach);
|
|
540
|
+
if (!fallback) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
options.dashboardFile = fallback.dashboardFile;
|
|
544
|
+
options.watch = true;
|
|
473
545
|
}
|
|
474
546
|
|
|
475
547
|
let terminalStateReachedAt = null;
|
|
@@ -26,7 +26,7 @@ import { deriveWaveLedger, readWaveLedger } from "./ledger.mjs";
|
|
|
26
26
|
import { buildDocsQueue, readDocsQueue } from "./docs-queue.mjs";
|
|
27
27
|
import { parseStructuredSignalsFromLog } from "./dashboard-state.mjs";
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
isSecurityReviewAgentForLane,
|
|
30
30
|
resolveSecurityReviewReportPath,
|
|
31
31
|
isContEvalImplementationOwningAgent,
|
|
32
32
|
resolveWaveRoleBindings,
|
|
@@ -214,7 +214,9 @@ export function buildWaveSecuritySummary({
|
|
|
214
214
|
summariesByAgentId = {},
|
|
215
215
|
}) {
|
|
216
216
|
const createdAt = toIsoTimestamp();
|
|
217
|
-
const securityAgents = (wave.agents || []).filter((agent) =>
|
|
217
|
+
const securityAgents = (wave.agents || []).filter((agent) =>
|
|
218
|
+
isSecurityReviewAgentForLane(agent, lanePaths),
|
|
219
|
+
);
|
|
218
220
|
if (securityAgents.length === 0) {
|
|
219
221
|
return {
|
|
220
222
|
wave: wave.wave,
|
|
@@ -377,7 +379,7 @@ function buildIntegrationEvidence({
|
|
|
377
379
|
isContEvalImplementationOwningAgent(agent, {
|
|
378
380
|
contEvalAgentId: roleBindings.contEvalAgentId,
|
|
379
381
|
});
|
|
380
|
-
if (
|
|
382
|
+
if (isSecurityReviewAgentForLane(agent, lanePaths)) {
|
|
381
383
|
continue;
|
|
382
384
|
}
|
|
383
385
|
if (agent.agentId === roleBindings.contEvalAgentId) {
|
|
@@ -710,6 +712,7 @@ export function buildWaveDerivedState({
|
|
|
710
712
|
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
711
713
|
capabilityAssignments,
|
|
712
714
|
dependencySnapshot,
|
|
715
|
+
securityRolePromptPath: lanePaths.securityRolePromptPath,
|
|
713
716
|
});
|
|
714
717
|
const inboxDir = waveInboxDir(lanePaths, wave.wave);
|
|
715
718
|
const sharedSummary = compileSharedSummary({
|