@chllming/wave-orchestration 0.8.5 → 0.8.7

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 (58) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +14 -9
  3. package/docs/README.md +3 -1
  4. package/docs/context7/bundles.json +19 -20
  5. package/docs/context7/planner-agent/README.md +4 -1
  6. package/docs/guides/author-and-run-waves.md +4 -1
  7. package/docs/guides/planner.md +3 -1
  8. package/docs/guides/signal-wrappers.md +165 -0
  9. package/docs/guides/terminal-surfaces.md +15 -0
  10. package/docs/plans/context7-wave-orchestrator.md +24 -7
  11. package/docs/plans/current-state.md +7 -3
  12. package/docs/plans/end-state-architecture.md +16 -4
  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 +179 -72
  16. package/docs/plans/wave-orchestrator.md +11 -5
  17. package/docs/reference/cli-reference.md +21 -4
  18. package/docs/reference/coordination-and-closure.md +26 -5
  19. package/docs/reference/live-proof-waves.md +9 -0
  20. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  21. package/docs/reference/runtime-config/README.md +9 -3
  22. package/docs/reference/sample-waves.md +5 -5
  23. package/docs/reference/skills.md +9 -1
  24. package/docs/reference/wave-control.md +18 -0
  25. package/docs/reference/wave-planning-lessons.md +7 -1
  26. package/docs/research/coordination-failure-review.md +6 -6
  27. package/package.json +1 -1
  28. package/releases/manifest.json +38 -0
  29. package/scripts/context7-api-check.sh +57 -13
  30. package/scripts/wave-orchestrator/agent-state.mjs +42 -0
  31. package/scripts/wave-orchestrator/autonomous.mjs +42 -6
  32. package/scripts/wave-orchestrator/clarification-triage.mjs +4 -3
  33. package/scripts/wave-orchestrator/control-cli.mjs +145 -11
  34. package/scripts/wave-orchestrator/control-plane.mjs +12 -1
  35. package/scripts/wave-orchestrator/coordination-store.mjs +124 -4
  36. package/scripts/wave-orchestrator/coordination.mjs +35 -0
  37. package/scripts/wave-orchestrator/executors.mjs +11 -6
  38. package/scripts/wave-orchestrator/gate-engine.mjs +5 -5
  39. package/scripts/wave-orchestrator/install.mjs +2 -0
  40. package/scripts/wave-orchestrator/launcher-runtime.mjs +12 -1
  41. package/scripts/wave-orchestrator/launcher.mjs +236 -0
  42. package/scripts/wave-orchestrator/ledger.mjs +14 -12
  43. package/scripts/wave-orchestrator/reducer-snapshot.mjs +8 -6
  44. package/scripts/wave-orchestrator/retry-engine.mjs +19 -11
  45. package/scripts/wave-orchestrator/routing-state.mjs +50 -3
  46. package/scripts/wave-orchestrator/session-supervisor.mjs +119 -10
  47. package/scripts/wave-orchestrator/shared.mjs +1 -0
  48. package/scripts/wave-orchestrator/signals.mjs +681 -0
  49. package/scripts/wave-orchestrator/task-entity.mjs +4 -4
  50. package/scripts/wave-orchestrator/terminals.mjs +14 -14
  51. package/scripts/wave-orchestrator/wave-control-schema.mjs +2 -0
  52. package/scripts/wave-orchestrator/wave-files.mjs +15 -21
  53. package/scripts/wave-orchestrator/wave-state-reducer.mjs +72 -5
  54. package/scripts/wave-status.sh +200 -0
  55. package/scripts/wave-watch.sh +200 -0
  56. package/skills/README.md +3 -0
  57. package/skills/signal-hygiene/SKILL.md +51 -0
  58. package/skills/signal-hygiene/skill.json +20 -0
