@chllming/wave-orchestration 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +7 -7
  3. package/docs/README.md +3 -3
  4. package/docs/concepts/operating-modes.md +1 -1
  5. package/docs/guides/author-and-run-waves.md +1 -1
  6. package/docs/guides/planner.md +2 -2
  7. package/docs/guides/recommendations-0.9.15.md +83 -0
  8. package/docs/guides/sandboxed-environments.md +2 -2
  9. package/docs/guides/signal-wrappers.md +10 -0
  10. package/docs/plans/agent-first-closure-hardening.md +612 -0
  11. package/docs/plans/current-state.md +3 -3
  12. package/docs/plans/end-state-architecture.md +1 -1
  13. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  14. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  15. package/docs/plans/migration.md +75 -20
  16. package/docs/reference/cli-reference.md +34 -1
  17. package/docs/reference/coordination-and-closure.md +16 -1
  18. package/docs/reference/npmjs-token-publishing.md +3 -3
  19. package/docs/reference/package-publishing-flow.md +13 -11
  20. package/docs/reference/runtime-config/README.md +2 -2
  21. package/docs/reference/sample-waves.md +5 -5
  22. package/docs/reference/skills.md +1 -1
  23. package/docs/reference/wave-control.md +1 -1
  24. package/docs/roadmap.md +5 -3
  25. package/package.json +1 -1
  26. package/releases/manifest.json +35 -0
  27. package/scripts/wave-orchestrator/agent-state.mjs +221 -313
  28. package/scripts/wave-orchestrator/artifact-schemas.mjs +37 -2
  29. package/scripts/wave-orchestrator/closure-adjudicator.mjs +311 -0
  30. package/scripts/wave-orchestrator/control-cli.mjs +212 -18
  31. package/scripts/wave-orchestrator/dashboard-state.mjs +40 -0
  32. package/scripts/wave-orchestrator/derived-state-engine.mjs +3 -0
  33. package/scripts/wave-orchestrator/gate-engine.mjs +140 -3
  34. package/scripts/wave-orchestrator/install.mjs +1 -1
  35. package/scripts/wave-orchestrator/launcher.mjs +49 -10
  36. package/scripts/wave-orchestrator/signal-cli.mjs +271 -0
  37. package/scripts/wave-orchestrator/structured-signal-parser.mjs +499 -0
  38. package/scripts/wave-orchestrator/task-entity.mjs +13 -4
  39. package/scripts/wave.mjs +9 -0
