@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
@@ -15,14 +15,15 @@ import {
15
15
  waveControlPlaneLogPath,
16
16
  } from "./control-plane.mjs";
17
17
  import { readWaveLedger } from "./ledger.mjs";
18
- import { readRunExecutionSummary } from "./launcher-gates.mjs";
19
- import { buildResumePlan, clearWaveRelaunchPlan } from "./launcher-retry.mjs";
18
+ import { readRunResultEnvelope } from "./gate-engine.mjs";
19
+ import { buildResumePlan, clearWaveRelaunchPlan } from "./retry-engine.mjs";
20
20
  import { readWaveProofRegistry } from "./proof-registry.mjs";
21
21
  import { buildDependencySnapshot, buildRequestAssignments, syncAssignmentRecords } from "./routing-state.mjs";
22
22
  import { buildLanePaths } from "./shared.mjs";
23
23
  import { reduceWaveState } from "./wave-state-reducer.mjs";
24
24
  import { parseWaveFiles } from "./wave-files.mjs";
25
25
  import { writeWaveRetryOverride } from "./retry-control.mjs";
26
+ import { resolveWaveRoleBindings } from "./role-helpers.mjs";
26
27
 
27
28
  function coordinationLogPath(lanePaths, waveNumber) {
28
29
  return path.join(lanePaths.coordinationDir, `wave-${waveNumber}.jsonl`);
@@ -67,6 +68,9 @@ function taskRunInfoForAgent(lanePaths, wave, agent, proofRegistry) {
67
68
  const safeName = `wave-${wave.wave}-${agent.slug}`;
68
69
  return {
69
70
  agent,
71
+ lane: lanePaths.lane,
72
+ wave: wave.wave,
73
+ resultsDir: lanePaths.resultsDir,
70
74
  logPath: path.join(lanePaths.logsDir, `${safeName}.log`),
71
75
  statusPath: path.join(lanePaths.statusDir, `${safeName}.status`),
72
76
  proofRegistry,
@@ -207,10 +211,13 @@ export function buildResumePlanFromDisk({ lanePaths, wave }) {
207
211
  const agentRuns = wave.agents.map((agent) =>
208
212
  taskRunInfoForAgent(lanePaths, wave, agent, proofRegistry),
209
213
  );
210
- const agentResults = Object.fromEntries(
214
+ const agentEnvelopes = Object.fromEntries(
211
215
  agentRuns
212
- .map((runInfo) => [runInfo.agent.agentId, readRunExecutionSummary(runInfo, wave)])
213
- .filter(([, summary]) => Boolean(summary)),
216
+ .map((runInfo) => {
217
+ const envelopeResult = readRunResultEnvelope(runInfo, wave, { mode: "compat" });
218
+ return [runInfo.agent.agentId, envelopeResult?.valid ? envelopeResult.envelope : null];
219
+ })
220
+ .filter(([, envelope]) => Boolean(envelope)),
214
221
  );
215
222
  const coordinationState = readMaterializedCoordinationState(coordinationLogPath(lanePaths, wave.wave));
216
223
  const feedbackRequests = readWaveHumanFeedbackRequests({
@@ -232,16 +239,13 @@ export function buildResumePlanFromDisk({ lanePaths, wave }) {
232
239
  const reducerState = reduceWaveState({
233
240
  controlPlaneEvents: readControlPlaneEvents(waveControlPlaneLogPath(lanePaths, wave.wave)),
234
241
  coordinationRecords: coordinationState.latestRecords || [],
235
- agentResults,
242
+ agentEnvelopes,
236
243
  waveDefinition: wave,
237
244
  dependencyTickets: dependencySnapshot,
238
245
  feedbackRequests,
239
246
  laneConfig: {
240
247
  lane: lanePaths.lane,
241
- contQaAgentId: lanePaths.contQaAgentId || "A0",
242
- contEvalAgentId: lanePaths.contEvalAgentId || "E0",
243
- integrationAgentId: lanePaths.integrationAgentId || "A8",
244
- documentationAgentId: lanePaths.documentationAgentId || "A9",
248
+ ...resolveWaveRoleBindings(wave, lanePaths, wave.agents),
245
249
  validationMode: "live",
246
250
  evalTargets: wave.evalTargets,
247
251
  benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
@@ -287,3 +287,107 @@ export function computeHumanInputMetrics(requests) {
287
287
  : null,
288
288
  };
289
289
  }
290
+
291
+ export function buildHumanFeedbackWorkflowUpdate({
292
+ request,
293
+ lane,
294
+ waveNumber,
295
+ existingEscalation = null,
296
+ }) {
297
+ const question = request?.question || "n/a";
298
+ const context = request?.context ? `; context=${request.context}` : "";
299
+ const agentId = request?.agentId || "human";
300
+ const responseOperator = request?.responseOperator || "human-operator";
301
+ const responseText = request?.responseText || "(empty response)";
302
+ if (request?.status === "pending") {
303
+ return {
304
+ combinedEvent: {
305
+ level: "warn",
306
+ agentId: request.agentId,
307
+ message: `Human feedback requested (${request.id}): ${question}`,
308
+ },
309
+ coordinationNotice: {
310
+ event: "human_feedback_requested",
311
+ waves: [waveNumber],
312
+ status: "waiting-human",
313
+ details: `request_id=${request.id}; agent=${request.agentId}; question=${question}${context}`,
314
+ actionRequested:
315
+ `Launcher operator should ask or answer in the parent session, then run: pnpm exec wave control task act answer --lane ${lane} --wave ${waveNumber} --id ${request.id} --response "<answer>" --operator "<name>"`,
316
+ },
317
+ consoleLines: [
318
+ `[human-feedback] wave=${waveNumber} agent=${request.agentId} request=${request.id} pending: ${question}`,
319
+ `[human-feedback] respond with: pnpm exec wave control task act answer --lane ${lane} --wave ${waveNumber} --id ${request.id} --response "<answer>" --operator "<name>"`,
320
+ ],
321
+ coordinationUpdates: [
322
+ {
323
+ id: request.id,
324
+ lane,
325
+ wave: waveNumber,
326
+ agentId,
327
+ kind: "human-feedback",
328
+ targets: request.agentId ? [`agent:${request.agentId}`] : [],
329
+ priority: "high",
330
+ summary: question,
331
+ detail: request.context || "",
332
+ status: "open",
333
+ source: "feedback",
334
+ },
335
+ ],
336
+ triageUpdates: [],
337
+ };
338
+ }
339
+ if (request?.status === "answered") {
340
+ const escalationId = `escalation-${request.id}`;
341
+ const escalationRecord = {
342
+ id: escalationId,
343
+ lane,
344
+ wave: waveNumber,
345
+ agentId,
346
+ kind: "human-escalation",
347
+ targets:
348
+ existingEscalation?.targets ||
349
+ (request.agentId ? [`agent:${request.agentId}`] : []),
350
+ dependsOn: existingEscalation?.dependsOn || [],
351
+ closureCondition: existingEscalation?.closureCondition || "",
352
+ priority: "high",
353
+ summary: question,
354
+ detail: responseText,
355
+ artifactRefs: [request.id],
356
+ status: "resolved",
357
+ source: "feedback",
358
+ };
359
+ return {
360
+ combinedEvent: {
361
+ level: "info",
362
+ agentId: request.agentId,
363
+ message: `Human feedback answered (${request.id}) by ${responseOperator}: ${responseText}`,
364
+ },
365
+ coordinationNotice: {
366
+ event: "human_feedback_answered",
367
+ waves: [waveNumber],
368
+ status: "resolved",
369
+ details: `request_id=${request.id}; agent=${request.agentId}; operator=${responseOperator}; response=${responseText}`,
370
+ actionRequested: "None",
371
+ },
372
+ consoleLines: [],
373
+ coordinationUpdates: [
374
+ escalationRecord,
375
+ {
376
+ id: request.id,
377
+ lane,
378
+ wave: waveNumber,
379
+ agentId,
380
+ kind: "human-feedback",
381
+ targets: request.agentId ? [`agent:${request.agentId}`] : [],
382
+ priority: "high",
383
+ summary: question,
384
+ detail: responseText,
385
+ status: "resolved",
386
+ source: "feedback",
387
+ },
388
+ ],
389
+ triageUpdates: [escalationRecord],
390
+ };
391
+ }
392
+ return null;
393
+ }
@@ -0,0 +1,120 @@
1
+ import { readStatusCodeIfPresent } from "./dashboard-state.mjs";
2
+ import { resolveRetryOverrideRuns } from "./retry-control.mjs";
3
+ import {
4
+ applyPersistedRelaunchPlan,
5
+ persistedRelaunchPlanMatchesCurrentState,
6
+ resolveRelaunchRuns,
7
+ selectInitialWaveRuns,
8
+ selectReusablePreCompletedAgentIds,
9
+ } from "./retry-engine.mjs";
10
+
11
+ export function planInitialWaveAttempt({
12
+ agentRuns,
13
+ lanePaths,
14
+ wave,
15
+ derivedState,
16
+ proofRegistry,
17
+ retryOverride,
18
+ persistedRelaunchPlan,
19
+ }) {
20
+ const preCompletedAgentIds = selectReusablePreCompletedAgentIds(agentRuns, lanePaths, {
21
+ retryOverride,
22
+ wave,
23
+ derivedState,
24
+ proofRegistry,
25
+ });
26
+ const staleCompletedAgentIds = agentRuns
27
+ .filter(
28
+ (run) =>
29
+ !preCompletedAgentIds.has(run.agent.agentId) &&
30
+ readStatusCodeIfPresent(run.statusPath) === 0,
31
+ )
32
+ .map((run) => run.agent.agentId);
33
+
34
+ const persistedPlanIsCurrent =
35
+ !persistedRelaunchPlan ||
36
+ persistedRelaunchPlanMatchesCurrentState(
37
+ agentRuns,
38
+ persistedRelaunchPlan,
39
+ lanePaths,
40
+ wave,
41
+ );
42
+ const effectivePersistedPlan = persistedPlanIsCurrent ? persistedRelaunchPlan : null;
43
+ const availableRuns = agentRuns.filter((run) => !preCompletedAgentIds.has(run.agent.agentId));
44
+ const persistedRuns = applyPersistedRelaunchPlan(
45
+ availableRuns,
46
+ effectivePersistedPlan,
47
+ lanePaths,
48
+ wave,
49
+ );
50
+ const overrideResolution = resolveRetryOverrideRuns(availableRuns, retryOverride, lanePaths, wave);
51
+
52
+ let selectedRuns = [];
53
+ let source = "initial";
54
+ if (overrideResolution.unknownAgentIds.length === 0 && overrideResolution.runs.length > 0) {
55
+ selectedRuns = overrideResolution.runs;
56
+ source = "override";
57
+ } else if (persistedRuns.length > 0) {
58
+ selectedRuns = persistedRuns;
59
+ source = "persisted-relaunch";
60
+ } else {
61
+ selectedRuns = selectInitialWaveRuns(availableRuns, lanePaths, wave);
62
+ }
63
+
64
+ return {
65
+ preCompletedAgentIds,
66
+ staleCompletedAgentIds,
67
+ availableRuns,
68
+ selectedRuns,
69
+ source,
70
+ overrideResolution,
71
+ persistedPlanIsCurrent,
72
+ shouldClearPersistedRelaunchPlan: Boolean(
73
+ persistedRelaunchPlan && !persistedPlanIsCurrent,
74
+ ),
75
+ };
76
+ }
77
+
78
+ export function planRetryWaveAttempt({
79
+ agentRuns,
80
+ failures,
81
+ derivedState,
82
+ lanePaths,
83
+ wave,
84
+ retryOverride,
85
+ waveState = null,
86
+ }) {
87
+ const relaunchResolution = resolveRelaunchRuns(
88
+ agentRuns,
89
+ failures,
90
+ derivedState,
91
+ lanePaths,
92
+ wave,
93
+ waveState ? { waveState } : {},
94
+ );
95
+ const overrideResolution = resolveRetryOverrideRuns(
96
+ agentRuns,
97
+ retryOverride,
98
+ lanePaths,
99
+ wave,
100
+ );
101
+
102
+ let selectedRuns = [];
103
+ let source = "retry";
104
+ let barrier = relaunchResolution.barrier || null;
105
+ if (overrideResolution.unknownAgentIds.length === 0 && overrideResolution.runs.length > 0) {
106
+ selectedRuns = overrideResolution.runs;
107
+ barrier = null;
108
+ source = "override";
109
+ } else if (!barrier) {
110
+ selectedRuns = relaunchResolution.runs;
111
+ }
112
+
113
+ return {
114
+ selectedRuns,
115
+ source,
116
+ barrier,
117
+ relaunchResolution,
118
+ overrideResolution,
119
+ };
120
+ }
@@ -14,6 +14,7 @@ import { readStatusCodeIfPresent } from "./dashboard-state.mjs";
14
14
  import { buildExecutorLaunchSpec } from "./executors.mjs";
15
15
  import { hashAgentPromptFingerprint, prefetchContext7ForSelection } from "./context7.mjs";
16
16
  import { killTmuxSessionIfExists } from "./terminals.mjs";
17
+ import { resolveWaveRoleBindings } from "./role-helpers.mjs";
17
18
  import {
18
19
  resolveAgentSkills,
19
20
  summarizeResolvedSkills,
@@ -71,6 +72,7 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
71
72
  inboxText,
72
73
  promptOverride = "",
73
74
  orchestratorId,
75
+ attempt = 1,
74
76
  agentRateLimitRetries,
75
77
  agentRateLimitBaseDelaySeconds,
76
78
  agentRateLimitMaxDelaySeconds,
@@ -105,6 +107,7 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
105
107
  const prompt =
106
108
  String(promptOverride || "").trim() ||
107
109
  buildExecutionPrompt({
110
+ ...resolveWaveRoleBindings(resolvedWaveDefinition, lanePaths),
108
111
  lane: lanePaths.lane,
109
112
  wave,
110
113
  agent,
@@ -120,10 +123,6 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
120
123
  evalTargets: resolvedWaveDefinition.evalTargets,
121
124
  benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
122
125
  sharedPlanDocs: lanePaths.sharedPlanDocs,
123
- contQaAgentId: lanePaths.contQaAgentId,
124
- contEvalAgentId: lanePaths.contEvalAgentId,
125
- integrationAgentId: lanePaths.integrationAgentId,
126
- documentationAgentId: lanePaths.documentationAgentId,
127
126
  });
128
127
  const promptHash = hashAgentPromptFingerprint(agent);
129
128
  fs.writeFileSync(promptPath, `${prompt}\n`, "utf8");
@@ -216,8 +215,8 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
216
215
  `export WAVE_EXECUTOR_MODE=${shellQuote(resolvedExecutorMode)}`,
217
216
  ...executionLines,
218
217
  `node -e ${shellQuote(
219
- "const fs=require('node:fs'); const statusPath=process.argv[1]; const payload={code:Number(process.argv[2]),promptHash:process.argv[3]||null,orchestratorId:process.argv[4]||null,completedAt:new Date().toISOString()}; fs.writeFileSync(statusPath, JSON.stringify(payload, null, 2)+'\\n', 'utf8');",
220
- )} ${shellQuote(statusPath)} "$status" ${shellQuote(promptHash)} ${shellQuote(orchestratorId || "")}`,
218
+ "const fs=require('node:fs'); const statusPath=process.argv[1]; const payload={code:Number(process.argv[2]),promptHash:process.argv[3]||null,orchestratorId:process.argv[4]||null,attempt:Number(process.argv[5])||1,completedAt:new Date().toISOString()}; fs.writeFileSync(statusPath, JSON.stringify(payload, null, 2)+'\\n', 'utf8');",
219
+ )} ${shellQuote(statusPath)} "$status" ${shellQuote(promptHash)} ${shellQuote(orchestratorId || "")} ${shellQuote(String(attempt || 1))}`,
221
220
  `echo "[${lanePaths.lane}-wave-launcher] ${sessionName} finished with code $status"`,
222
221
  "exec bash -l",
223
222
  ].join("\n");