@chllming/wave-orchestration 0.7.1 → 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 +15 -0
- package/README.md +8 -8
- 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 +2 -2
- 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/npmjs-trusted-publishing.md +2 -2
- package/docs/reference/sample-waves.md +4 -4
- package/package.json +1 -1
- package/releases/manifest.json +19 -0
- package/scripts/wave-orchestrator/agent-state.mjs +447 -33
- package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
- package/scripts/wave-orchestrator/control-cli.mjs +7 -1
- package/scripts/wave-orchestrator/coordination.mjs +11 -10
- package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
- package/scripts/wave-orchestrator/install.mjs +22 -0
- 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-supervisor.mjs +704 -0
- package/scripts/wave-orchestrator/launcher.mjs +153 -2922
- package/scripts/wave-orchestrator/task-entity.mjs +557 -0
- package/scripts/wave-orchestrator/wave-files.mjs +11 -2
- package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
- package/wave.config.json +1 -1
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
readWaveComponentGate,
|
|
5
|
+
buildSharedComponentSiblingPendingFailure,
|
|
6
|
+
analyzePromotedComponentOwners,
|
|
7
|
+
readRunExecutionSummary,
|
|
8
|
+
readClarificationBarrier,
|
|
9
|
+
readWaveAssignmentBarrier,
|
|
10
|
+
readWaveDependencyBarrier,
|
|
11
|
+
} from "./launcher-gates.mjs";
|
|
12
|
+
import {
|
|
13
|
+
isOpenCoordinationStatus,
|
|
14
|
+
openClarificationLinkedRequests,
|
|
15
|
+
} from "./coordination-store.mjs";
|
|
16
|
+
import {
|
|
17
|
+
readStatusRecordIfPresent,
|
|
18
|
+
REPO_ROOT,
|
|
19
|
+
toIsoTimestamp,
|
|
20
|
+
} from "./shared.mjs";
|
|
21
|
+
import {
|
|
22
|
+
readAgentExecutionSummary,
|
|
23
|
+
validateImplementationSummary,
|
|
24
|
+
} from "./agent-state.mjs";
|
|
25
|
+
import {
|
|
26
|
+
agentRequiresProofCentricValidation,
|
|
27
|
+
waveRequiresProofCentricValidation,
|
|
28
|
+
validateWaveRuntimeMixAssignments,
|
|
29
|
+
} from "./wave-files.mjs";
|
|
30
|
+
import {
|
|
31
|
+
augmentSummaryWithProofRegistry,
|
|
32
|
+
} from "./proof-registry.mjs";
|
|
33
|
+
import { hashAgentPromptFingerprint } from "./context7.mjs";
|
|
34
|
+
import {
|
|
35
|
+
isSecurityReviewAgent,
|
|
36
|
+
} from "./role-helpers.mjs";
|
|
37
|
+
import {
|
|
38
|
+
commandForExecutor,
|
|
39
|
+
isExecutorCommandAvailable,
|
|
40
|
+
} from "./executors.mjs";
|
|
41
|
+
import {
|
|
42
|
+
readWaveRelaunchPlanSnapshot,
|
|
43
|
+
waveRelaunchPlanPath,
|
|
44
|
+
} from "./retry-control.mjs";
|
|
45
|
+
import {
|
|
46
|
+
writeRelaunchPlan,
|
|
47
|
+
} from "./artifact-schemas.mjs";
|
|
48
|
+
import {
|
|
49
|
+
refreshResolvedSkillsForRun,
|
|
50
|
+
} from "./launcher-runtime.mjs";
|
|
51
|
+
import {
|
|
52
|
+
buildRequestAssignments,
|
|
53
|
+
} from "./routing-state.mjs";
|
|
54
|
+
import {
|
|
55
|
+
setWaveDashboardAgent,
|
|
56
|
+
} from "./dashboard-state.mjs";
|
|
57
|
+
|
|
58
|
+
export function readWaveRelaunchPlan(lanePaths, waveNumber) {
|
|
59
|
+
return readWaveRelaunchPlanSnapshot(lanePaths, waveNumber);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeWaveRelaunchPlan(lanePaths, waveNumber, payload) {
|
|
63
|
+
const filePath = waveRelaunchPlanPath(lanePaths, waveNumber);
|
|
64
|
+
writeRelaunchPlan(filePath, payload, { wave: waveNumber });
|
|
65
|
+
return filePath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function clearWaveRelaunchPlan(lanePaths, waveNumber) {
|
|
69
|
+
const filePath = waveRelaunchPlanPath(lanePaths, waveNumber);
|
|
70
|
+
try {
|
|
71
|
+
fs.rmSync(filePath, { force: true });
|
|
72
|
+
} catch {
|
|
73
|
+
// no-op
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resetPersistedWaveLaunchState(lanePaths, waveNumber, options = {}) {
|
|
78
|
+
if (options?.dryRun || options?.resumeControlState) {
|
|
79
|
+
return {
|
|
80
|
+
clearedRelaunchPlan: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const persistedRelaunchPlan = readWaveRelaunchPlan(lanePaths, waveNumber);
|
|
84
|
+
if (!persistedRelaunchPlan) {
|
|
85
|
+
return {
|
|
86
|
+
clearedRelaunchPlan: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
clearWaveRelaunchPlan(lanePaths, waveNumber);
|
|
90
|
+
return {
|
|
91
|
+
clearedRelaunchPlan: true,
|
|
92
|
+
relaunchPlan: persistedRelaunchPlan,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function proofCentricReuseBlocked(derivedState) {
|
|
97
|
+
if (!derivedState) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return (
|
|
101
|
+
readClarificationBarrier(derivedState).ok === false ||
|
|
102
|
+
readWaveAssignmentBarrier(derivedState).ok === false ||
|
|
103
|
+
readWaveDependencyBarrier(derivedState).ok === false
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sameAgentIdSet(left = [], right = []) {
|
|
108
|
+
const leftIds = Array.from(new Set((left || []).filter(Boolean))).toSorted();
|
|
109
|
+
const rightIds = Array.from(new Set((right || []).filter(Boolean))).toSorted();
|
|
110
|
+
return leftIds.length === rightIds.length && leftIds.every((agentId, index) => agentId === rightIds[index]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function persistedRelaunchPlanMatchesCurrentState(
|
|
114
|
+
agentRuns,
|
|
115
|
+
persistedPlan,
|
|
116
|
+
lanePaths,
|
|
117
|
+
waveDefinition,
|
|
118
|
+
) {
|
|
119
|
+
if (!persistedPlan || !Array.isArray(persistedPlan.selectedAgentIds)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const componentGate = readWaveComponentGate(waveDefinition, agentRuns, {
|
|
123
|
+
laneProfile: lanePaths?.laneProfile,
|
|
124
|
+
});
|
|
125
|
+
if (componentGate?.statusCode !== "shared-component-sibling-pending") {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return sameAgentIdSet(
|
|
129
|
+
persistedPlan.selectedAgentIds,
|
|
130
|
+
componentGate.waitingOnAgentIds || [],
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function applyPersistedRelaunchPlan(agentRuns, persistedPlan, lanePaths, waveDefinition) {
|
|
135
|
+
if (!persistedPlan || !Array.isArray(persistedPlan.selectedAgentIds)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const runsByAgentId = new Map(agentRuns.map((run) => [run.agent.agentId, run]));
|
|
139
|
+
for (const [agentId, executorState] of Object.entries(persistedPlan.executorStates || {})) {
|
|
140
|
+
const run = runsByAgentId.get(agentId);
|
|
141
|
+
if (!run || !executorState || typeof executorState !== "object") {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
run.agent.executorResolved = executorState;
|
|
145
|
+
refreshResolvedSkillsForRun(run, waveDefinition, lanePaths);
|
|
146
|
+
}
|
|
147
|
+
return persistedPlan.selectedAgentIds
|
|
148
|
+
.map((agentId) => runsByAgentId.get(agentId))
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { applyPersistedRelaunchPlan };
|
|
153
|
+
|
|
154
|
+
export function resolveSharedComponentContinuationRuns(
|
|
155
|
+
currentRuns,
|
|
156
|
+
agentRuns,
|
|
157
|
+
failures,
|
|
158
|
+
derivedState,
|
|
159
|
+
lanePaths,
|
|
160
|
+
waveDefinition = null,
|
|
161
|
+
) {
|
|
162
|
+
if (!Array.isArray(currentRuns) || currentRuns.length === 0 || !Array.isArray(failures) || failures.length === 0) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
if (!failures.every((failure) => failure.statusCode === "shared-component-sibling-pending")) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const currentRunIds = new Set(currentRuns.map((run) => run.agent.agentId));
|
|
169
|
+
const waitingAgentIds = new Set(
|
|
170
|
+
failures.flatMap((failure) => failure.waitingOnAgentIds || []).filter(Boolean),
|
|
171
|
+
);
|
|
172
|
+
if (Array.from(currentRunIds).some((agentId) => waitingAgentIds.has(agentId))) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
const relaunchResolution = resolveRelaunchRuns(
|
|
176
|
+
agentRuns,
|
|
177
|
+
failures,
|
|
178
|
+
derivedState,
|
|
179
|
+
lanePaths,
|
|
180
|
+
waveDefinition,
|
|
181
|
+
);
|
|
182
|
+
if (relaunchResolution.barrier || relaunchResolution.runs.length === 0) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
return relaunchResolution.runs.some((run) => !currentRunIds.has(run.agent.agentId))
|
|
186
|
+
? relaunchResolution.runs
|
|
187
|
+
: [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function relaunchReasonBuckets(runs, failures, derivedState) {
|
|
191
|
+
const selectedAgentIds = new Set((runs || []).map((run) => run.agent.agentId));
|
|
192
|
+
return {
|
|
193
|
+
clarification: openClarificationLinkedRequests(derivedState?.coordinationState)
|
|
194
|
+
.flatMap((record) => record.targets || [])
|
|
195
|
+
.some((target) => {
|
|
196
|
+
const agentId = String(target || "").startsWith("agent:")
|
|
197
|
+
? String(target).slice("agent:".length)
|
|
198
|
+
: String(target || "");
|
|
199
|
+
return selectedAgentIds.has(agentId);
|
|
200
|
+
}),
|
|
201
|
+
helperAssignment: (derivedState?.capabilityAssignments || []).some(
|
|
202
|
+
(assignment) => assignment.blocking && selectedAgentIds.has(assignment.assignedAgentId),
|
|
203
|
+
),
|
|
204
|
+
dependency: ((derivedState?.dependencySnapshot?.openInbound || []).some((record) =>
|
|
205
|
+
selectedAgentIds.has(record.assignedAgentId),
|
|
206
|
+
)),
|
|
207
|
+
blocker: (derivedState?.coordinationState?.blockers || []).some(
|
|
208
|
+
(record) =>
|
|
209
|
+
isOpenCoordinationStatus(record.status) &&
|
|
210
|
+
(selectedAgentIds.has(record.agentId) ||
|
|
211
|
+
(record.targets || []).some((target) => {
|
|
212
|
+
const agentId = String(target || "").startsWith("agent:")
|
|
213
|
+
? String(target).slice("agent:".length)
|
|
214
|
+
: String(target || "");
|
|
215
|
+
return selectedAgentIds.has(agentId);
|
|
216
|
+
})),
|
|
217
|
+
),
|
|
218
|
+
closureGate: (failures || []).some(
|
|
219
|
+
(failure) => failure.agentId && selectedAgentIds.has(failure.agentId),
|
|
220
|
+
),
|
|
221
|
+
sharedComponentSiblingWait: (failures || []).some(
|
|
222
|
+
(failure) =>
|
|
223
|
+
failure.statusCode === "shared-component-sibling-pending" &&
|
|
224
|
+
(failure.waitingOnAgentIds || []).some((agentId) => selectedAgentIds.has(agentId)),
|
|
225
|
+
),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function applySharedComponentWaitStateToDashboard(componentGate, dashboardState) {
|
|
230
|
+
const waitingSummary = (componentGate?.waitingOnAgentIds || []).join("/");
|
|
231
|
+
if (!waitingSummary) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const agentId of componentGate?.satisfiedAgentIds || []) {
|
|
235
|
+
setWaveDashboardAgent(dashboardState, agentId, {
|
|
236
|
+
state: "completed",
|
|
237
|
+
detail: `Desired-state slice landed; waiting on ${waitingSummary} for shared component closure`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function reconcileFailuresAgainstSharedComponentState(wave, agentRuns, failures) {
|
|
243
|
+
if (!Array.isArray(failures) || failures.length === 0) {
|
|
244
|
+
return failures;
|
|
245
|
+
}
|
|
246
|
+
const summariesByAgentId = Object.fromEntries(
|
|
247
|
+
(agentRuns || []).map((runInfo) => [runInfo.agent.agentId, readRunExecutionSummary(runInfo, wave)]),
|
|
248
|
+
);
|
|
249
|
+
const failureAgentIds = new Set(failures.map((failure) => failure.agentId).filter(Boolean));
|
|
250
|
+
const consumedSatisfiedAgentIds = new Set();
|
|
251
|
+
const synthesizedFailures = [];
|
|
252
|
+
for (const promotion of wave?.componentPromotions || []) {
|
|
253
|
+
const componentState = analyzePromotedComponentOwners(
|
|
254
|
+
promotion.componentId,
|
|
255
|
+
agentRuns,
|
|
256
|
+
summariesByAgentId,
|
|
257
|
+
);
|
|
258
|
+
if (
|
|
259
|
+
componentState.satisfiedAgentIds.length === 0 ||
|
|
260
|
+
componentState.waitingOnAgentIds.length === 0 ||
|
|
261
|
+
!componentState.satisfiedAgentIds.some((agentId) => failureAgentIds.has(agentId))
|
|
262
|
+
) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
for (const agentId of componentState.satisfiedAgentIds) {
|
|
266
|
+
if (failureAgentIds.has(agentId)) {
|
|
267
|
+
consumedSatisfiedAgentIds.add(agentId);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
synthesizedFailures.push(buildSharedComponentSiblingPendingFailure(componentState));
|
|
271
|
+
}
|
|
272
|
+
return [
|
|
273
|
+
...synthesizedFailures.filter(Boolean),
|
|
274
|
+
...failures.filter((failure) => !consumedSatisfiedAgentIds.has(failure.agentId)),
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function hasReusableSuccessStatus(agent, statusPath, options = {}) {
|
|
279
|
+
const statusRecord = readStatusRecordIfPresent(statusPath);
|
|
280
|
+
const basicReuseOk = Boolean(
|
|
281
|
+
statusRecord && statusRecord.code === 0 && statusRecord.promptHash === hashAgentPromptFingerprint(agent),
|
|
282
|
+
);
|
|
283
|
+
if (!basicReuseOk) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const proofCentric =
|
|
287
|
+
agentRequiresProofCentricValidation(agent) || waveRequiresProofCentricValidation(options.wave);
|
|
288
|
+
if (!proofCentric) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
const summary = readAgentExecutionSummary(statusPath, {
|
|
292
|
+
agent,
|
|
293
|
+
statusPath,
|
|
294
|
+
statusRecord,
|
|
295
|
+
logPath: options.logPath || null,
|
|
296
|
+
reportPath: options.reportPath || null,
|
|
297
|
+
});
|
|
298
|
+
if (!summary) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const effectiveSummary = options.proofRegistry
|
|
302
|
+
? augmentSummaryWithProofRegistry(agent, summary, options.proofRegistry)
|
|
303
|
+
: summary;
|
|
304
|
+
if (!validateImplementationSummary(agent, effectiveSummary).ok) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (proofCentricReuseBlocked(options.derivedState)) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isClosureAgentId(agent, lanePaths) {
|
|
314
|
+
return [
|
|
315
|
+
lanePaths.contEvalAgentId || "E0",
|
|
316
|
+
lanePaths.integrationAgentId || "A8",
|
|
317
|
+
lanePaths.documentationAgentId || "A9",
|
|
318
|
+
lanePaths.contQaAgentId || "A0",
|
|
319
|
+
].includes(agent?.agentId) || isSecurityReviewAgent(agent);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function selectReusablePreCompletedAgentIds(
|
|
323
|
+
agentRuns,
|
|
324
|
+
lanePaths,
|
|
325
|
+
{ retryOverride = null, wave = null, derivedState = null, proofRegistry = null } = {},
|
|
326
|
+
) {
|
|
327
|
+
const retryOverrideClearedAgentIds = new Set(retryOverride?.clearReusableAgentIds || []);
|
|
328
|
+
return new Set(
|
|
329
|
+
(agentRuns || [])
|
|
330
|
+
.filter(
|
|
331
|
+
(run) =>
|
|
332
|
+
!retryOverrideClearedAgentIds.has(run.agent.agentId) &&
|
|
333
|
+
!isClosureAgentId(run.agent, lanePaths) &&
|
|
334
|
+
hasReusableSuccessStatus(run.agent, run.statusPath, {
|
|
335
|
+
wave,
|
|
336
|
+
derivedState,
|
|
337
|
+
proofRegistry,
|
|
338
|
+
logPath: run.logPath,
|
|
339
|
+
}),
|
|
340
|
+
)
|
|
341
|
+
.map((run) => run.agent.agentId),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function selectInitialWaveRuns(agentRuns, lanePaths) {
|
|
346
|
+
const implementationRuns = (agentRuns || []).filter(
|
|
347
|
+
(run) => !isClosureAgentId(run?.agent, lanePaths),
|
|
348
|
+
);
|
|
349
|
+
return implementationRuns.length > 0 ? implementationRuns : agentRuns;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isLauncherSeedRequest(record) {
|
|
353
|
+
return (
|
|
354
|
+
record?.source === "launcher" &&
|
|
355
|
+
/^wave-\d+-agent-[^-]+-request$/.test(String(record.id || "")) &&
|
|
356
|
+
!String(record.closureCondition || "").trim() &&
|
|
357
|
+
(!Array.isArray(record.dependsOn) || record.dependsOn.length === 0)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function runtimeMixValidationForRuns(agentRuns, lanePaths) {
|
|
362
|
+
return validateWaveRuntimeMixAssignments(
|
|
363
|
+
{
|
|
364
|
+
wave: 0,
|
|
365
|
+
agents: agentRuns.map((run) => run.agent),
|
|
366
|
+
},
|
|
367
|
+
{ laneProfile: lanePaths.laneProfile },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function nextExecutorModel(executorState, executorId) {
|
|
372
|
+
if (executorId === "claude") {
|
|
373
|
+
return executorState?.claude?.model || null;
|
|
374
|
+
}
|
|
375
|
+
if (executorId === "opencode") {
|
|
376
|
+
return executorState?.opencode?.model || null;
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function executorFallbackChain(executorState) {
|
|
382
|
+
if (
|
|
383
|
+
executorState?.retryPolicy === "sticky" ||
|
|
384
|
+
executorState?.allowFallbackOnRetry === false
|
|
385
|
+
) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
return Array.isArray(executorState?.fallbacks)
|
|
389
|
+
? executorState.fallbacks.filter(Boolean)
|
|
390
|
+
: [];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildFallbackExecutorState(executorState, executorId, attempt, reason) {
|
|
394
|
+
const history = Array.isArray(executorState?.executorHistory)
|
|
395
|
+
? executorState.executorHistory
|
|
396
|
+
: [];
|
|
397
|
+
return {
|
|
398
|
+
...executorState,
|
|
399
|
+
id: executorId,
|
|
400
|
+
model: nextExecutorModel(executorState, executorId),
|
|
401
|
+
selectedBy: "retry-fallback",
|
|
402
|
+
fallbackUsed: true,
|
|
403
|
+
fallbackReason: reason,
|
|
404
|
+
initialExecutorId: executorState?.initialExecutorId || executorState?.id || executorId,
|
|
405
|
+
executorHistory: [
|
|
406
|
+
...history,
|
|
407
|
+
{
|
|
408
|
+
attempt,
|
|
409
|
+
executorId,
|
|
410
|
+
reason,
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function applyRetryFallbacks(agentRuns, failures, lanePaths, attemptNumber, waveDefinition = null) {
|
|
417
|
+
const failedAgentIds = new Set(
|
|
418
|
+
failures
|
|
419
|
+
.filter((failure) => failure.statusCode !== "shared-component-sibling-pending")
|
|
420
|
+
.map((failure) => failure.agentId),
|
|
421
|
+
);
|
|
422
|
+
let changed = false;
|
|
423
|
+
const outcomes = new Map();
|
|
424
|
+
for (const run of agentRuns) {
|
|
425
|
+
if (!failedAgentIds.has(run.agent.agentId)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const executorState = run.agent.executorResolved;
|
|
429
|
+
if (!executorState) {
|
|
430
|
+
outcomes.set(run.agent.agentId, {
|
|
431
|
+
applied: false,
|
|
432
|
+
blocking: false,
|
|
433
|
+
statusCode: "no-executor-state",
|
|
434
|
+
detail: `Agent ${run.agent.agentId} has no resolved executor state.`,
|
|
435
|
+
});
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const fallbackChain = executorFallbackChain(executorState);
|
|
439
|
+
if (fallbackChain.length === 0) {
|
|
440
|
+
outcomes.set(run.agent.agentId, {
|
|
441
|
+
applied: false,
|
|
442
|
+
blocking: false,
|
|
443
|
+
statusCode: "no-fallback-configured",
|
|
444
|
+
detail: `Agent ${run.agent.agentId} has no configured fallback executors.`,
|
|
445
|
+
});
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const attemptedExecutors = new Set(
|
|
449
|
+
Array.isArray(executorState.executorHistory)
|
|
450
|
+
? executorState.executorHistory.map((entry) => entry.executorId)
|
|
451
|
+
: [executorState.id],
|
|
452
|
+
);
|
|
453
|
+
const fallbackReason = failures.find((failure) => failure.agentId === run.agent.agentId);
|
|
454
|
+
const blockedCandidates = [];
|
|
455
|
+
for (const candidate of fallbackChain) {
|
|
456
|
+
if (!candidate || candidate === executorState.id || attemptedExecutors.has(candidate)) {
|
|
457
|
+
if (candidate) {
|
|
458
|
+
blockedCandidates.push(`${candidate}: already tried`);
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const command = commandForExecutor(executorState, candidate);
|
|
463
|
+
if (!isExecutorCommandAvailable(command)) {
|
|
464
|
+
blockedCandidates.push(`${candidate}: command unavailable`);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const nextState = buildFallbackExecutorState(
|
|
468
|
+
executorState,
|
|
469
|
+
candidate,
|
|
470
|
+
attemptNumber,
|
|
471
|
+
`retry:${fallbackReason?.statusCode || "failed-attempt"}`,
|
|
472
|
+
);
|
|
473
|
+
const validation = runtimeMixValidationForRuns(
|
|
474
|
+
agentRuns.map((entry) =>
|
|
475
|
+
entry.agent.agentId === run.agent.agentId
|
|
476
|
+
? { ...entry, agent: { ...entry.agent, executorResolved: nextState } }
|
|
477
|
+
: entry,
|
|
478
|
+
),
|
|
479
|
+
lanePaths,
|
|
480
|
+
);
|
|
481
|
+
if (!validation.ok) {
|
|
482
|
+
blockedCandidates.push(`${candidate}: ${validation.detail}`);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
run.agent.executorResolved = nextState;
|
|
486
|
+
refreshResolvedSkillsForRun(run, waveDefinition, lanePaths);
|
|
487
|
+
changed = true;
|
|
488
|
+
outcomes.set(run.agent.agentId, {
|
|
489
|
+
applied: true,
|
|
490
|
+
blocking: false,
|
|
491
|
+
statusCode: "fallback-applied",
|
|
492
|
+
detail: `Agent ${run.agent.agentId} will retry on ${candidate}.`,
|
|
493
|
+
executorId: candidate,
|
|
494
|
+
});
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
if (!outcomes.has(run.agent.agentId)) {
|
|
498
|
+
outcomes.set(run.agent.agentId, {
|
|
499
|
+
applied: false,
|
|
500
|
+
blocking: true,
|
|
501
|
+
statusCode: "retry-fallback-blocked",
|
|
502
|
+
detail: `Agent ${run.agent.agentId} cannot retry safely on a configured fallback (${blockedCandidates.join("; ") || "no safe fallback remained"}).`,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
changed,
|
|
508
|
+
outcomes,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function retryBarrierFromOutcomes(outcomes, failures) {
|
|
513
|
+
const blockingFailures = [];
|
|
514
|
+
for (const failure of failures) {
|
|
515
|
+
const outcome = outcomes.get(failure.agentId);
|
|
516
|
+
if (!outcome?.blocking) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
blockingFailures.push({
|
|
520
|
+
agentId: failure.agentId,
|
|
521
|
+
statusCode: outcome.statusCode,
|
|
522
|
+
logPath: failure.logPath,
|
|
523
|
+
detail: outcome.detail,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (blockingFailures.length === 0) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
statusCode: "retry-fallback-blocked",
|
|
531
|
+
detail: blockingFailures.map((failure) => failure.detail).join(" "),
|
|
532
|
+
failures: blockingFailures,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function resolveRelaunchRuns(agentRuns, failures, derivedState, lanePaths, waveDefinition = null) {
|
|
537
|
+
const runsByAgentId = new Map(agentRuns.map((run) => [run.agent.agentId, run]));
|
|
538
|
+
const pendingFeedback = (derivedState?.coordinationState?.humanFeedback || []).filter((record) =>
|
|
539
|
+
isOpenCoordinationStatus(record.status),
|
|
540
|
+
);
|
|
541
|
+
const pendingHumanEscalations = (derivedState?.coordinationState?.humanEscalations || []).filter(
|
|
542
|
+
(record) => isOpenCoordinationStatus(record.status),
|
|
543
|
+
);
|
|
544
|
+
if (pendingFeedback.length > 0 || pendingHumanEscalations.length > 0) {
|
|
545
|
+
return { runs: [], barrier: null };
|
|
546
|
+
}
|
|
547
|
+
const nextAttemptNumber = Number(derivedState?.ledger?.attempt || 0) + 1;
|
|
548
|
+
const fallbackResolution = applyRetryFallbacks(
|
|
549
|
+
agentRuns,
|
|
550
|
+
failures,
|
|
551
|
+
lanePaths,
|
|
552
|
+
nextAttemptNumber,
|
|
553
|
+
waveDefinition,
|
|
554
|
+
);
|
|
555
|
+
const retryBarrier = retryBarrierFromOutcomes(fallbackResolution.outcomes, failures);
|
|
556
|
+
if (retryBarrier) {
|
|
557
|
+
return { runs: [], barrier: retryBarrier };
|
|
558
|
+
}
|
|
559
|
+
const clarificationTargets = new Set();
|
|
560
|
+
for (const record of openClarificationLinkedRequests(derivedState?.coordinationState)) {
|
|
561
|
+
for (const target of record.targets || []) {
|
|
562
|
+
if (String(target).startsWith("agent:")) {
|
|
563
|
+
clarificationTargets.add(String(target).slice("agent:".length));
|
|
564
|
+
} else if (runsByAgentId.has(target)) {
|
|
565
|
+
clarificationTargets.add(target);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (clarificationTargets.size > 0) {
|
|
570
|
+
return {
|
|
571
|
+
runs: Array.from(clarificationTargets)
|
|
572
|
+
.map((agentId) => runsByAgentId.get(agentId))
|
|
573
|
+
.filter(Boolean),
|
|
574
|
+
barrier: null,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const blockingAssignments = (derivedState?.capabilityAssignments || []).filter(
|
|
578
|
+
(assignment) => assignment.blocking,
|
|
579
|
+
);
|
|
580
|
+
const effectiveAssignments =
|
|
581
|
+
blockingAssignments.length > 0
|
|
582
|
+
? blockingAssignments
|
|
583
|
+
: buildRequestAssignments({
|
|
584
|
+
coordinationState: derivedState?.coordinationState,
|
|
585
|
+
agents: agentRuns.map((run) => run.agent),
|
|
586
|
+
ledger: derivedState?.ledger,
|
|
587
|
+
capabilityRouting: lanePaths?.capabilityRouting,
|
|
588
|
+
}).filter((assignment) => assignment.blocking);
|
|
589
|
+
const assignmentSource = effectiveAssignments.length > 0 ? effectiveAssignments : blockingAssignments;
|
|
590
|
+
const unresolvedFromSource = assignmentSource.filter((assignment) => !assignment.assignedAgentId);
|
|
591
|
+
if (unresolvedFromSource.length > 0) {
|
|
592
|
+
return {
|
|
593
|
+
runs: [],
|
|
594
|
+
barrier: {
|
|
595
|
+
statusCode: "helper-assignment-unresolved",
|
|
596
|
+
detail: `No matching assignee exists for helper requests (${unresolvedFromSource.map((assignment) => assignment.requestId).join(", ")}).`,
|
|
597
|
+
failures: unresolvedFromSource.map((assignment) => ({
|
|
598
|
+
agentId: null,
|
|
599
|
+
statusCode: "helper-assignment-unresolved",
|
|
600
|
+
logPath: null,
|
|
601
|
+
detail: assignment.assignmentDetail || assignment.summary || assignment.requestId,
|
|
602
|
+
})),
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const assignedAgentIds = new Set(
|
|
607
|
+
assignmentSource.map((assignment) => assignment.assignedAgentId).filter(Boolean),
|
|
608
|
+
);
|
|
609
|
+
if (assignedAgentIds.size > 0) {
|
|
610
|
+
return {
|
|
611
|
+
runs: Array.from(assignedAgentIds)
|
|
612
|
+
.map((agentId) => runsByAgentId.get(agentId))
|
|
613
|
+
.filter(Boolean),
|
|
614
|
+
barrier: null,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const unresolvedInboundAssignments =
|
|
618
|
+
derivedState?.dependencySnapshot?.unresolvedInboundAssignments || [];
|
|
619
|
+
if (unresolvedInboundAssignments.length > 0) {
|
|
620
|
+
return {
|
|
621
|
+
runs: [],
|
|
622
|
+
barrier: {
|
|
623
|
+
statusCode: "dependency-assignment-unresolved",
|
|
624
|
+
detail: `Required inbound dependencies are not assigned (${unresolvedInboundAssignments.map((record) => record.id).join(", ")}).`,
|
|
625
|
+
failures: unresolvedInboundAssignments.map((record) => ({
|
|
626
|
+
agentId: null,
|
|
627
|
+
statusCode: "dependency-assignment-unresolved",
|
|
628
|
+
logPath: null,
|
|
629
|
+
detail: record.assignmentDetail || record.summary || record.id,
|
|
630
|
+
})),
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const inboundDependencyAgentIds = new Set(
|
|
635
|
+
(derivedState?.dependencySnapshot?.openInbound || [])
|
|
636
|
+
.map((record) => record.assignedAgentId)
|
|
637
|
+
.filter(Boolean),
|
|
638
|
+
);
|
|
639
|
+
if (inboundDependencyAgentIds.size > 0) {
|
|
640
|
+
return {
|
|
641
|
+
runs: Array.from(inboundDependencyAgentIds)
|
|
642
|
+
.map((agentId) => runsByAgentId.get(agentId))
|
|
643
|
+
.filter(Boolean),
|
|
644
|
+
barrier: null,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const blockerAgentIds = new Set();
|
|
648
|
+
for (const record of derivedState?.coordinationState?.blockers || []) {
|
|
649
|
+
if (!isOpenCoordinationStatus(record.status)) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
blockerAgentIds.add(record.agentId);
|
|
653
|
+
for (const target of record.targets || []) {
|
|
654
|
+
if (String(target).startsWith("agent:")) {
|
|
655
|
+
blockerAgentIds.add(String(target).slice("agent:".length));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (blockerAgentIds.size > 0) {
|
|
660
|
+
return {
|
|
661
|
+
runs: Array.from(blockerAgentIds)
|
|
662
|
+
.map((agentId) => runsByAgentId.get(agentId))
|
|
663
|
+
.filter(Boolean),
|
|
664
|
+
barrier: null,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (derivedState?.ledger?.phase === "docs-closure") {
|
|
668
|
+
return {
|
|
669
|
+
runs: [runsByAgentId.get(lanePaths.documentationAgentId)].filter(Boolean),
|
|
670
|
+
barrier: null,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (derivedState?.ledger?.phase === "security-review") {
|
|
674
|
+
return {
|
|
675
|
+
runs: agentRuns.filter((run) => isSecurityReviewAgent(run.agent)),
|
|
676
|
+
barrier: null,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (derivedState?.ledger?.phase === "cont-eval") {
|
|
680
|
+
return {
|
|
681
|
+
runs: [runsByAgentId.get(lanePaths.contEvalAgentId)].filter(Boolean),
|
|
682
|
+
barrier: null,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (derivedState?.ledger?.phase === "cont-qa-closure") {
|
|
686
|
+
return {
|
|
687
|
+
runs: [runsByAgentId.get(lanePaths.contQaAgentId)].filter(Boolean),
|
|
688
|
+
barrier: null,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (derivedState?.ledger?.phase === "integrating") {
|
|
692
|
+
return {
|
|
693
|
+
runs: [runsByAgentId.get(lanePaths.integrationAgentId)].filter(Boolean),
|
|
694
|
+
barrier: null,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const sharedComponentWaitingAgentIds = new Set(
|
|
698
|
+
(failures || [])
|
|
699
|
+
.filter((failure) => failure.statusCode === "shared-component-sibling-pending")
|
|
700
|
+
.flatMap((failure) => failure.waitingOnAgentIds || [])
|
|
701
|
+
.filter((agentId) => runsByAgentId.has(agentId)),
|
|
702
|
+
);
|
|
703
|
+
if (sharedComponentWaitingAgentIds.size > 0) {
|
|
704
|
+
return {
|
|
705
|
+
runs: Array.from(sharedComponentWaitingAgentIds)
|
|
706
|
+
.map((agentId) => runsByAgentId.get(agentId))
|
|
707
|
+
.filter(Boolean),
|
|
708
|
+
barrier: null,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const failedAgentIds = new Set(failures.map((failure) => failure.agentId));
|
|
712
|
+
return {
|
|
713
|
+
runs: agentRuns.filter((run) => failedAgentIds.has(run.agent.agentId)),
|
|
714
|
+
barrier: null,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function preflightWavesForExecutorAvailability(waves, lanePaths) {
|
|
719
|
+
for (const wave of waves) {
|
|
720
|
+
const mixValidation = validateWaveRuntimeMixAssignments(wave, {
|
|
721
|
+
laneProfile: lanePaths.laneProfile,
|
|
722
|
+
});
|
|
723
|
+
if (!mixValidation.ok) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`Wave ${wave.wave} exceeds lane runtime mix targets (${mixValidation.detail})`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
for (const agent of wave.agents) {
|
|
729
|
+
const executorState = agent.executorResolved;
|
|
730
|
+
if (!executorState) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const chain = [executorState.id, ...executorFallbackChain(executorState)];
|
|
734
|
+
const availableExecutorId = chain.find((executorId) =>
|
|
735
|
+
isExecutorCommandAvailable(commandForExecutor(executorState, executorId)),
|
|
736
|
+
);
|
|
737
|
+
if (!availableExecutorId) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
`Agent ${agent.agentId} has no available executor command in its configured chain (${chain.join(" -> ")})`,
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ---------------------------------------------------------------------------
|
|
747
|
+
// Resume plan — pure function, no file I/O (Wave 3)
|
|
748
|
+
// ---------------------------------------------------------------------------
|
|
749
|
+
|
|
750
|
+
function phaseFromGate(gateName) {
|
|
751
|
+
switch (gateName) {
|
|
752
|
+
case "implementationGate":
|
|
753
|
+
case "componentGate":
|
|
754
|
+
case "contEvalGate":
|
|
755
|
+
return "implementation";
|
|
756
|
+
case "integrationBarrier":
|
|
757
|
+
return "integrating";
|
|
758
|
+
case "documentationGate":
|
|
759
|
+
return "docs-closure";
|
|
760
|
+
case "contQaGate":
|
|
761
|
+
return "cont-qa-closure";
|
|
762
|
+
default:
|
|
763
|
+
return "implementation";
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function classifyResumeReason(waveState) {
|
|
768
|
+
if (waveState.closureEligibility?.waveMayClose) {
|
|
769
|
+
return "all-gates-pass";
|
|
770
|
+
}
|
|
771
|
+
const humanBlockers = (waveState.openBlockers || []).filter(
|
|
772
|
+
(blocker) => blocker.kind === "human-input",
|
|
773
|
+
);
|
|
774
|
+
if (humanBlockers.length > 0) {
|
|
775
|
+
return "human-request";
|
|
776
|
+
}
|
|
777
|
+
const componentPending = (waveState.openBlockers || []).some(
|
|
778
|
+
(blocker) => blocker.kind === "shared-component-sibling-pending",
|
|
779
|
+
);
|
|
780
|
+
if (componentPending && (waveState.closureEligibility?.pendingAgentIds || []).length > 0) {
|
|
781
|
+
return "shared-component-sibling-pending";
|
|
782
|
+
}
|
|
783
|
+
const gateSnapshot = waveState.gateSnapshot || {};
|
|
784
|
+
if (gateSnapshot.overall && !gateSnapshot.overall.ok) {
|
|
785
|
+
return "gate-failure";
|
|
786
|
+
}
|
|
787
|
+
if ((waveState.openBlockers || []).some((b) => b.kind === "timeout")) {
|
|
788
|
+
return "timeout";
|
|
789
|
+
}
|
|
790
|
+
return "gate-failure";
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function collectResumeHumanInputBlockers(waveState) {
|
|
794
|
+
return (waveState.openBlockers || [])
|
|
795
|
+
.filter((b) => b.kind === "human-input")
|
|
796
|
+
.map((b) => ({
|
|
797
|
+
taskId: b.taskId || b.id || null,
|
|
798
|
+
title: b.title || b.detail || null,
|
|
799
|
+
assigneeAgentId: b.agentId || b.assigneeAgentId || null,
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function collectResumeGateBlockers(gateSnapshot) {
|
|
804
|
+
if (!gateSnapshot?.overall || gateSnapshot.overall.ok) {
|
|
805
|
+
return [];
|
|
806
|
+
}
|
|
807
|
+
return [{
|
|
808
|
+
gate: gateSnapshot.overall.gate,
|
|
809
|
+
statusCode: gateSnapshot.overall.statusCode,
|
|
810
|
+
detail: gateSnapshot.overall.detail || null,
|
|
811
|
+
agentId: gateSnapshot.overall.agentId || null,
|
|
812
|
+
}];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function collectResumeExecutorChanges(waveState) {
|
|
816
|
+
const retryTargets = waveState.retryTargetSet || [];
|
|
817
|
+
return (Array.isArray(retryTargets) ? retryTargets : [])
|
|
818
|
+
.filter((t) => t.reason === "rate-limit-exhausted" || t.reason === "rate-limit" || t.retriesExhausted === true)
|
|
819
|
+
.map((t) => ({
|
|
820
|
+
agentId: t.agentId,
|
|
821
|
+
currentExecutor: t.currentExecutor || t.executor || null,
|
|
822
|
+
suggestedFallback: "claude",
|
|
823
|
+
reason: t.reason || "rate-limit-exhausted",
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Deterministic resume planner operating on reducer output (WaveState).
|
|
829
|
+
* Pure function — no file I/O.
|
|
830
|
+
*/
|
|
831
|
+
export function buildResumePlan(waveState, options = {}) {
|
|
832
|
+
const waveDefinition = options.waveDefinition || {};
|
|
833
|
+
const lanePaths = options.lanePaths || {};
|
|
834
|
+
const reason = classifyResumeReason(waveState);
|
|
835
|
+
const canResume = reason !== "all-gates-pass";
|
|
836
|
+
const pendingAgentIds = waveState.closureEligibility?.pendingAgentIds || [];
|
|
837
|
+
const provenAgentIds = waveState.closureEligibility?.ownedSliceProvenAgentIds || [];
|
|
838
|
+
const invalidatedAgentIds = [...pendingAgentIds].sort();
|
|
839
|
+
const reusableAgentIds = [...provenAgentIds].sort();
|
|
840
|
+
const proofBundles =
|
|
841
|
+
waveState.closureEligibility?.proofBundles ||
|
|
842
|
+
waveState.proofAvailability?.activeProofBundles ||
|
|
843
|
+
[];
|
|
844
|
+
const reusableProofBundleIds = proofBundles
|
|
845
|
+
.filter((b) => b.state === "active" && reusableAgentIds.includes(b.agentId))
|
|
846
|
+
.map((b) => b.proofBundleId || b.id)
|
|
847
|
+
.sort();
|
|
848
|
+
const gateSnapshot = waveState.gateSnapshot || {};
|
|
849
|
+
let resumeFromPhase = "completed";
|
|
850
|
+
if (canResume && gateSnapshot.overall && !gateSnapshot.overall.ok) {
|
|
851
|
+
resumeFromPhase = phaseFromGate(gateSnapshot.overall.gate);
|
|
852
|
+
} else if (canResume) {
|
|
853
|
+
resumeFromPhase = "implementation";
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
resumePlanVersion: 1,
|
|
857
|
+
wave: waveDefinition.wave ?? waveState.wave ?? null,
|
|
858
|
+
lane: lanePaths.lane ?? waveState.lane ?? null,
|
|
859
|
+
attempt: waveState.attempt ?? null,
|
|
860
|
+
reason,
|
|
861
|
+
canResume,
|
|
862
|
+
invalidatedAgentIds,
|
|
863
|
+
reusableAgentIds,
|
|
864
|
+
reusableProofBundleIds,
|
|
865
|
+
resumeFromPhase,
|
|
866
|
+
executorChanges: collectResumeExecutorChanges(waveState),
|
|
867
|
+
humanInputBlockers: collectResumeHumanInputBlockers(waveState),
|
|
868
|
+
gateBlockers: collectResumeGateBlockers(gateSnapshot),
|
|
869
|
+
closureEligibility: waveState.closureEligibility || null,
|
|
870
|
+
deterministic: true,
|
|
871
|
+
createdAt: toIsoTimestamp(),
|
|
872
|
+
};
|
|
873
|
+
}
|