@@ -0,0 +1,311 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { writeClosureAdjudication } from "./artifact-schemas.mjs";
4
+ import { REPO_ROOT, ensureDirectory } from "./shared.mjs";
5
+ import { parseStructuredSignalCandidate } from "./structured-signal-parser.mjs";
6
+ import { validateImplementationSummary } from "./agent-state.mjs";
7
+
8
+ function isOpenCoordinationStatus(status) {
9
+ return ["open", "acknowledged", "in_progress"].includes(String(status || "").trim().toLowerCase());
10
+ }
11
+
12
+ function blockingCoordinationForAgent(derivedState, agentId) {
13
+ const latestRecords = Array.isArray(derivedState?.coordinationState?.latestRecords)
14
+ ? derivedState.coordinationState.latestRecords
15
+ : [];
16
+ return latestRecords.some((record) => {
17
+ if (!isOpenCoordinationStatus(record?.status) || record?.blocking === false) {
18
+ return false;
19
+ }
20
+ if (record?.agentId === agentId) {
21
+ return true;
22
+ }
23
+ return (record?.targets || []).some((target) => String(target || "").trim() === `agent:${agentId}`);
24
+ });
25
+ }
26
+
27
+ function buildEvidence(summary, envelope, derivedState) {
28
+ return [
29
+ { kind: "exit-code", value: summary?.exitCode ?? null },
30
+ {
31
+ kind: "deliverables",
32
+ value: (summary?.deliverables || []).map((deliverable) => ({
33
+ path: deliverable.path,
34
+ exists: deliverable.exists === true,
35
+ })),
36
+ },
37
+ {
38
+ kind: "proof-artifacts",
39
+ value: (summary?.proofArtifacts || []).map((artifact) => ({
40
+ path: artifact.path,
41
+ exists: artifact.exists === true,
42
+ })),
43
+ },
44
+ {
45
+ kind: "envelope-role",
46
+ value: envelope?.role || null,
47
+ },
48
+ {
49
+ kind: "integration-summary",
50
+ value: derivedState?.integrationSummary?.recommendation || null,
51
+ },
52
+ ];
53
+ }
54
+
55
+ function synthesizedSignals(summary) {
56
+ const signals = [];
57
+ if (summary?.proof) {
58
+ signals.push(
59
+ `[wave-proof] completion=${summary.proof.completion} durability=${summary.proof.durability} proof=${summary.proof.proof} state=${summary.proof.state}${summary.proof.detail ? ` detail=${summary.proof.detail}` : ""}`,
60
+ );
61
+ }
62
+ if (summary?.docDelta) {
63
+ signals.push(
64
+ `[wave-doc-delta] state=${summary.docDelta.state}${(summary.docDelta.paths || []).length > 0 ? ` paths=${summary.docDelta.paths.join(",")}` : ""}${summary.docDelta.detail ? ` detail=${summary.docDelta.detail}` : ""}`,
65
+ );
66
+ }
67
+ for (const component of summary?.components || []) {
68
+ signals.push(
69
+ `[wave-component] component=${component.componentId} level=${component.level} state=${component.state}${component.detail ? ` detail=${component.detail}` : ""}`,
70
+ );
71
+ }
72
+ return signals;
73
+ }
74
+
75
+ function cleanText(value) {
76
+ return String(value || "").trim();
77
+ }
78
+
79
+ function cleanState(value) {
80
+ const normalized = cleanText(value).toLowerCase();
81
+ return normalized === "complete" ? "met" : normalized;
82
+ }
83
+
84
+ function proofLineFromRawValues(rawValues) {
85
+ const completion = cleanText(rawValues?.completion).toLowerCase();
86
+ const durability = cleanText(rawValues?.durability).toLowerCase();
87
+ const proof = cleanText(rawValues?.proof).toLowerCase();
88
+ const state = cleanState(rawValues?.state);
89
+ const line = `[wave-proof] completion=${completion} durability=${durability} proof=${proof} state=${state}`;
90
+ const candidate = parseStructuredSignalCandidate(line);
91
+ return candidate?.accepted ? candidate.normalizedLine : null;
92
+ }
93
+
94
+ function docDeltaLineFromRawValues(rawValues) {
95
+ const state = cleanText(rawValues?.state).toLowerCase();
96
+ const line = `[wave-doc-delta] state=${state}`;
97
+ const candidate = parseStructuredSignalCandidate(line);
98
+ return candidate?.accepted ? candidate.normalizedLine : null;
99
+ }
100
+
101
+ function componentLineFromRawValues(rawValues) {
102
+ const componentId = cleanText(rawValues?.component);
103
+ const level = cleanText(rawValues?.level).toLowerCase();
104
+ const state = cleanState(rawValues?.state);
105
+ const line = `[wave-component] component=${componentId} level=${level} state=${state}`;
106
+ const candidate = parseStructuredSignalCandidate(line);
107
+ return candidate?.accepted ? candidate.normalizedLine : null;
108
+ }
109
+
110
+ function rejectedStructuredSignalSamples(summary, key) {
111
+ const bucket = summary?.structuredSignalDiagnostics?.[key];
112
+ return Array.isArray(bucket?.rejectedSamples) ? bucket.rejectedSamples : [];
113
+ }
114
+
115
+ function recoverStructuredSignalLine(gate, summary, agentRun) {
116
+ if (gate?.statusCode === "invalid-wave-proof-format") {
117
+ for (const sample of rejectedStructuredSignalSamples(summary, "proof")) {
118
+ const line = proofLineFromRawValues(sample?.rawValues);
119
+ if (line) {
120
+ return line;
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ if (gate?.statusCode === "invalid-doc-delta-format") {
126
+ for (const sample of rejectedStructuredSignalSamples(summary, "docDelta")) {
127
+ const line = docDeltaLineFromRawValues(sample?.rawValues);
128
+ if (line) {
129
+ return line;
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+ if (gate?.statusCode === "invalid-wave-component-format") {
135
+ const ownedComponents = new Set(Array.isArray(agentRun?.agent?.components) ? agentRun.agent.components : []);
136
+ for (const sample of rejectedStructuredSignalSamples(summary, "component")) {
137
+ if (!ownedComponents.has(cleanText(sample?.componentId))) {
138
+ continue;
139
+ }
140
+ const line = componentLineFromRawValues(sample?.rawValues);
141
+ if (line) {
142
+ return line;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function recoverSummaryFromRejectedSignals(gate, summary, agentRun) {
151
+ const recoveredLine = recoverStructuredSignalLine(gate, summary, agentRun);
152
+ if (!recoveredLine) {
153
+ return null;
154
+ }
155
+ const candidate = parseStructuredSignalCandidate(recoveredLine);
156
+ if (!candidate?.accepted) {
157
+ return null;
158
+ }
159
+ const recoveredSummary = {
160
+ ...summary,
161
+ };
162
+ if (candidate.kind === "proof") {
163
+ recoveredSummary.proof = {
164
+ completion: candidate.rawValues.completion.toLowerCase(),
165
+ durability: candidate.rawValues.durability.toLowerCase(),
166
+ proof: candidate.rawValues.proof.toLowerCase(),
167
+ state: cleanState(candidate.rawValues.state),
168
+ detail: "",
169
+ };
170
+ } else if (candidate.kind === "docDelta") {
171
+ recoveredSummary.docDelta = {
172
+ state: candidate.rawValues.state.toLowerCase(),
173
+ paths: [],
174
+ detail: "",
175
+ };
176
+ } else if (candidate.kind === "component") {
177
+ const recoveredComponent = {
178
+ componentId: cleanText(candidate.rawValues.component),
179
+ level: cleanText(candidate.rawValues.level).toLowerCase(),
180
+ state: cleanState(candidate.rawValues.state),
181
+ detail: "",
182
+ };
183
+ const existingComponents = Array.isArray(summary?.components) ? summary.components : [];
184
+ recoveredSummary.components = [
185
+ ...existingComponents.filter((component) => component.componentId !== recoveredComponent.componentId),
186
+ recoveredComponent,
187
+ ];
188
+ }
189
+ return {
190
+ recoveredLine,
191
+ recoveredSummary,
192
+ recoveredValidation: validateImplementationSummary(agentRun?.agent || null, recoveredSummary),
193
+ };
194
+ }
195
+
196
+ export function closureAdjudicationPath(lanePaths, waveNumber, attempt, agentId) {
197
+ return path.join(
198
+ lanePaths.statusDir,
199
+ "..",
200
+ "closure",
201
+ `wave-${waveNumber}`,
202
+ `attempt-${attempt || 1}`,
203
+ `${agentId}.json`,
204
+ );
205
+ }
206
+
207
+ export function evaluateClosureAdjudication({
208
+ wave,
209
+ lanePaths,
210
+ gate,
211
+ summary,
212
+ derivedState,
213
+ agentRun,
214
+ envelope,
215
+ }) {
216
+ if (gate?.failureClass !== "transport-failure" || gate?.eligibleForAdjudication !== true) {
217
+ return {
218
+ status: "ambiguous",
219
+ reason: "not-eligible",
220
+ detail: "Closure failure is not eligible for deterministic adjudication.",
221
+ evidence: [],
222
+ synthesizedSignals: [],
223
+ };
224
+ }
225
+ if (summary?.proof?.state === "gap" || (summary?.gaps || []).length > 0) {
226
+ return {
227
+ status: "rework-required",
228
+ reason: "semantic-negative-signal",
229
+ detail: "Explicit negative semantic proof signals remain.",
230
+ evidence: buildEvidence(summary, envelope, derivedState),
231
+ synthesizedSignals: synthesizedSignals(summary),
232
+ };
233
+ }
234
+ if (blockingCoordinationForAgent(derivedState, agentRun?.agent?.agentId)) {
235
+ return {
236
+ status: "ambiguous",
237
+ reason: "blocking-coordination",
238
+ detail: "Blocking coordination owned by the same agent slice remains open.",
239
+ evidence: buildEvidence(summary, envelope, derivedState),
240
+ synthesizedSignals: synthesizedSignals(summary),
241
+ };
242
+ }
243
+ const recovered = recoverSummaryFromRejectedSignals(gate, summary, agentRun);
244
+ if (!recovered) {
245
+ return {
246
+ status: "rework-required",
247
+ reason: "reconstruction-failed",
248
+ detail: "Rejected marker text did not preserve enough safe contract data to reconstruct a canonical closure signal.",
249
+ evidence: buildEvidence(summary, envelope, derivedState),
250
+ synthesizedSignals: synthesizedSignals(summary),
251
+ };
252
+ }
253
+ if (!recovered.recoveredValidation?.ok) {
254
+ return {
255
+ status: "rework-required",
256
+ reason: "recovered-signal-failed-validation",
257
+ detail: recovered.recoveredValidation?.detail || "Recovered closure signal still does not satisfy the implementation exit contract.",
258
+ evidence: buildEvidence(summary, envelope, derivedState),
259
+ synthesizedSignals: [...synthesizedSignals(summary), recovered.recoveredLine],
260
+ };
261
+ }
262
+ return {
263
+ status: "pass",
264
+ reason: "recovered-canonical-signal",
265
+ detail: "Rejected marker text preserved enough contract data to recover a canonical closure signal that satisfies the implementation exit contract.",
266
+ evidence: buildEvidence(summary, envelope, derivedState),
267
+ synthesizedSignals: [...synthesizedSignals(summary), recovered.recoveredLine],
268
+ };
269
+ }
270
+
271
+ export function persistClosureAdjudication({
272
+ lanePaths,
273
+ waveNumber,
274
+ attempt,
275
+ agentId,
276
+ payload,
277
+ }) {
278
+ const filePath = closureAdjudicationPath(lanePaths, waveNumber, attempt, agentId);
279
+ ensureDirectory(path.dirname(filePath));
280
+ return {
281
+ filePath,
282
+ adjudication: writeClosureAdjudication(
283
+ filePath,
284
+ {
285
+ lane: lanePaths?.lane || null,
286
+ wave: waveNumber,
287
+ attempt,
288
+ agentId,
289
+ ...payload,
290
+ },
291
+ {
292
+ lane: lanePaths?.lane || null,
293
+ wave: waveNumber,
294
+ attempt,
295
+ agentId,
296
+ },
297
+ ),
298
+ };
299
+ }
300
+
301
+ export function readPersistedClosureAdjudication(filePath, defaults = {}) {
302
+ if (!filePath || !fs.existsSync(filePath)) {
303
+ return null;
304
+ }
305
+ const payload = JSON.parse(fs.readFileSync(filePath, "utf8"));
306
+ return {
307
+ ...payload,
308
+ filePath: path.isAbsolute(filePath) ? path.relative(REPO_ROOT, filePath) : filePath,
309
+ ...defaults,
310
+ };
311
+ }
@@ -39,9 +39,11 @@ import {
39
39
  registerWaveProofBundle,
40
40
  waveProofRegistryPath,
41
41
  } from "./proof-registry.mjs";
42
+ import { closureAdjudicationPath, evaluateClosureAdjudication } from "./closure-adjudicator.mjs";
42
43
  import { readWaveRelaunchPlanSnapshot, readWaveRetryOverride, resolveRetryOverrideAgentIds, writeWaveRetryOverride, clearWaveRetryOverride } from "./retry-control.mjs";
43
44
  import { flushWaveControlQueue, readWaveControlQueueState } from "./wave-control-client.mjs";
44
45
  import { readAgentExecutionSummary, validateImplementationSummary } from "./agent-state.mjs";
46
+ import { readClosureAdjudication } from "./artifact-schemas.mjs";
45
47
  import { isContEvalReportOnlyAgent, isSecurityReviewAgentForLane } from "./role-helpers.mjs";
46
48
  import {
47
49
  buildSignalStatusLine,
@@ -68,6 +70,8 @@ function printUsage() {
68
70
  wave control proof get --project <id> --lane <lane> --wave <n> [--agent <id>] [--id <bundle-id>] [--json]
69
71
  wave control proof supersede --project <id> --lane <lane> --wave <n> --id <bundle-id> --agent <id> --artifact <path> [--artifact <path> ...] [options]
70
72
  wave control proof revoke --project <id> --lane <lane> --wave <n> --id <bundle-id> [--operator <name>] [--detail <text>] [--json]
73
+
74
+ wave control adjudication get --project <id> --lane <lane> --wave <n> [--agent <id>] [--json]
71
75
  `);
72
76
  }
73
77
 
@@ -135,7 +139,7 @@ function parseArgs(argv) {
135
139
  ? 1
136
140
  : surface === "telemetry"
137
141
  ? 2
138
- : surface === "task" || surface === "rerun" || surface === "proof"
142
+ : surface === "task" || surface === "rerun" || surface === "proof" || surface === "adjudication"
139
143
  ? operation === "act"
140
144
  ? 3
141
145
  : 2
@@ -256,6 +260,42 @@ function ledgerPath(lanePaths, waveNumber) {
256
260
  return path.join(lanePaths.ledgerDir, `wave-${waveNumber}.json`);
257
261
  }
258
262
 
263
+ function readWaveClosureAdjudications(lanePaths, waveNumber, agentId = "") {
264
+ const samplePath = closureAdjudicationPath(lanePaths, waveNumber, 1, agentId || "sample");
265
+ const attemptsRoot = path.dirname(path.dirname(samplePath));
266
+ if (!fs.existsSync(attemptsRoot)) {
267
+ return [];
268
+ }
269
+ const adjudications = [];
270
+ for (const attemptEntry of fs.readdirSync(attemptsRoot, { withFileTypes: true })) {
271
+ if (!attemptEntry.isDirectory()) {
272
+ continue;
273
+ }
274
+ const attemptDir = path.join(attemptsRoot, attemptEntry.name);
275
+ for (const fileEntry of fs.readdirSync(attemptDir, { withFileTypes: true })) {
276
+ if (!fileEntry.isFile() || !fileEntry.name.endsWith(".json")) {
277
+ continue;
278
+ }
279
+ const filePath = path.join(attemptDir, fileEntry.name);
280
+ const payload = readClosureAdjudication(filePath, {
281
+ lane: lanePaths.lane,
282
+ wave: waveNumber,
283
+ });
284
+ if (!payload) {
285
+ continue;
286
+ }
287
+ if (agentId && payload.agentId !== agentId) {
288
+ continue;
289
+ }
290
+ adjudications.push({
291
+ ...payload,
292
+ filePath: path.relative(process.cwd(), filePath),
293
+ });
294
+ }
295
+ }
296
+ return adjudications.sort((left, right) => String(left.createdAt || "").localeCompare(String(right.createdAt || "")));
297
+ }
298
+
259
299
  function targetAgentId(target) {
260
300
  const value = String(target || "").trim();
261
301
  return value.startsWith("agent:") ? value.slice("agent:".length) : value;
@@ -342,6 +382,7 @@ function buildLogicalAgents({
342
382
  lanePaths,
343
383
  wave,
344
384
  tasks,
385
+ coordinationState,
345
386
  dependencySnapshot,
346
387
  capabilityAssignments,
347
388
  selection,
@@ -371,6 +412,36 @@ function buildLogicalAgents({
371
412
  !isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId })
372
413
  ? validateImplementationSummary(agent, summary ? summary : null)
373
414
  : { ok: statusRecord?.code === 0, statusCode: statusRecord?.code === 0 ? "pass" : "pending" };
415
+ const adjudicationPath = closureAdjudicationPath(
416
+ lanePaths,
417
+ wave.wave,
418
+ statusRecord?.attempt || 1,
419
+ agent.agentId,
420
+ );
421
+ const persistedAdjudication = readClosureAdjudication(adjudicationPath, {
422
+ lane: lanePaths.lane,
423
+ wave: wave.wave,
424
+ attempt: statusRecord?.attempt || 1,
425
+ agentId: agent.agentId,
426
+ });
427
+ const adjudication =
428
+ proofValidation.eligibleForAdjudication && !proofValidation.ok
429
+ ? persistedAdjudication ||
430
+ evaluateClosureAdjudication({
431
+ wave,
432
+ lanePaths,
433
+ gate: proofValidation,
434
+ summary,
435
+ derivedState: {
436
+ coordinationState,
437
+ },
438
+ agentRun: {
439
+ agent,
440
+ logPath,
441
+ },
442
+ envelope: null,
443
+ })
444
+ : null;
374
445
  const targetedTasks = tasks.filter(
375
446
  (task) =>
376
447
  task.ownerAgentId === agent.agentId ||
@@ -387,19 +458,29 @@ function buildLogicalAgents({
387
458
  const satisfiedByStatus =
388
459
  statusRecord?.code === 0 &&
389
460
  (proofValidation.ok ||
461
+ adjudication?.status === "pass" ||
390
462
  isSecurityReviewAgentForLane(agent, lanePaths) ||
391
463
  isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId }));
464
+ const executionState =
465
+ selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId)
466
+ ? "active"
467
+ : Number.isInteger(statusRecord?.code)
468
+ ? "settled"
469
+ : "pending";
392
470
  let state = "planned";
393
471
  let reason = "";
472
+ let closureState = "pending";
394
473
  if (selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId)) {
395
474
  state = "working";
396
475
  reason = selection?.detail || "Selected by the active launcher attempt.";
476
+ closureState = "evaluating";
397
477
  } else if (selectedAgentIds.has(agent.agentId)) {
398
478
  state = "needs-rerun";
399
479
  reason =
400
480
  selection?.source === "relaunch-plan"
401
481
  ? "Selected by the persisted relaunch plan."
402
482
  : "Selected by active rerun request.";
483
+ closureState = "failed";
403
484
  } else if (completedPhase && satisfiedByStatus) {
404
485
  state = [
405
486
  lanePaths.contEvalAgentId || "E0",
@@ -410,9 +491,11 @@ function buildLogicalAgents({
410
491
  ? "closed"
411
492
  : "satisfied";
412
493
  reason = "Completed wave preserves the latest satisfied agent state.";
494
+ closureState = "passed";
413
495
  } else if (targetedBlockingTasks.some((task) => task.state === "working")) {
414
496
  state = "working";
415
497
  reason = targetedBlockingTasks.find((task) => task.state === "working")?.title || "";
498
+ closureState = "evaluating";
416
499
  } else if (targetedBlockingTasks.length > 0 || helperAssignment || dependency) {
417
500
  state = "blocked";
418
501
  reason =
@@ -421,6 +504,7 @@ function buildLogicalAgents({
421
504
  helperAssignment?.summary ||
422
505
  dependency?.summary ||
423
506
  "";
507
+ closureState = "blocked";
424
508
  } else if (satisfiedByStatus) {
425
509
  state = [
426
510
  lanePaths.contEvalAgentId || "E0",
@@ -431,14 +515,26 @@ function buildLogicalAgents({
431
515
  ? "closed"
432
516
  : "satisfied";
433
517
  reason = "Latest attempt satisfied current control-plane state.";
518
+ closureState = "passed";
519
+ } else if (adjudication?.status === "ambiguous") {
520
+ state = "awaiting-adjudication";
521
+ reason = adjudication.detail || proofValidation.detail || "Closure transport is awaiting deterministic adjudication.";
522
+ closureState = "awaiting-adjudication";
523
+ } else if (adjudication?.status === "rework-required") {
524
+ state = "needs-rerun";
525
+ reason = adjudication.detail || proofValidation.detail || "Closure adjudication requires more work.";
526
+ closureState = "failed";
434
527
  } else if (Number.isInteger(statusRecord?.code) && statusRecord.code !== 0) {
435
528
  state = "needs-rerun";
436
529
  reason = `Latest attempt exited with code ${statusRecord.code}.`;
530
+ closureState = "failed";
437
531
  }
438
532
  return {
439
533
  agentId: agent.agentId,
440
534
  state,
441
535
  reason: reason || null,
536
+ executionState,
537
+ closureState,
442
538
  taskIds: targetedTasks.map((task) => task.taskId),
443
539
  selectedForRerun: selectedAgentIds.has(agent.agentId) && selection?.source !== "active-attempt",
444
540
  selectedForActiveAttempt: selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId),
@@ -453,6 +549,53 @@ function buildLogicalAgents({
453
549
  });
454
550
  }
455
551
 
552
+ function hasLiveSupervisorRuntime(supervisor) {
553
+ const runtimeSummary = Array.isArray(supervisor?.agentRuntimeSummary) ? supervisor.agentRuntimeSummary : [];
554
+ return runtimeSummary.some((runtime) => !["completed", "failed", "terminated"].includes(String(runtime?.terminalDisposition || "").trim().toLowerCase()));
555
+ }
556
+
557
+ function deriveExecutionState({ phase, activeAttempt, supervisor, logicalAgents }) {
558
+ if (activeAttempt || hasLiveSupervisorRuntime(supervisor)) {
559
+ return "active";
560
+ }
561
+ if (
562
+ isCompletedPhase(phase) ||
563
+ (Array.isArray(logicalAgents) && logicalAgents.some((agent) => agent.executionState === "settled" || agent.closureState === "passed"))
564
+ ) {
565
+ return "settled";
566
+ }
567
+ return "pending";
568
+ }
569
+
570
+ function deriveControllerState({ executionState, relaunchPlan, supervisor }) {
571
+ if (executionState === "active") {
572
+ return "active";
573
+ }
574
+ if (relaunchPlan?.selectedAgentIds?.length > 0) {
575
+ return "relaunch-planned";
576
+ }
577
+ if (["degraded", "failed"].includes(String(supervisor?.recoveryState || "").trim().toLowerCase())) {
578
+ return "stale";
579
+ }
580
+ return "idle";
581
+ }
582
+
583
+ function deriveClosureState({ executionState, blockingEdge, logicalAgents, relaunchPlan, phase }) {
584
+ if ((logicalAgents || []).some((agent) => agent.closureState === "awaiting-adjudication")) {
585
+ return "awaiting-adjudication";
586
+ }
587
+ if ((logicalAgents || []).some((agent) => agent.closureState === "failed") || blockingEdge || relaunchPlan?.selectedAgentIds?.length > 0) {
588
+ return "blocked";
589
+ }
590
+ if (isCompletedPhase(phase) || (logicalAgents || []).every((agent) => agent.closureState === "passed")) {
591
+ return "passed";
592
+ }
593
+ if (executionState === "active" || (logicalAgents || []).some((agent) => agent.closureState === "evaluating")) {
594
+ return "evaluating";
595
+ }
596
+ return "pending";
597
+ }
598
+
456
599
  function selectionTargetsAgent(agentId, selectionSet) {
457
600
  return Boolean(agentId) && selectionSet.has(agentId);
458
601
  }
@@ -682,11 +825,30 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
682
825
  rerunRequest,
683
826
  relaunchPlan,
684
827
  });
685
- return {
686
- lane: lanePaths.lane,
687
- wave: wave.wave,
828
+ const logicalAgents = buildLogicalAgents({
829
+ lanePaths,
830
+ wave,
831
+ tasks,
832
+ dependencySnapshot,
833
+ capabilityAssignments,
834
+ selection,
835
+ proofRegistry,
688
836
  phase,
689
- agentId: agentId || null,
837
+ coordinationState,
838
+ }).filter((agent) => !agentId || agent.agentId === agentId);
839
+ const executionState = deriveExecutionState({
840
+ phase,
841
+ activeAttempt: controlState.activeAttempt,
842
+ supervisor,
843
+ logicalAgents,
844
+ });
845
+ const controllerState = deriveControllerState({
846
+ executionState,
847
+ relaunchPlan,
848
+ supervisor,
849
+ });
850
+ const closureState = deriveClosureState({
851
+ executionState,
690
852
  blockingEdge: buildBlockingEdge({
691
853
  tasks,
692
854
  capabilityAssignments,
@@ -697,16 +859,30 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
697
859
  agentId,
698
860
  phase,
699
861
  }),
700
- logicalAgents: buildLogicalAgents({
701
- lanePaths,
702
- wave,
703
- tasks,
704
- dependencySnapshot,
705
- capabilityAssignments,
706
- selection,
707
- proofRegistry,
708
- phase,
709
- }).filter((agent) => !agentId || agent.agentId === agentId),
862
+ logicalAgents,
863
+ relaunchPlan,
864
+ phase,
865
+ });
866
+ const blockingEdge = buildBlockingEdge({
867
+ tasks,
868
+ capabilityAssignments,
869
+ dependencySnapshot,
870
+ activeAttempt: controlState.activeAttempt,
871
+ rerunRequest,
872
+ relaunchPlan,
873
+ agentId,
874
+ phase,
875
+ });
876
+ return {
877
+ lane: lanePaths.lane,
878
+ wave: wave.wave,
879
+ phase,
880
+ agentId: agentId || null,
881
+ executionState,
882
+ closureState,
883
+ controllerState,
884
+ blockingEdge,
885
+ logicalAgents,
710
886
  tasks,
711
887
  helperAssignments: (capabilityAssignments || []).filter(
712
888
  (assignment) => assignment.blocking && assignmentRelevantToAgent(assignment, agentId),
@@ -764,6 +940,9 @@ function printStatus(payload) {
764
940
  ? `${payload.blockingEdge.kind} ${payload.blockingEdge.id}: ${payload.blockingEdge.detail}`
765
941
  : "none";
766
942
  console.log(`lane=${payload.lane} wave=${payload.wave} phase=${payload.phase}`);
943
+ console.log(
944
+ `execution=${payload.executionState || "unknown"} closure=${payload.closureState || "unknown"} controller=${payload.controllerState || "unknown"}`,
945
+ );
767
946
  if (payload.signals?.wave) {
768
947
  console.log(buildSignalStatusLine(payload.signals.wave, payload));
769
948
  }
@@ -801,7 +980,9 @@ function printStatus(payload) {
801
980
  if (payload.logicalAgents.length > 0) {
802
981
  console.log("logical-agents:");
803
982
  for (const agent of payload.logicalAgents) {
804
- console.log(`- ${agent.agentId} ${agent.state}${agent.reason ? `: ${agent.reason}` : ""}`);
983
+ console.log(
984
+ `- ${agent.agentId} ${agent.state} execution=${agent.executionState || "unknown"} closure=${agent.closureState || "unknown"}${agent.reason ? `: ${agent.reason}` : ""}`,
985
+ );
805
986
  }
806
987
  }
807
988
  }
@@ -991,8 +1172,8 @@ export async function runControlCli(argv) {
991
1172
  printUsage();
992
1173
  return;
993
1174
  }
994
- if (surface !== "status" && !["telemetry", "task", "rerun", "proof"].includes(surface)) {
995
- throw new Error("Expected control surface: status | telemetry | task | rerun | proof");
1175
+ if (surface !== "status" && !["telemetry", "task", "rerun", "adjudication", "proof"].includes(surface)) {
1176
+ throw new Error("Expected control surface: status | telemetry | task | rerun | adjudication | proof");
996
1177
  }
997
1178
  if (options.runId) {
998
1179
  const context = resolveRunContext(options.runId, options.project, options.lane);
@@ -1291,6 +1472,19 @@ export async function runControlCli(argv) {
1291
1472
  throw new Error("Expected rerun operation: request | get | clear");
1292
1473
  }
1293
1474
 
1475
+ if (surface === "adjudication") {
1476
+ if (operation !== "get") {
1477
+ throw new Error("Expected adjudication operation: get");
1478
+ }
1479
+ const adjudications = readWaveClosureAdjudications(lanePaths, wave.wave, options.agent || "");
1480
+ console.log(JSON.stringify({
1481
+ lane: lanePaths.lane,
1482
+ wave: wave.wave,
1483
+ adjudications,
1484
+ }, null, 2));
1485
+ return;
1486
+ }
1487
+
1294
1488
  if (surface === "proof") {
1295
1489
  if (operation === "get") {
1296
1490
  const registry = readWaveProofRegistry(lanePaths, wave.wave);