@@ -0,0 +1,681 @@
1
+ import path from "node:path";
2
+ import {
3
+ REPO_ROOT,
4
+ ensureDirectory,
5
+ readJsonOrNull,
6
+ toIsoTimestamp,
7
+ writeJsonAtomic,
8
+ } from "./shared.mjs";
9
+
10
+ export const SIGNAL_HYGIENE_SKILL_ID = "signal-hygiene";
11
+ export const RESIDENT_SIGNAL_ID = "resident-orchestrator";
12
+
13
+ const ACTIONABLE_SIGNAL_KINDS = new Set([
14
+ "feedback-requested",
15
+ "feedback-answered",
16
+ "coordination-action",
17
+ "resume-ready",
18
+ "completed",
19
+ "failed",
20
+ ]);
21
+
22
+ const ACTIONABLE_TASK_TYPES = new Set([
23
+ "request",
24
+ "blocker",
25
+ "clarification",
26
+ "human-input",
27
+ "escalation",
28
+ ]);
29
+
30
+ function normalizeId(value, fallback = "unknown") {
31
+ return (
32
+ String(value || "")
33
+ .trim()
34
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
35
+ .replace(/-+/g, "-")
36
+ .replace(/^-+|-+$/g, "") || fallback
37
+ );
38
+ }
39
+
40
+ function relativePath(filePath) {
41
+ return filePath ? path.relative(REPO_ROOT, filePath) : null;
42
+ }
43
+
44
+ function normalizeString(value) {
45
+ const normalized = String(value || "").trim();
46
+ return normalized || null;
47
+ }
48
+
49
+ function normalizeArray(values) {
50
+ return Array.from(
51
+ new Set((Array.isArray(values) ? values : []).map((value) => String(value || "").trim()).filter(Boolean)),
52
+ ).sort();
53
+ }
54
+
55
+ function terminalWaveSignal(phase) {
56
+ const normalized = String(phase || "").trim().toLowerCase();
57
+ if (normalized === "completed") {
58
+ return "completed";
59
+ }
60
+ if (["failed", "timed_out", "timed-out"].includes(normalized)) {
61
+ return "failed";
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function pendingFeedbackRequests(feedbackRequests, agentId = "") {
67
+ return (Array.isArray(feedbackRequests) ? feedbackRequests : []).filter((request) => {
68
+ if (String(request?.status || "").trim().toLowerCase() !== "pending") {
69
+ return false;
70
+ }
71
+ return !agentId || String(request?.agentId || "").trim() === agentId;
72
+ });
73
+ }
74
+
75
+ function answeredFeedbackRequests(feedbackRequests, agentId = "") {
76
+ return (Array.isArray(feedbackRequests) ? feedbackRequests : []).filter((request) => {
77
+ if (String(request?.status || "").trim().toLowerCase() !== "answered") {
78
+ return false;
79
+ }
80
+ return !agentId || String(request?.agentId || "").trim() === agentId;
81
+ });
82
+ }
83
+
84
+ function isActionableTask(task) {
85
+ const taskType = String(task?.taskType || "").trim().toLowerCase();
86
+ const state = String(task?.state || "").trim().toLowerCase();
87
+ if (!ACTIONABLE_TASK_TYPES.has(taskType)) {
88
+ return false;
89
+ }
90
+ return ["open", "working", "input-required"].includes(state);
91
+ }
92
+
93
+ function firstActionableTask(tasks, agentId = "") {
94
+ return (Array.isArray(tasks) ? tasks : []).find((task) => {
95
+ if (!isActionableTask(task)) {
96
+ return false;
97
+ }
98
+ if (!agentId) {
99
+ return true;
100
+ }
101
+ return task.ownerAgentId === agentId || task.assigneeAgentId === agentId;
102
+ }) || null;
103
+ }
104
+
105
+ function selectedAgentIdsFromStatus(payload) {
106
+ if (Array.isArray(payload?.activeAttempt?.selectedAgentIds) && payload.activeAttempt.selectedAgentIds.length > 0) {
107
+ return normalizeArray(payload.activeAttempt.selectedAgentIds);
108
+ }
109
+ if (Array.isArray(payload?.rerunRequest?.selectedAgentIds) && payload.rerunRequest.selectedAgentIds.length > 0) {
110
+ return normalizeArray(payload.rerunRequest.selectedAgentIds);
111
+ }
112
+ if (Array.isArray(payload?.relaunchPlan?.selectedAgentIds) && payload.relaunchPlan.selectedAgentIds.length > 0) {
113
+ return normalizeArray(payload.relaunchPlan.selectedAgentIds);
114
+ }
115
+ return normalizeArray(
116
+ (Array.isArray(payload?.logicalAgents) ? payload.logicalAgents : [])
117
+ .filter((agent) => agent?.selectedForRerun || agent?.selectedForActiveAttempt)
118
+ .map((agent) => agent.agentId),
119
+ );
120
+ }
121
+
122
+ function readSignalAck(filePath) {
123
+ const payload = readJsonOrNull(filePath);
124
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
125
+ return null;
126
+ }
127
+ const version = Number.parseInt(String(payload.version ?? ""), 10);
128
+ return {
129
+ agentId: normalizeString(payload.agentId),
130
+ version: Number.isFinite(version) && version > 0 ? version : 0,
131
+ signal: normalizeString(payload.signal),
132
+ observedAt: normalizeString(payload.observedAt),
133
+ };
134
+ }
135
+
136
+ function writeSignalSnapshot(filePath, nextSnapshot, comparablePayload, decorateSnapshot = null) {
137
+ ensureDirectory(path.dirname(filePath));
138
+ const previous = readJsonOrNull(filePath);
139
+ const previousComparable = previous?.comparable || null;
140
+ const comparableChanged =
141
+ JSON.stringify(previousComparable || null) !== JSON.stringify(comparablePayload || null);
142
+ const previousVersion = Number.parseInt(String(previous?.version ?? ""), 10);
143
+ const version =
144
+ comparableChanged || !Number.isFinite(previousVersion) || previousVersion <= 0
145
+ ? Math.max(1, Number.isFinite(previousVersion) ? previousVersion + 1 : 1)
146
+ : previousVersion;
147
+ const changedAt =
148
+ comparableChanged || !normalizeString(previous?.changedAt)
149
+ ? toIsoTimestamp()
150
+ : previous.changedAt;
151
+ let payload = {
152
+ ...nextSnapshot,
153
+ version,
154
+ changedAt,
155
+ comparable: comparablePayload,
156
+ };
157
+ if (typeof decorateSnapshot === "function") {
158
+ payload = decorateSnapshot(payload) || payload;
159
+ }
160
+ writeJsonAtomic(filePath, payload);
161
+ return {
162
+ snapshot: payload,
163
+ changed: comparableChanged,
164
+ };
165
+ }
166
+
167
+ function finalizePersistedSnapshot(snapshot) {
168
+ if (!snapshot || typeof snapshot !== "object") {
169
+ return null;
170
+ }
171
+ const { comparable, ...rest } = snapshot;
172
+ return rest;
173
+ }
174
+
175
+ export function waveSignalPath(lanePaths, waveNumber) {
176
+ return path.join(lanePaths.signalsDir, `wave-${waveNumber}.json`);
177
+ }
178
+
179
+ export function waveSignalAgentDir(lanePaths, waveNumber) {
180
+ return path.join(lanePaths.signalsDir, `wave-${waveNumber}`);
181
+ }
182
+
183
+ export function agentSignalPath(lanePaths, waveNumber, agentId) {
184
+ return path.join(waveSignalAgentDir(lanePaths, waveNumber), `${normalizeId(agentId)}.json`);
185
+ }
186
+
187
+ export function agentSignalAckPath(lanePaths, waveNumber, agentId) {
188
+ return path.join(
189
+ waveSignalAgentDir(lanePaths, waveNumber),
190
+ "acks",
191
+ `${normalizeId(agentId)}.json`,
192
+ );
193
+ }
194
+
195
+ export function residentSignalPath(lanePaths, waveNumber) {
196
+ return agentSignalPath(lanePaths, waveNumber, RESIDENT_SIGNAL_ID);
197
+ }
198
+
199
+ export function residentSignalAckPath(lanePaths, waveNumber) {
200
+ return agentSignalAckPath(lanePaths, waveNumber, RESIDENT_SIGNAL_ID);
201
+ }
202
+
203
+ export function agentUsesSignalHygiene(agent) {
204
+ const resolvedIds = Array.isArray(agent?.skillsResolved?.ids) ? agent.skillsResolved.ids : [];
205
+ const explicitIds = Array.isArray(agent?.skills) ? agent.skills : [];
206
+ return [...resolvedIds, ...explicitIds].some(
207
+ (skillId) => String(skillId || "").trim().toLowerCase() === SIGNAL_HYGIENE_SKILL_ID,
208
+ );
209
+ }
210
+
211
+ export function waveSignalExitCode(signalSnapshot) {
212
+ const signal = String(signalSnapshot?.signal || "").trim().toLowerCase();
213
+ if (signal === "completed") {
214
+ return 0;
215
+ }
216
+ if (signal === "failed") {
217
+ return 40;
218
+ }
219
+ if (signal === "feedback-requested") {
220
+ return 20;
221
+ }
222
+ return 10;
223
+ }
224
+
225
+ export function buildSignalStatusLine(signalSnapshot, context = {}) {
226
+ const lane = normalizeString(context.lane) || normalizeString(signalSnapshot?.lane) || "main";
227
+ const wave = Number.parseInt(String(context.wave ?? signalSnapshot?.wave ?? 0), 10);
228
+ const agentId = normalizeString(context.agentId) || normalizeString(signalSnapshot?.agentId);
229
+ const targetKey = agentId ? "agent" : "agents";
230
+ const targetValue = agentId
231
+ ? agentId
232
+ : normalizeArray(signalSnapshot?.targetAgentIds || []).join(",") || "none";
233
+ const blocking = normalizeString(signalSnapshot?.blocking?.kind) || "none";
234
+ const attempt = Number.parseInt(String(signalSnapshot?.attempt ?? 0), 10) || 0;
235
+ const shouldWake =
236
+ typeof signalSnapshot?.shouldWake === "boolean"
237
+ ? signalSnapshot.shouldWake
238
+ ? "yes"
239
+ : "no"
240
+ : "n/a";
241
+ return [
242
+ `signal=${normalizeString(signalSnapshot?.signal) || "waiting"}`,
243
+ `lane=${lane}`,
244
+ `wave=${Number.isFinite(wave) ? wave : 0}`,
245
+ `phase=${normalizeString(signalSnapshot?.phase) || "unknown"}`,
246
+ `status=${normalizeString(signalSnapshot?.status) || "running"}`,
247
+ `blocking=${blocking}`,
248
+ `attempt=${attempt}`,
249
+ `${targetKey}=${targetValue}`,
250
+ `version=${Number.parseInt(String(signalSnapshot?.version ?? 0), 10) || 0}`,
251
+ `should_wake=${shouldWake}`,
252
+ ].join(" ");
253
+ }
254
+
255
+ function buildWaveComparable(snapshot) {
256
+ return {
257
+ status: snapshot.status,
258
+ phase: snapshot.phase,
259
+ signal: snapshot.signal,
260
+ reason: snapshot.reason,
261
+ attempt: snapshot.attempt,
262
+ blocking: snapshot.blocking,
263
+ selectionSource: snapshot.selectionSource,
264
+ targetAgentIds: snapshot.targetAgentIds,
265
+ };
266
+ }
267
+
268
+ function buildAgentComparable(snapshot) {
269
+ return {
270
+ status: snapshot.status,
271
+ phase: snapshot.phase,
272
+ signal: snapshot.signal,
273
+ reason: snapshot.reason,
274
+ attempt: snapshot.attempt,
275
+ targetAgentIds: normalizeArray(snapshot.targetAgentIds),
276
+ logicalState: snapshot.logicalState,
277
+ selectedForRerun: snapshot.selectedForRerun,
278
+ selectedForActiveAttempt: snapshot.selectedForActiveAttempt,
279
+ blocking: snapshot.blocking,
280
+ openFeedbackRequestIds: snapshot.openFeedbackRequestIds,
281
+ answeredFeedbackRequestIds: snapshot.answeredFeedbackRequestIds,
282
+ pendingCoordinationAction: snapshot.pendingCoordinationAction,
283
+ pendingTaskIds: snapshot.pendingTaskIds,
284
+ };
285
+ }
286
+
287
+ function buildWaveSignalCore(lanePaths, wave, statusPayload) {
288
+ const phase = normalizeString(statusPayload?.phase) || "unknown";
289
+ const blocking = statusPayload?.blockingEdge || null;
290
+ const selectedAgentIds = selectedAgentIdsFromStatus(statusPayload);
291
+ const terminal = terminalWaveSignal(phase);
292
+ if (terminal) {
293
+ return {
294
+ kind: "wave-signal",
295
+ lane: lanePaths.lane,
296
+ wave: wave.wave,
297
+ status: terminal === "completed" ? "completed" : "failed",
298
+ phase,
299
+ signal: terminal,
300
+ reason:
301
+ terminal === "completed"
302
+ ? `Wave ${wave.wave} completed.`
303
+ : `Wave ${wave.wave} entered a terminal failure state.`,
304
+ attempt: statusPayload?.activeAttempt?.attemptNumber || 0,
305
+ blocking,
306
+ selectionSource: normalizeString(statusPayload?.selectionSource) || "none",
307
+ targetAgentIds: [],
308
+ artifacts: {
309
+ signalPath: relativePath(waveSignalPath(lanePaths, wave.wave)),
310
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
311
+ sharedSummaryPath: relativePath(
312
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
313
+ ),
314
+ dashboardPath: relativePath(path.join(lanePaths.dashboardsDir, `wave-${wave.wave}.json`)),
315
+ },
316
+ };
317
+ }
318
+ const pendingFeedback = pendingFeedbackRequests(statusPayload?.feedbackRequests);
319
+ if (
320
+ pendingFeedback.length > 0 ||
321
+ ["human-input", "human-escalation"].includes(String(blocking?.kind || "").trim().toLowerCase())
322
+ ) {
323
+ return {
324
+ kind: "wave-signal",
325
+ lane: lanePaths.lane,
326
+ wave: wave.wave,
327
+ status: "blocked",
328
+ phase,
329
+ signal: "feedback-requested",
330
+ reason:
331
+ normalizeString(pendingFeedback[0]?.question) ||
332
+ normalizeString(blocking?.detail) ||
333
+ "Human feedback is required before the wave can continue.",
334
+ attempt: statusPayload?.activeAttempt?.attemptNumber || 0,
335
+ blocking,
336
+ selectionSource: normalizeString(statusPayload?.selectionSource) || "none",
337
+ targetAgentIds: normalizeArray(
338
+ pendingFeedback.map((request) => request.agentId).filter(Boolean),
339
+ ),
340
+ artifacts: {
341
+ signalPath: relativePath(waveSignalPath(lanePaths, wave.wave)),
342
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
343
+ sharedSummaryPath: relativePath(
344
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
345
+ ),
346
+ dashboardPath: relativePath(path.join(lanePaths.dashboardsDir, `wave-${wave.wave}.json`)),
347
+ },
348
+ };
349
+ }
350
+ const answeredFeedback = answeredFeedbackRequests(statusPayload?.feedbackRequests);
351
+ if (answeredFeedback.length > 0) {
352
+ return {
353
+ kind: "wave-signal",
354
+ lane: lanePaths.lane,
355
+ wave: wave.wave,
356
+ status: "running",
357
+ phase,
358
+ signal: "feedback-answered",
359
+ reason:
360
+ normalizeString(answeredFeedback[0]?.responseText) ||
361
+ `Human feedback ${answeredFeedback[0]?.id || ""} was answered.`,
362
+ attempt: statusPayload?.activeAttempt?.attemptNumber || 0,
363
+ blocking,
364
+ selectionSource: normalizeString(statusPayload?.selectionSource) || "none",
365
+ targetAgentIds: normalizeArray(
366
+ answeredFeedback.map((request) => request.agentId).filter(Boolean),
367
+ ),
368
+ artifacts: {
369
+ signalPath: relativePath(waveSignalPath(lanePaths, wave.wave)),
370
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
371
+ sharedSummaryPath: relativePath(
372
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
373
+ ),
374
+ dashboardPath: relativePath(path.join(lanePaths.dashboardsDir, `wave-${wave.wave}.json`)),
375
+ },
376
+ };
377
+ }
378
+ const coordinationAction = firstActionableTask(statusPayload?.tasks);
379
+ if (coordinationAction) {
380
+ return {
381
+ kind: "wave-signal",
382
+ lane: lanePaths.lane,
383
+ wave: wave.wave,
384
+ status: blocking ? "blocked" : "running",
385
+ phase,
386
+ signal: "coordination-action",
387
+ reason: normalizeString(coordinationAction.title) || "Targeted coordination action is pending.",
388
+ attempt: statusPayload?.activeAttempt?.attemptNumber || 0,
389
+ blocking,
390
+ selectionSource: normalizeString(statusPayload?.selectionSource) || "none",
391
+ targetAgentIds: normalizeArray([
392
+ coordinationAction.assigneeAgentId,
393
+ coordinationAction.ownerAgentId,
394
+ ]),
395
+ artifacts: {
396
+ signalPath: relativePath(waveSignalPath(lanePaths, wave.wave)),
397
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
398
+ sharedSummaryPath: relativePath(
399
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
400
+ ),
401
+ dashboardPath: relativePath(path.join(lanePaths.dashboardsDir, `wave-${wave.wave}.json`)),
402
+ },
403
+ };
404
+ }
405
+ if (
406
+ !statusPayload?.activeAttempt &&
407
+ selectedAgentIds.length > 0 &&
408
+ ["rerun-request", "relaunch-plan"].includes(String(statusPayload?.selectionSource || ""))
409
+ ) {
410
+ return {
411
+ kind: "wave-signal",
412
+ lane: lanePaths.lane,
413
+ wave: wave.wave,
414
+ status: "running",
415
+ phase,
416
+ signal: "resume-ready",
417
+ reason: `Wave ${wave.wave} is ready to relaunch selected agents.`,
418
+ attempt: statusPayload?.activeAttempt?.attemptNumber || 0,
419
+ blocking,
420
+ selectionSource: normalizeString(statusPayload?.selectionSource) || "none",
421
+ targetAgentIds: selectedAgentIds,
422
+ artifacts: {
423
+ signalPath: relativePath(waveSignalPath(lanePaths, wave.wave)),
424
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
425
+ sharedSummaryPath: relativePath(
426
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
427
+ ),
428
+ dashboardPath: relativePath(path.join(lanePaths.dashboardsDir, `wave-${wave.wave}.json`)),
429
+ },
430
+ };
431
+ }
432
+ return {
433
+ kind: "wave-signal",
434
+ lane: lanePaths.lane,
435
+ wave: wave.wave,
436
+ status: blocking ? "blocked" : "running",
437
+ phase,
438
+ signal: statusPayload?.activeAttempt || blocking ? "waiting" : "stable",
439
+ reason:
440
+ normalizeString(blocking?.detail) ||
441
+ (statusPayload?.activeAttempt ? "Wave is still running." : "No new actionable signal."),
442
+ attempt: statusPayload?.activeAttempt?.attemptNumber || 0,
443
+ blocking,
444
+ selectionSource: normalizeString(statusPayload?.selectionSource) || "none",
445
+ targetAgentIds: selectedAgentIds,
446
+ artifacts: {
447
+ signalPath: relativePath(waveSignalPath(lanePaths, wave.wave)),
448
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
449
+ sharedSummaryPath: relativePath(
450
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
451
+ ),
452
+ dashboardPath: relativePath(path.join(lanePaths.dashboardsDir, `wave-${wave.wave}.json`)),
453
+ },
454
+ };
455
+ }
456
+
457
+ function buildAgentSignalCore(lanePaths, wave, statusPayload, logicalAgent) {
458
+ const agentId = logicalAgent.agentId;
459
+ const phase = normalizeString(statusPayload?.phase) || "unknown";
460
+ const terminal = terminalWaveSignal(phase);
461
+ const pendingFeedback = pendingFeedbackRequests(statusPayload?.feedbackRequests, agentId);
462
+ const answeredFeedback = answeredFeedbackRequests(statusPayload?.feedbackRequests, agentId);
463
+ const coordinationAction = firstActionableTask(statusPayload?.tasks, agentId);
464
+ const blocking =
465
+ statusPayload?.blockingEdge?.agentId === agentId ? statusPayload.blockingEdge : null;
466
+ const pendingTaskIds = normalizeArray(
467
+ (Array.isArray(statusPayload?.tasks) ? statusPayload.tasks : [])
468
+ .filter((task) => task.ownerAgentId === agentId || task.assigneeAgentId === agentId)
469
+ .filter((task) => isActionableTask(task))
470
+ .map((task) => task.taskId),
471
+ );
472
+ let signal = "stable";
473
+ let status = "waiting";
474
+ let reason = "No new actionable signal.";
475
+ if (terminal === "completed") {
476
+ signal = "completed";
477
+ status = "completed";
478
+ reason = `Wave ${wave.wave} completed.`;
479
+ } else if (terminal === "failed") {
480
+ signal = "failed";
481
+ status = "failed";
482
+ reason =
483
+ normalizeString(logicalAgent.reason) ||
484
+ `Wave ${wave.wave} entered a terminal failure state.`;
485
+ } else if (pendingFeedback.length > 0) {
486
+ signal = "feedback-requested";
487
+ status = "blocked";
488
+ reason = normalizeString(pendingFeedback[0]?.question) || "Human feedback is pending.";
489
+ } else if (answeredFeedback.length > 0) {
490
+ signal = "feedback-answered";
491
+ status = "running";
492
+ reason =
493
+ normalizeString(answeredFeedback[0]?.responseText) ||
494
+ `Human feedback ${answeredFeedback[0]?.id || ""} was answered.`;
495
+ } else if (coordinationAction) {
496
+ signal = "coordination-action";
497
+ status = String(coordinationAction.state || "").trim().toLowerCase() === "input-required"
498
+ ? "blocked"
499
+ : "running";
500
+ reason = normalizeString(coordinationAction.title) || "Targeted coordination action is pending.";
501
+ } else if (logicalAgent.state === "needs-rerun") {
502
+ signal = "failed";
503
+ status = "failed";
504
+ reason =
505
+ normalizeString(logicalAgent.reason) ||
506
+ `Agent ${agentId} needs another run before the wave can complete.`;
507
+ } else if (logicalAgent.selectedForRerun && !logicalAgent.selectedForActiveAttempt) {
508
+ signal = "resume-ready";
509
+ status = "running";
510
+ reason = `Agent ${agentId} was selected for the next resume pass.`;
511
+ } else if (logicalAgent.state === "working" || logicalAgent.selectedForActiveAttempt) {
512
+ signal = "waiting";
513
+ status = "running";
514
+ reason = normalizeString(logicalAgent.reason) || `Agent ${agentId} is currently working.`;
515
+ } else if (logicalAgent.state === "blocked") {
516
+ signal = "waiting";
517
+ status = "blocked";
518
+ reason = normalizeString(logicalAgent.reason) || "Agent is blocked on an external dependency.";
519
+ }
520
+ return {
521
+ kind: "agent-signal",
522
+ lane: lanePaths.lane,
523
+ wave: wave.wave,
524
+ agentId,
525
+ status,
526
+ phase,
527
+ signal,
528
+ reason,
529
+ attempt: statusPayload?.activeAttempt?.attemptNumber || 0,
530
+ logicalState: normalizeString(logicalAgent.state) || "planned",
531
+ selectedForRerun: logicalAgent.selectedForRerun === true,
532
+ selectedForActiveAttempt: logicalAgent.selectedForActiveAttempt === true,
533
+ blocking,
534
+ pendingTaskIds,
535
+ pendingCoordinationAction: coordinationAction
536
+ ? {
537
+ taskId: coordinationAction.taskId,
538
+ taskType: coordinationAction.taskType,
539
+ state: coordinationAction.state,
540
+ title: coordinationAction.title,
541
+ }
542
+ : null,
543
+ openFeedbackRequestIds: normalizeArray(pendingFeedback.map((request) => request.id)),
544
+ answeredFeedbackRequestIds: normalizeArray(answeredFeedback.map((request) => request.id)),
545
+ artifacts: {
546
+ signalPath: relativePath(agentSignalPath(lanePaths, wave.wave, agentId)),
547
+ ackPath: relativePath(agentSignalAckPath(lanePaths, wave.wave, agentId)),
548
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
549
+ sharedSummaryPath: relativePath(
550
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
551
+ ),
552
+ inboxPath: relativePath(path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, `${agentId}.md`)),
553
+ },
554
+ };
555
+ }
556
+
557
+ function buildResidentSignalCore(lanePaths, wave, waveSignal) {
558
+ return {
559
+ kind: "resident-orchestrator-signal",
560
+ lane: lanePaths.lane,
561
+ wave: wave.wave,
562
+ agentId: RESIDENT_SIGNAL_ID,
563
+ status: waveSignal.status,
564
+ phase: waveSignal.phase,
565
+ signal: waveSignal.signal,
566
+ reason: waveSignal.reason,
567
+ attempt: waveSignal.attempt,
568
+ targetAgentIds: normalizeArray(waveSignal.targetAgentIds),
569
+ artifacts: {
570
+ signalPath: relativePath(residentSignalPath(lanePaths, wave.wave)),
571
+ ackPath: relativePath(residentSignalAckPath(lanePaths, wave.wave)),
572
+ coordinationLogPath: relativePath(path.join(lanePaths.coordinationDir, `wave-${wave.wave}.jsonl`)),
573
+ messageBoardPath: relativePath(path.join(lanePaths.messageboardsDir, `wave-${wave.wave}.md`)),
574
+ sharedSummaryPath: relativePath(
575
+ path.join(lanePaths.inboxesDir, `wave-${wave.wave}`, "shared-summary.md"),
576
+ ),
577
+ dashboardPath: relativePath(path.join(lanePaths.dashboardsDir, `wave-${wave.wave}.json`)),
578
+ triagePath: relativePath(path.join(lanePaths.feedbackTriageDir, `wave-${wave.wave}.jsonl`)),
579
+ },
580
+ };
581
+ }
582
+
583
+ function withAck(snapshot, ack) {
584
+ const version = Number.parseInt(String(snapshot?.version ?? 0), 10) || 0;
585
+ const ackVersion = Number.parseInt(String(ack?.version ?? 0), 10) || 0;
586
+ const actionable = ACTIONABLE_SIGNAL_KINDS.has(String(snapshot?.signal || "").trim().toLowerCase());
587
+ return {
588
+ ...snapshot,
589
+ ack: ack
590
+ ? {
591
+ agentId: ack.agentId,
592
+ version: ackVersion,
593
+ signal: ack.signal,
594
+ observedAt: ack.observedAt,
595
+ }
596
+ : null,
597
+ shouldWake: actionable && ackVersion < version,
598
+ };
599
+ }
600
+
601
+ export function buildSignalProjectionSet({ lanePaths, wave, statusPayload, includeResident = false }) {
602
+ const waveSignal = buildWaveSignalCore(lanePaths, wave, statusPayload);
603
+ const agentSignals = (Array.isArray(statusPayload?.logicalAgents) ? statusPayload.logicalAgents : []).map(
604
+ (logicalAgent) => buildAgentSignalCore(lanePaths, wave, statusPayload, logicalAgent),
605
+ );
606
+ return {
607
+ wave: waveSignal,
608
+ agents: agentSignals,
609
+ resident: includeResident ? buildResidentSignalCore(lanePaths, wave, waveSignal) : null,
610
+ };
611
+ }
612
+
613
+ export function syncWaveSignalProjections({
614
+ lanePaths,
615
+ wave,
616
+ statusPayload,
617
+ includeResident = false,
618
+ }) {
619
+ ensureDirectory(lanePaths.signalsDir);
620
+ ensureDirectory(waveSignalAgentDir(lanePaths, wave.wave));
621
+ ensureDirectory(path.join(waveSignalAgentDir(lanePaths, wave.wave), "acks"));
622
+ const built = buildSignalProjectionSet({
623
+ lanePaths,
624
+ wave,
625
+ statusPayload,
626
+ includeResident,
627
+ });
628
+ const waveWrite = writeSignalSnapshot(
629
+ waveSignalPath(lanePaths, wave.wave),
630
+ built.wave,
631
+ buildWaveComparable(built.wave),
632
+ );
633
+ const agentResults = [];
634
+ for (const agentSignal of built.agents) {
635
+ const ack = readSignalAck(agentSignalAckPath(lanePaths, wave.wave, agentSignal.agentId));
636
+ const agentWrite = writeSignalSnapshot(
637
+ agentSignalPath(lanePaths, wave.wave, agentSignal.agentId),
638
+ agentSignal,
639
+ buildAgentComparable(agentSignal),
640
+ (snapshot) => withAck(snapshot, ack),
641
+ );
642
+ agentResults.push({
643
+ agentId: agentSignal.agentId,
644
+ changed: agentWrite.changed,
645
+ snapshot: finalizePersistedSnapshot(agentWrite.snapshot),
646
+ });
647
+ }
648
+ let residentResult = null;
649
+ if (built.resident) {
650
+ const ack = readSignalAck(residentSignalAckPath(lanePaths, wave.wave));
651
+ const residentWrite = writeSignalSnapshot(
652
+ residentSignalPath(lanePaths, wave.wave),
653
+ built.resident,
654
+ buildAgentComparable({
655
+ ...built.resident,
656
+ logicalState: null,
657
+ selectedForRerun: false,
658
+ selectedForActiveAttempt: false,
659
+ blocking: null,
660
+ pendingTaskIds: [],
661
+ pendingCoordinationAction: null,
662
+ openFeedbackRequestIds: [],
663
+ answeredFeedbackRequestIds: [],
664
+ }),
665
+ (snapshot) => withAck(snapshot, ack),
666
+ );
667
+ residentResult = {
668
+ agentId: RESIDENT_SIGNAL_ID,
669
+ changed: residentWrite.changed,
670
+ snapshot: finalizePersistedSnapshot(residentWrite.snapshot),
671
+ };
672
+ }
673
+ return {
674
+ wave: {
675
+ changed: waveWrite.changed,
676
+ snapshot: finalizePersistedSnapshot(waveWrite.snapshot),
677
+ },
678
+ agents: agentResults,
679
+ resident: residentResult,
680
+ };
681
+ }