@chllming/wave-orchestration 0.8.3 → 0.8.4

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 +19 -0
  2. package/README.md +47 -11
  3. package/docs/README.md +6 -2
  4. package/docs/concepts/what-is-a-wave.md +1 -1
  5. package/docs/plans/architecture-hardening-migration.md +8 -1
  6. package/docs/plans/current-state.md +15 -7
  7. package/docs/plans/end-state-architecture.md +82 -69
  8. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  9. package/docs/plans/migration.md +235 -62
  10. package/docs/plans/wave-orchestrator.md +37 -11
  11. package/docs/reference/cli-reference.md +34 -14
  12. package/docs/reference/coordination-and-closure.md +19 -6
  13. package/docs/reference/npmjs-trusted-publishing.md +5 -4
  14. package/docs/reference/sample-waves.md +4 -4
  15. package/package.json +1 -1
  16. package/releases/manifest.json +20 -0
  17. package/scripts/wave-orchestrator/agent-state.mjs +0 -491
  18. package/scripts/wave-orchestrator/autonomous.mjs +10 -6
  19. package/scripts/wave-orchestrator/{launcher-closure.mjs → closure-engine.mjs} +190 -74
  20. package/scripts/wave-orchestrator/{launcher-derived-state.mjs → derived-state-engine.mjs} +34 -146
  21. package/scripts/wave-orchestrator/{launcher-gates.mjs → gate-engine.mjs} +395 -139
  22. package/scripts/wave-orchestrator/human-input-resolution.mjs +14 -10
  23. package/scripts/wave-orchestrator/human-input-workflow.mjs +104 -0
  24. package/scripts/wave-orchestrator/implementation-engine.mjs +120 -0
  25. package/scripts/wave-orchestrator/launcher-runtime.mjs +5 -6
  26. package/scripts/wave-orchestrator/launcher.mjs +271 -724
  27. package/scripts/wave-orchestrator/projection-writer.mjs +256 -0
  28. package/scripts/wave-orchestrator/reconcile-format.mjs +32 -0
  29. package/scripts/wave-orchestrator/reducer-snapshot.mjs +297 -0
  30. package/scripts/wave-orchestrator/replay.mjs +3 -1
  31. package/scripts/wave-orchestrator/result-envelope.mjs +589 -0
  32. package/scripts/wave-orchestrator/retry-control.mjs +5 -0
  33. package/scripts/wave-orchestrator/{launcher-retry.mjs → retry-engine.mjs} +267 -18
  34. package/scripts/wave-orchestrator/role-helpers.mjs +51 -0
  35. package/scripts/wave-orchestrator/{launcher-supervisor.mjs → session-supervisor.mjs} +178 -103
  36. package/scripts/wave-orchestrator/shared.mjs +1 -0
  37. package/scripts/wave-orchestrator/traces.mjs +10 -1
  38. package/scripts/wave-orchestrator/wave-files.mjs +11 -9
  39. package/scripts/wave-orchestrator/wave-state-reducer.mjs +52 -5
@@ -14,9 +14,11 @@ import {
14
14
  import {
15
15
  readStatusCodeIfPresent,
16
16
  } from "./dashboard-state.mjs";
17
+ import { appendWaveControlEvent } from "./control-plane.mjs";
17
18
  import {
18
19
  REPO_ROOT,
19
20
  readJsonOrNull,
21
+ readStatusRecordIfPresent,
20
22
  ensureDirectory,
21
23
  shellQuote,
22
24
  PACKAGE_ROOT,
@@ -31,8 +33,8 @@ import {
31
33
  } from "./terminals.mjs";
32
34
  import {
33
35
  recordGlobalDashboardEvent,
34
- writeGlobalDashboard,
35
36
  } from "./dashboard-state.mjs";
37
+ import { buildHumanFeedbackWorkflowUpdate } from "./human-input-workflow.mjs";
36
38
  import {
37
39
  collectUnexpectedSessionFailures as collectUnexpectedSessionFailuresImpl,
38
40
  launchAgentSession as launchAgentSessionImpl,
@@ -51,6 +53,108 @@ function isProcessAlive(pid) {
51
53
  }
52
54
  }
53
55
 
56
+ function relativeArtifactPath(filePath) {
57
+ return filePath ? path.relative(REPO_ROOT, filePath) : null;
58
+ }
59
+
60
+ export function recordWaveRunState(lanePaths, waveNumber, state, data = {}) {
61
+ return appendWaveControlEvent(lanePaths, waveNumber, {
62
+ entityType: "wave_run",
63
+ entityId: `wave-${waveNumber}`,
64
+ action: state,
65
+ source: "session-supervisor",
66
+ actor: "session-supervisor",
67
+ data: {
68
+ waveId: `wave-${waveNumber}`,
69
+ waveNumber,
70
+ updatedAt: toIsoTimestamp(),
71
+ ...data,
72
+ },
73
+ });
74
+ }
75
+
76
+ export function recordAttemptState(lanePaths, waveNumber, attemptNumber, state, data = {}) {
77
+ return appendWaveControlEvent(lanePaths, waveNumber, {
78
+ entityType: "attempt",
79
+ entityId: `wave-${waveNumber}-attempt-${attemptNumber}`,
80
+ action: state,
81
+ source: "session-supervisor",
82
+ actor: "session-supervisor",
83
+ attempt: attemptNumber,
84
+ data: {
85
+ attemptId: `wave-${waveNumber}-attempt-${attemptNumber}`,
86
+ attemptNumber,
87
+ state,
88
+ selectedAgentIds: data.selectedAgentIds || [],
89
+ detail: data.detail || null,
90
+ updatedAt: toIsoTimestamp(),
91
+ ...(data.createdAt ? { createdAt: data.createdAt } : {}),
92
+ },
93
+ });
94
+ }
95
+
96
+ export function recordAgentRunStarted(lanePaths, { waveNumber, attempt, runInfo }) {
97
+ if (!runInfo?.agent?.agentId || !Number.isFinite(Number(waveNumber))) {
98
+ return null;
99
+ }
100
+ return appendWaveControlEvent(lanePaths, waveNumber, {
101
+ entityType: "agent_run",
102
+ entityId: `wave-${waveNumber}-attempt-${attempt}-agent-${runInfo.agent.agentId}`,
103
+ action: "started",
104
+ source: "session-supervisor",
105
+ actor: "session-supervisor",
106
+ attempt,
107
+ data: {
108
+ agentId: runInfo.agent.agentId,
109
+ attemptNumber: attempt,
110
+ sessionName: runInfo.sessionName || null,
111
+ executorId: runInfo.lastExecutorId || runInfo.agent.executorResolved?.id || null,
112
+ promptPath: relativeArtifactPath(runInfo.promptPath),
113
+ statusPath: relativeArtifactPath(runInfo.statusPath),
114
+ logPath: relativeArtifactPath(runInfo.logPath),
115
+ startedAt: toIsoTimestamp(),
116
+ },
117
+ });
118
+ }
119
+
120
+ export function recordAgentRunFinished(
121
+ lanePaths,
122
+ { waveNumber, attempt, runInfo, failure = null, statusRecord = null },
123
+ ) {
124
+ if (!runInfo?.agent?.agentId || !Number.isFinite(Number(waveNumber))) {
125
+ return null;
126
+ }
127
+ const effectiveStatusRecord = statusRecord || readStatusRecordIfPresent(runInfo.statusPath);
128
+ const timedOut =
129
+ failure?.statusCode === "timeout-no-status" || failure?.statusCode === "timed_out";
130
+ const action =
131
+ timedOut
132
+ ? "timed_out"
133
+ : Number(effectiveStatusRecord?.code) === 0
134
+ ? "completed"
135
+ : "failed";
136
+ return appendWaveControlEvent(lanePaths, waveNumber, {
137
+ entityType: "agent_run",
138
+ entityId: `wave-${waveNumber}-attempt-${attempt}-agent-${runInfo.agent.agentId}`,
139
+ action,
140
+ source: "session-supervisor",
141
+ actor: "session-supervisor",
142
+ attempt,
143
+ data: {
144
+ agentId: runInfo.agent.agentId,
145
+ attemptNumber: attempt,
146
+ exitCode: effectiveStatusRecord?.code ?? null,
147
+ completedAt: effectiveStatusRecord?.completedAt || toIsoTimestamp(),
148
+ promptHash: effectiveStatusRecord?.promptHash || runInfo.lastPromptHash || null,
149
+ executorId: runInfo.lastExecutorId || runInfo.agent.executorResolved?.id || null,
150
+ statusCode: failure?.statusCode || null,
151
+ detail: failure?.detail || null,
152
+ logPath: relativeArtifactPath(runInfo.logPath),
153
+ statusPath: relativeArtifactPath(runInfo.statusPath),
154
+ },
155
+ });
156
+ }
157
+
54
158
  export function markLauncherFailed(
55
159
  globalDashboard,
56
160
  lanePaths,
@@ -64,7 +168,6 @@ export function markLauncherFailed(
64
168
  level: "error",
65
169
  message: error instanceof Error ? error.message : String(error),
66
170
  });
67
- writeGlobalDashboard(lanePaths.globalDashboardPath, globalDashboard);
68
171
  }
69
172
  appendCoordination({
70
173
  event: "launcher_finish",
@@ -553,13 +656,46 @@ export function launchWaveDashboardSession(lanePaths, { sessionName, dashboardPa
553
656
  }
554
657
 
555
658
  export async function launchAgentSession(lanePaths, params) {
556
- return launchAgentSessionImpl(lanePaths, params, { runTmuxFn: runTmux });
659
+ const result = await launchAgentSessionImpl(lanePaths, params, { runTmuxFn: runTmux });
660
+ const controlPlane = params?.controlPlane || null;
661
+ if (!params?.dryRun && controlPlane?.waveNumber !== undefined && controlPlane?.attempt) {
662
+ recordAgentRunStarted(lanePaths, {
663
+ waveNumber: controlPlane.waveNumber,
664
+ attempt: controlPlane.attempt,
665
+ runInfo: {
666
+ ...params,
667
+ lastExecutorId: result?.executorId || params?.agent?.executorResolved?.id || null,
668
+ },
669
+ });
670
+ }
671
+ return result;
557
672
  }
558
673
 
559
- export async function waitForWaveCompletion(lanePaths, agentRuns, timeoutMinutes, onProgress = null) {
560
- return waitForWaveCompletionImpl(lanePaths, agentRuns, timeoutMinutes, onProgress, {
674
+ export async function waitForWaveCompletion(
675
+ lanePaths,
676
+ agentRuns,
677
+ timeoutMinutes,
678
+ onProgress = null,
679
+ options = {},
680
+ ) {
681
+ const result = await waitForWaveCompletionImpl(lanePaths, agentRuns, timeoutMinutes, onProgress, {
561
682
  collectUnexpectedSessionFailuresFn: collectUnexpectedSessionFailures,
562
683
  });
684
+ const controlPlane = options?.controlPlane || null;
685
+ if (controlPlane?.waveNumber !== undefined && controlPlane?.attempt) {
686
+ const failuresByAgentId = new Map(
687
+ (result?.failures || []).map((failure) => [failure.agentId, failure]),
688
+ );
689
+ for (const runInfo of agentRuns || []) {
690
+ recordAgentRunFinished(lanePaths, {
691
+ waveNumber: controlPlane.waveNumber,
692
+ attempt: controlPlane.attempt,
693
+ runInfo,
694
+ failure: failuresByAgentId.get(runInfo.agent.agentId) || null,
695
+ });
696
+ }
697
+ }
698
+ return result;
563
699
  }
564
700
 
565
701
  export function monitorWaveHumanFeedback({
@@ -592,111 +728,50 @@ export function monitorWaveHumanFeedback({
592
728
  const context = request.context ? `; context=${request.context}` : "";
593
729
  const responseOperator = request.responseOperator || "human-operator";
594
730
  const responseText = request.responseText || "(empty response)";
731
+ const escalationId = `escalation-${request.id}`;
732
+ const existingEscalation =
733
+ (fs.existsSync(triageLogPath)
734
+ ? readMaterializedCoordinationState(triageLogPath).byId.get(escalationId)
735
+ : null) ||
736
+ readMaterializedCoordinationState(coordinationLogPath).byId.get(escalationId) ||
737
+ null;
738
+ const workflowUpdate = buildHumanFeedbackWorkflowUpdate({
739
+ request,
740
+ lane: lanePaths.lane,
741
+ waveNumber,
742
+ existingEscalation,
743
+ });
595
744
  if (request.status === "pending") {
596
- recordCombinedEvent({
597
- level: "warn",
598
- agentId: request.agentId,
599
- message: `Human feedback requested (${request.id}): ${question}`,
600
- });
601
- console.warn(
602
- `[human-feedback] wave=${waveNumber} agent=${request.agentId} request=${request.id} pending: ${question}`,
603
- );
604
- console.warn(
605
- `[human-feedback] respond with: pnpm exec wave control task act answer --lane ${lanePaths.lane} --wave ${waveNumber} --id ${request.id} --response "<answer>" --operator "<name>"`,
606
- );
607
- appendCoordination({
608
- event: "human_feedback_requested",
609
- waves: [waveNumber],
610
- status: "waiting-human",
611
- details: `request_id=${request.id}; agent=${request.agentId}; question=${question}${context}`,
612
- actionRequested: `Launcher operator should ask or answer in the parent session, then run: pnpm exec wave control task act answer --lane ${lanePaths.lane} --wave ${waveNumber} --id ${request.id} --response "<answer>" --operator "<name>"`,
613
- });
745
+ if (workflowUpdate?.combinedEvent) {
746
+ recordCombinedEvent(workflowUpdate.combinedEvent);
747
+ }
748
+ for (const line of workflowUpdate?.consoleLines || []) {
749
+ console.warn(line);
750
+ }
751
+ if (workflowUpdate?.coordinationNotice) {
752
+ appendCoordination(workflowUpdate.coordinationNotice);
753
+ }
614
754
  if (coordinationLogPath) {
615
- appendCoordinationRecord(coordinationLogPath, {
616
- id: request.id,
617
- lane: lanePaths.lane,
618
- wave: waveNumber,
619
- agentId: request.agentId || "human",
620
- kind: "human-feedback",
621
- targets: request.agentId ? [`agent:${request.agentId}`] : [],
622
- priority: "high",
623
- summary: question,
624
- detail: request.context || "",
625
- status: "open",
626
- source: "feedback",
627
- });
755
+ for (const update of workflowUpdate?.coordinationUpdates || []) {
756
+ appendCoordinationRecord(coordinationLogPath, update);
757
+ }
628
758
  }
629
759
  } else if (request.status === "answered") {
630
- recordCombinedEvent({
631
- level: "info",
632
- agentId: request.agentId,
633
- message: `Human feedback answered (${request.id}) by ${responseOperator}: ${responseText}`,
634
- });
635
- appendCoordination({
636
- event: "human_feedback_answered",
637
- waves: [waveNumber],
638
- status: "resolved",
639
- details: `request_id=${request.id}; agent=${request.agentId}; operator=${responseOperator}; response=${responseText}`,
640
- });
760
+ if (workflowUpdate?.combinedEvent) {
761
+ recordCombinedEvent(workflowUpdate.combinedEvent);
762
+ }
763
+ if (workflowUpdate?.coordinationNotice) {
764
+ appendCoordination(workflowUpdate.coordinationNotice);
765
+ }
641
766
  if (coordinationLogPath) {
642
- const escalationId = `escalation-${request.id}`;
643
- const existingEscalation =
644
- (fs.existsSync(triageLogPath)
645
- ? readMaterializedCoordinationState(triageLogPath).byId.get(escalationId)
646
- : null) ||
647
- readMaterializedCoordinationState(coordinationLogPath).byId.get(escalationId) ||
648
- null;
649
767
  if (fs.existsSync(triageLogPath)) {
650
- appendCoordinationRecord(triageLogPath, {
651
- id: escalationId,
652
- lane: lanePaths.lane,
653
- wave: waveNumber,
654
- agentId: request.agentId || "human",
655
- kind: "human-escalation",
656
- targets:
657
- existingEscalation?.targets ||
658
- (request.agentId ? [`agent:${request.agentId}`] : []),
659
- dependsOn: existingEscalation?.dependsOn || [],
660
- closureCondition: existingEscalation?.closureCondition || "",
661
- priority: "high",
662
- summary: question,
663
- detail: responseText,
664
- artifactRefs: [request.id],
665
- status: "resolved",
666
- source: "feedback",
667
- });
768
+ for (const update of workflowUpdate?.triageUpdates || []) {
769
+ appendCoordinationRecord(triageLogPath, update);
770
+ }
771
+ }
772
+ for (const update of workflowUpdate?.coordinationUpdates || []) {
773
+ appendCoordinationRecord(coordinationLogPath, update);
668
774
  }
669
- appendCoordinationRecord(coordinationLogPath, {
670
- id: escalationId,
671
- lane: lanePaths.lane,
672
- wave: waveNumber,
673
- agentId: request.agentId || "human",
674
- kind: "human-escalation",
675
- targets:
676
- existingEscalation?.targets ||
677
- (request.agentId ? [`agent:${request.agentId}`] : []),
678
- dependsOn: existingEscalation?.dependsOn || [],
679
- closureCondition: existingEscalation?.closureCondition || "",
680
- priority: "high",
681
- summary: question,
682
- detail: responseText,
683
- artifactRefs: [request.id],
684
- status: "resolved",
685
- source: "feedback",
686
- });
687
- appendCoordinationRecord(coordinationLogPath, {
688
- id: request.id,
689
- lane: lanePaths.lane,
690
- wave: waveNumber,
691
- agentId: request.agentId || "human",
692
- kind: "human-feedback",
693
- targets: request.agentId ? [`agent:${request.agentId}`] : [],
694
- priority: "high",
695
- summary: question,
696
- detail: responseText,
697
- status: "resolved",
698
- source: "feedback",
699
- });
700
775
  }
701
776
  }
702
777
  }
@@ -201,6 +201,7 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
201
201
  inboxesDir: path.join(stateDir, "inboxes"),
202
202
  ledgerDir: path.join(stateDir, "ledger"),
203
203
  integrationDir: path.join(stateDir, "integration"),
204
+ resultsDir: path.join(stateDir, "results"),
204
205
  proofDir: path.join(stateDir, "proof"),
205
206
  securityDir: path.join(stateDir, "security"),
206
207
  dependencySnapshotsDir: path.join(stateDir, "dependencies"),
@@ -55,6 +55,9 @@ function fileHashOrNull(filePath) {
55
55
  if (!filePath || !fs.existsSync(filePath)) {
56
56
  return null;
57
57
  }
58
+ if (!fs.statSync(filePath).isFile()) {
59
+ return null;
60
+ }
58
61
  return hashText(fs.readFileSync(filePath, "utf8"));
59
62
  }
60
63
 
@@ -62,6 +65,9 @@ function copyFileIfExists(sourcePath, destPath) {
62
65
  if (!sourcePath || !fs.existsSync(sourcePath)) {
63
66
  return false;
64
67
  }
68
+ if (!fs.statSync(sourcePath).isFile()) {
69
+ return false;
70
+ }
65
71
  ensureDirectory(path.dirname(destPath));
66
72
  fs.copyFileSync(sourcePath, destPath);
67
73
  return true;
@@ -603,7 +609,10 @@ function normalizeGateLogPath(gate, agentArtifacts) {
603
609
  }
604
610
  return {
605
611
  ...gate,
606
- logPath: artifact.path,
612
+ // Log artifacts are already described in the bundle manifest. Nulling the
613
+ // inline path keeps replay parity focused on gate semantics instead of the
614
+ // copied artifact layout.
615
+ logPath: null,
607
616
  };
608
617
  }
609
618
 
@@ -1253,7 +1253,11 @@ export function loadComponentCutoverMatrix(options = {}) {
1253
1253
  const matrixJsonPath =
1254
1254
  options.componentMatrixJsonPath ||
1255
1255
  (laneProfile
1256
- ? path.resolve(REPO_ROOT, laneProfile.paths.componentCutoverMatrixJsonPath)
1256
+ ? path.resolve(
1257
+ REPO_ROOT,
1258
+ laneProfile?.paths?.componentCutoverMatrixJsonPath ||
1259
+ "trace-bundle/component-cutover-matrix.json",
1260
+ )
1257
1261
  : "trace-bundle/component-cutover-matrix.json");
1258
1262
  const payload =
1259
1263
  options.componentMatrixPayload !== undefined
@@ -1366,8 +1370,8 @@ export function requiredDocumentationStewardPathsForWave(waveNumber, options = {
1366
1370
  const componentThreshold = laneProfile.validation.requireComponentPromotionsFromWave;
1367
1371
  if (componentThreshold !== null && waveNumber >= componentThreshold) {
1368
1372
  out.push(
1369
- laneProfile.paths.componentCutoverMatrixDocPath,
1370
- laneProfile.paths.componentCutoverMatrixJsonPath,
1373
+ laneProfile?.paths?.componentCutoverMatrixDocPath,
1374
+ laneProfile?.paths?.componentCutoverMatrixJsonPath,
1371
1375
  );
1372
1376
  }
1373
1377
  return Array.from(new Set(out));
@@ -2353,18 +2357,16 @@ export function validateWaveComponentMatrixCurrentLevels(wave, options = {}) {
2353
2357
  securityRolePromptPath,
2354
2358
  }),
2355
2359
  );
2356
- if (
2357
- promotions.length === 0 &&
2358
- ((componentThreshold === null || wave.wave < componentThreshold) ||
2359
- implementationOwningAgents.length === 0)
2360
- ) {
2360
+ if (promotions.length === 0) {
2361
2361
  return {
2362
2362
  ok: true,
2363
2363
  statusCode: "pass",
2364
2364
  detail:
2365
2365
  implementationOwningAgents.length === 0
2366
2366
  ? `Wave ${wave.wave} has no implementation-owned component promotions to reconcile.`
2367
- : "Component current-level gate is not active for this wave.",
2367
+ : componentThreshold === null || wave.wave < componentThreshold
2368
+ ? "Component current-level gate is not active for this wave."
2369
+ : `Wave ${wave.wave} declares no promoted components to reconcile against the component matrix.`,
2368
2370
  componentId: null,
2369
2371
  };
2370
2372
  }
@@ -16,12 +16,27 @@ import {
16
16
  buildGateSnapshotPure,
17
17
  readClarificationBarrier,
18
18
  readWaveAssignmentBarrier,
19
- } from "./launcher-gates.mjs";
19
+ } from "./gate-engine.mjs";
20
20
  import { buildHumanInputRequests } from "./human-input-workflow.mjs";
21
+ import { projectLegacySummaryFromEnvelope } from "./result-envelope.mjs";
21
22
  import { buildRequestAssignments } from "./routing-state.mjs";
23
+ import { resolveSecurityReviewReportPath } from "./role-helpers.mjs";
22
24
 
23
25
  const REDUCER_VERSION = 2;
24
26
 
27
+ function resolveReducerReportPath(waveDefinition, agent) {
28
+ if (!waveDefinition || !agent) {
29
+ return null;
30
+ }
31
+ if (agent.agentId === (waveDefinition.contQaAgentId || "A0")) {
32
+ return waveDefinition.contQaReportPath || null;
33
+ }
34
+ if (agent.agentId === (waveDefinition.contEvalAgentId || "E0")) {
35
+ return waveDefinition.contEvalReportPath || null;
36
+ }
37
+ return resolveSecurityReviewReportPath(agent);
38
+ }
39
+
25
40
  /**
26
41
  * Detect contradictions from control-plane events.
27
42
  * Returns a Map<contradictionId, contradiction>.
@@ -117,6 +132,7 @@ function derivePhase({
117
132
  gateSnapshot,
118
133
  coordinationState,
119
134
  dependencySnapshot,
135
+ clarificationBarrier,
120
136
  }) {
121
137
  const blockers = (coordinationState?.blockers || []).filter(
122
138
  (record) =>
@@ -134,6 +150,9 @@ function derivePhase({
134
150
  if (openClarifications.length > 0 || openClarificationRequests.length > 0) {
135
151
  return "clarifying";
136
152
  }
153
+ if (clarificationBarrier?.statusCode === "human-feedback-open") {
154
+ return "clarifying";
155
+ }
137
156
 
138
157
  const dependencyBlockers =
139
158
  (dependencySnapshot?.requiredInbound || []).length +
@@ -528,12 +547,34 @@ function applyProofAvailabilityToTasks(tasks, proofAvailability) {
528
547
  export function reduceWaveState({
529
548
  controlPlaneEvents = [],
530
549
  coordinationRecords = [],
531
- agentResults = {},
550
+ agentEnvelopes = null,
551
+ agentResults = null,
532
552
  waveDefinition = null,
533
553
  dependencyTickets = null,
534
554
  feedbackRequests = [],
535
555
  laneConfig = {},
536
556
  }) {
557
+ const resolvedAgentResults =
558
+ agentResults && typeof agentResults === "object" && !Array.isArray(agentResults)
559
+ ? agentResults
560
+ : Object.fromEntries(
561
+ Object.entries(agentEnvelopes || {})
562
+ .map(([agentId, envelope]) => {
563
+ const agent =
564
+ (Array.isArray(waveDefinition?.agents) ? waveDefinition.agents : []).find(
565
+ (candidate) => candidate?.agentId === agentId,
566
+ ) || { agentId };
567
+ return [
568
+ agentId,
569
+ projectLegacySummaryFromEnvelope(envelope, {
570
+ agent,
571
+ waveNumber: waveDefinition?.wave ?? null,
572
+ reportPath: resolveReducerReportPath(waveDefinition, agent),
573
+ }),
574
+ ];
575
+ })
576
+ .filter(([, summary]) => Boolean(summary)),
577
+ );
537
578
  // Step 1: Materialize control-plane state
538
579
  const controlPlaneState = materializeControlPlaneState(controlPlaneEvents);
539
580
 
@@ -555,7 +596,7 @@ export function reduceWaveState({
555
596
  // Step 5: Evaluate proof availability per agent
556
597
  const proofAvailability = buildProofAvailability(
557
598
  tasks,
558
- agentResults,
599
+ resolvedAgentResults,
559
600
  controlPlaneState,
560
601
  );
561
602
 
@@ -564,7 +605,7 @@ export function reduceWaveState({
564
605
 
565
606
  // Step 6: Build integration summary BEFORE creating derivedState for gates
566
607
  const integrationAgentId = laneConfig.integrationAgentId || "A8";
567
- const integrationResult = agentResults?.[integrationAgentId]?.integration || null;
608
+ const integrationResult = resolvedAgentResults?.[integrationAgentId]?.integration || null;
568
609
  const integrationSummary = integrationResult
569
610
  ? {
570
611
  recommendation: integrationResult.state === "ready-for-doc-closure"
@@ -622,7 +663,7 @@ export function reduceWaveState({
622
663
  // Step 8: Evaluate gates using pure variants (integrationSummary already in derivedState)
623
664
  const gateSnapshot = buildGateSnapshotPure({
624
665
  wave: waveDefinition || { wave: 0, agents: [] },
625
- agentResults,
666
+ agentResults: resolvedAgentResults,
626
667
  derivedState,
627
668
  validationMode: laneConfig.validationMode || "live",
628
669
  laneConfig,
@@ -647,6 +688,7 @@ export function reduceWaveState({
647
688
  gateSnapshot,
648
689
  coordinationState,
649
690
  dependencySnapshot: dependencyTickets,
691
+ clarificationBarrier,
650
692
  });
651
693
 
652
694
  // Step 13: Derive waveState from phase
@@ -708,6 +750,11 @@ export function reduceWaveState({
708
750
  facts,
709
751
  humanInputs,
710
752
  assignments,
753
+ agentEnvelopes: agentEnvelopes || null,
754
+ capabilityAssignments,
755
+ coordinationState,
756
+ dependencySnapshot: dependencyTickets,
757
+ integrationSummary,
711
758
 
712
759
  coordinationMetrics,
713
760
  controlPlaneState,