@chllming/wave-orchestration 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +7 -7
  3. package/docs/README.md +3 -3
  4. package/docs/concepts/operating-modes.md +1 -1
  5. package/docs/guides/author-and-run-waves.md +1 -1
  6. package/docs/guides/planner.md +2 -2
  7. package/docs/guides/recommendations-0.9.15.md +83 -0
  8. package/docs/guides/sandboxed-environments.md +2 -2
  9. package/docs/guides/signal-wrappers.md +10 -0
  10. package/docs/plans/agent-first-closure-hardening.md +612 -0
  11. package/docs/plans/current-state.md +3 -3
  12. package/docs/plans/end-state-architecture.md +1 -1
  13. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  14. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  15. package/docs/plans/migration.md +75 -20
  16. package/docs/reference/cli-reference.md +34 -1
  17. package/docs/reference/coordination-and-closure.md +16 -1
  18. package/docs/reference/npmjs-token-publishing.md +3 -3
  19. package/docs/reference/package-publishing-flow.md +13 -11
  20. package/docs/reference/runtime-config/README.md +2 -2
  21. package/docs/reference/sample-waves.md +5 -5
  22. package/docs/reference/skills.md +1 -1
  23. package/docs/reference/wave-control.md +1 -1
  24. package/docs/roadmap.md +5 -3
  25. package/package.json +1 -1
  26. package/releases/manifest.json +35 -0
  27. package/scripts/wave-orchestrator/agent-state.mjs +221 -313
  28. package/scripts/wave-orchestrator/artifact-schemas.mjs +37 -2
  29. package/scripts/wave-orchestrator/closure-adjudicator.mjs +311 -0
  30. package/scripts/wave-orchestrator/control-cli.mjs +212 -18
  31. package/scripts/wave-orchestrator/dashboard-state.mjs +40 -0
  32. package/scripts/wave-orchestrator/derived-state-engine.mjs +3 -0
  33. package/scripts/wave-orchestrator/gate-engine.mjs +140 -3
  34. package/scripts/wave-orchestrator/install.mjs +1 -1
  35. package/scripts/wave-orchestrator/launcher.mjs +49 -10
  36. package/scripts/wave-orchestrator/signal-cli.mjs +271 -0
  37. package/scripts/wave-orchestrator/structured-signal-parser.mjs +499 -0
  38. package/scripts/wave-orchestrator/task-entity.mjs +13 -4
  39. package/scripts/wave.mjs +9 -0
@@ -0,0 +1,499 @@
1
+ const STRUCTURED_SIGNAL_LINE_REGEX = /^\[wave-[a-z0-9-]+(?:\]|\s|=|$).*$/i;
2
+ const WRAPPED_STRUCTURED_SIGNAL_LINE_REGEX = /^`\[wave-[^`]+`$/;
3
+ const STRUCTURED_SIGNAL_LIST_PREFIX_REGEX = /^(?:[-*+]|\d+\.)\s+/;
4
+ const TAG_MATCH_REGEX = /^\[wave-([a-z0-9-]+)\](.*)$/i;
5
+ const KEY_START_REGEX = /(^|\s)([a-z][a-z0-9_-]*)=/gi;
6
+
7
+ const KIND_BY_TAG = {
8
+ proof: "proof",
9
+ "doc-delta": "docDelta",
10
+ "doc-closure": "docClosure",
11
+ integration: "integration",
12
+ eval: "eval",
13
+ security: "security",
14
+ design: "design",
15
+ gate: "gate",
16
+ gap: "gap",
17
+ component: "component",
18
+ };
19
+
20
+ function cleanText(value) {
21
+ return String(value || "").trim();
22
+ }
23
+
24
+ function parseCsv(value) {
25
+ return cleanText(value)
26
+ .split(",")
27
+ .map((item) => item.trim())
28
+ .filter(Boolean);
29
+ }
30
+
31
+ function parseIntValue(value) {
32
+ return Number.parseInt(String(value || "0"), 10) || 0;
33
+ }
34
+
35
+ const SIGNAL_SPECS = {
36
+ proof: {
37
+ requiredKeys: ["completion", "durability", "proof", "state"],
38
+ orderedKeys: ["completion", "durability", "proof", "state", "detail"],
39
+ normalize(values) {
40
+ const normalizedState = cleanText(values.state).toLowerCase() === "complete" ? "met" : cleanText(values.state).toLowerCase();
41
+ const completion = cleanText(values.completion).toLowerCase();
42
+ const durability = cleanText(values.durability).toLowerCase();
43
+ const proof = cleanText(values.proof).toLowerCase();
44
+ if (!["contract", "integrated", "authoritative", "live"].includes(completion)) {
45
+ return null;
46
+ }
47
+ if (!["none", "ephemeral", "durable"].includes(durability)) {
48
+ return null;
49
+ }
50
+ if (!["unit", "integration", "live"].includes(proof)) {
51
+ return null;
52
+ }
53
+ if (!["met", "gap"].includes(normalizedState)) {
54
+ return null;
55
+ }
56
+ return {
57
+ values: {
58
+ completion,
59
+ durability,
60
+ proof,
61
+ state: normalizedState,
62
+ detail: cleanText(values.detail),
63
+ },
64
+ };
65
+ },
66
+ },
67
+ docDelta: {
68
+ requiredKeys: ["state"],
69
+ orderedKeys: ["state", "paths", "detail"],
70
+ normalize(values) {
71
+ const state = cleanText(values.state).toLowerCase();
72
+ if (!["none", "owned", "shared-plan"].includes(state)) {
73
+ return null;
74
+ }
75
+ return {
76
+ values: {
77
+ state,
78
+ paths: parseCsv(values.paths).join(","),
79
+ detail: cleanText(values.detail),
80
+ },
81
+ };
82
+ },
83
+ },
84
+ docClosure: {
85
+ requiredKeys: ["state"],
86
+ orderedKeys: ["state", "paths", "detail"],
87
+ normalize(values) {
88
+ const state = cleanText(values.state).toLowerCase();
89
+ if (!["closed", "no-change", "delta"].includes(state)) {
90
+ return null;
91
+ }
92
+ return {
93
+ values: {
94
+ state,
95
+ paths: parseCsv(values.paths).join(","),
96
+ detail: cleanText(values.detail),
97
+ },
98
+ };
99
+ },
100
+ },
101
+ integration: {
102
+ requiredKeys: ["state", "claims", "conflicts", "blockers"],
103
+ orderedKeys: ["state", "claims", "conflicts", "blockers", "detail"],
104
+ normalize(values) {
105
+ const state = cleanText(values.state).toLowerCase();
106
+ if (!["ready-for-doc-closure", "needs-more-work"].includes(state)) {
107
+ return null;
108
+ }
109
+ return {
110
+ values: {
111
+ state,
112
+ claims: String(parseIntValue(values.claims)),
113
+ conflicts: String(parseIntValue(values.conflicts)),
114
+ blockers: String(parseIntValue(values.blockers)),
115
+ detail: cleanText(values.detail),
116
+ },
117
+ };
118
+ },
119
+ },
120
+ eval: {
121
+ requiredKeys: ["state", "targets", "benchmarks", "regressions"],
122
+ orderedKeys: ["state", "targets", "benchmarks", "regressions", "target_ids", "benchmark_ids", "detail"],
123
+ normalize(values) {
124
+ const state = cleanText(values.state).toLowerCase();
125
+ if (!["satisfied", "needs-more-work", "blocked"].includes(state)) {
126
+ return null;
127
+ }
128
+ return {
129
+ values: {
130
+ state,
131
+ targets: String(parseIntValue(values.targets)),
132
+ benchmarks: String(parseIntValue(values.benchmarks)),
133
+ regressions: String(parseIntValue(values.regressions)),
134
+ target_ids: parseCsv(values.target_ids).join(","),
135
+ benchmark_ids: parseCsv(values.benchmark_ids).join(","),
136
+ detail: cleanText(values.detail),
137
+ },
138
+ };
139
+ },
140
+ },
141
+ security: {
142
+ requiredKeys: ["state", "findings", "approvals"],
143
+ orderedKeys: ["state", "findings", "approvals", "detail"],
144
+ normalize(values) {
145
+ const state = cleanText(values.state).toLowerCase();
146
+ if (!["clear", "concerns", "blocked"].includes(state)) {
147
+ return null;
148
+ }
149
+ return {
150
+ values: {
151
+ state,
152
+ findings: String(parseIntValue(values.findings)),
153
+ approvals: String(parseIntValue(values.approvals)),
154
+ detail: cleanText(values.detail),
155
+ },
156
+ };
157
+ },
158
+ },
159
+ design: {
160
+ requiredKeys: ["state", "decisions", "assumptions", "open_questions"],
161
+ orderedKeys: ["state", "decisions", "assumptions", "open_questions", "detail"],
162
+ normalize(values) {
163
+ const state = cleanText(values.state).toLowerCase();
164
+ if (!["ready-for-implementation", "needs-clarification", "blocked"].includes(state)) {
165
+ return null;
166
+ }
167
+ return {
168
+ values: {
169
+ state,
170
+ decisions: String(parseIntValue(values.decisions)),
171
+ assumptions: String(parseIntValue(values.assumptions)),
172
+ open_questions: String(parseIntValue(values.open_questions)),
173
+ detail: cleanText(values.detail),
174
+ },
175
+ };
176
+ },
177
+ },
178
+ gate: {
179
+ requiredKeys: ["architecture", "integration", "durability", "live", "docs"],
180
+ orderedKeys: ["architecture", "integration", "durability", "live", "docs", "detail"],
181
+ normalize(values) {
182
+ const allowed = new Set(["pass", "concerns", "blocked", "gap"]);
183
+ const normalized = {
184
+ architecture: cleanText(values.architecture).toLowerCase(),
185
+ integration: cleanText(values.integration).toLowerCase(),
186
+ durability: cleanText(values.durability).toLowerCase(),
187
+ live: cleanText(values.live).toLowerCase(),
188
+ docs: cleanText(values.docs).toLowerCase(),
189
+ detail: cleanText(values.detail),
190
+ };
191
+ if (![normalized.architecture, normalized.integration, normalized.durability, normalized.live, normalized.docs].every((value) => allowed.has(value))) {
192
+ return null;
193
+ }
194
+ return { values: normalized };
195
+ },
196
+ },
197
+ gap: {
198
+ requiredKeys: ["kind"],
199
+ orderedKeys: ["kind", "detail"],
200
+ normalize(values) {
201
+ const kind = cleanText(values.kind).toLowerCase();
202
+ if (!["architecture", "integration", "durability", "ops", "docs"].includes(kind)) {
203
+ return null;
204
+ }
205
+ return {
206
+ values: {
207
+ kind,
208
+ detail: cleanText(values.detail),
209
+ },
210
+ };
211
+ },
212
+ },
213
+ component: {
214
+ requiredKeys: ["component", "level", "state"],
215
+ orderedKeys: ["component", "level", "state", "detail"],
216
+ normalize(values) {
217
+ const component = cleanText(values.component);
218
+ const level = cleanText(values.level).toLowerCase();
219
+ const state = cleanText(values.state).toLowerCase() === "complete" ? "met" : cleanText(values.state).toLowerCase();
220
+ if (!component || !level || !["met", "gap"].includes(state)) {
221
+ return null;
222
+ }
223
+ return {
224
+ values: {
225
+ component,
226
+ level,
227
+ state,
228
+ detail: cleanText(values.detail),
229
+ },
230
+ };
231
+ },
232
+ },
233
+ };
234
+
235
+ function buildEmptyStructuredSignalDiagnostics() {
236
+ return {
237
+ proof: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
238
+ docDelta: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
239
+ docClosure: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
240
+ integration: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
241
+ eval: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
242
+ security: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
243
+ design: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
244
+ gate: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
245
+ gap: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [] },
246
+ component: { rawCount: 0, acceptedCount: 0, normalizedCount: 0, rejectedCount: 0, rejectedSamples: [], normalizedSamples: [], unknownKeysSeen: [], seenComponentIds: [] },
247
+ };
248
+ }
249
+
250
+ function pushLimited(list, value, limit = 3) {
251
+ if (!value || list.length >= limit) {
252
+ return;
253
+ }
254
+ list.push(value);
255
+ }
256
+
257
+ export function normalizeStructuredSignalLine(line) {
258
+ const trimmed = cleanText(line);
259
+ if (!trimmed) {
260
+ return null;
261
+ }
262
+ const withoutListPrefix = trimmed.replace(STRUCTURED_SIGNAL_LIST_PREFIX_REGEX, "").trim();
263
+ if (STRUCTURED_SIGNAL_LINE_REGEX.test(withoutListPrefix)) {
264
+ return withoutListPrefix;
265
+ }
266
+ if (WRAPPED_STRUCTURED_SIGNAL_LINE_REGEX.test(withoutListPrefix)) {
267
+ return withoutListPrefix.slice(1, -1).trim();
268
+ }
269
+ return null;
270
+ }
271
+
272
+ function parseKeyValues(body) {
273
+ const matches = Array.from(String(body || "").matchAll(KEY_START_REGEX));
274
+ const values = {};
275
+ if (matches.length === 0) {
276
+ return values;
277
+ }
278
+ for (let index = 0; index < matches.length; index += 1) {
279
+ const match = matches[index];
280
+ const key = String(match[2] || "").toLowerCase();
281
+ const keyStart = match.index + match[1].length;
282
+ const valueStart = keyStart + key.length + 1;
283
+ const nextStart = index + 1 < matches.length ? matches[index + 1].index + matches[index + 1][1].length : String(body || "").length;
284
+ values[key] = cleanText(String(body || "").slice(valueStart, nextStart));
285
+ }
286
+ return values;
287
+ }
288
+
289
+ function buildCanonicalLine(tag, orderedKeys, values) {
290
+ const parts = [`[wave-${tag}]`];
291
+ for (const key of orderedKeys) {
292
+ const value = cleanText(values[key]);
293
+ if (!value) {
294
+ continue;
295
+ }
296
+ parts.push(`${key}=${value}`);
297
+ }
298
+ return parts.join(" ").trim();
299
+ }
300
+
301
+ export function parseStructuredSignalCandidate(line) {
302
+ const rawLine = cleanText(line);
303
+ if (!rawLine) {
304
+ return null;
305
+ }
306
+ const canonicalLine = normalizeStructuredSignalLine(rawLine);
307
+ if (!canonicalLine) {
308
+ return null;
309
+ }
310
+ const tagMatch = canonicalLine.match(TAG_MATCH_REGEX);
311
+ if (!tagMatch) {
312
+ return null;
313
+ }
314
+ const tag = cleanText(tagMatch[1]).toLowerCase();
315
+ const kind = KIND_BY_TAG[tag] || null;
316
+ const body = cleanText(tagMatch[2]);
317
+ const rawValues = parseKeyValues(body);
318
+ const spec = kind ? SIGNAL_SPECS[kind] : null;
319
+ const unknownKeys = spec
320
+ ? Object.keys(rawValues).filter((key) => !spec.orderedKeys.includes(key))
321
+ : Object.keys(rawValues);
322
+ let normalizedLine = canonicalLine;
323
+ let normalized = false;
324
+ let accepted = false;
325
+ let componentId = cleanText(rawValues.component || "");
326
+ if (spec) {
327
+ const requiredPresent = spec.requiredKeys.every((key) => cleanText(rawValues[key]));
328
+ if (requiredPresent) {
329
+ const parsed = spec.normalize(rawValues);
330
+ if (parsed?.values) {
331
+ accepted = true;
332
+ normalizedLine = buildCanonicalLine(tag, spec.orderedKeys, parsed.values);
333
+ normalized = normalizedLine !== canonicalLine;
334
+ if (kind === "component") {
335
+ componentId = cleanText(parsed.values.component || componentId);
336
+ }
337
+ }
338
+ }
339
+ }
340
+ return {
341
+ rawLine,
342
+ canonicalLine,
343
+ normalizedLine,
344
+ normalized,
345
+ accepted,
346
+ tag,
347
+ kind,
348
+ rawValues,
349
+ unknownKeys,
350
+ componentId: componentId || null,
351
+ };
352
+ }
353
+
354
+ function appendParsedStructuredSignalCandidates(lines, candidates, { requireAll = false } = {}) {
355
+ const parsedCandidates = [];
356
+ for (const line of lines || []) {
357
+ const candidate = parseStructuredSignalCandidate(line);
358
+ if (candidate) {
359
+ parsedCandidates.push(candidate);
360
+ continue;
361
+ }
362
+ if (requireAll) {
363
+ return;
364
+ }
365
+ }
366
+ candidates.push(...parsedCandidates);
367
+ }
368
+
369
+ function collectEmbeddedStructuredSignalTexts(value, texts) {
370
+ if (!value || typeof value !== "object") {
371
+ return;
372
+ }
373
+ if (Array.isArray(value)) {
374
+ for (const item of value) {
375
+ collectEmbeddedStructuredSignalTexts(item, texts);
376
+ }
377
+ return;
378
+ }
379
+ if (typeof value.text === "string") {
380
+ texts.push(value.text);
381
+ }
382
+ if (typeof value.aggregated_output === "string") {
383
+ texts.push(value.aggregated_output);
384
+ }
385
+ for (const nestedValue of Object.values(value)) {
386
+ if (nestedValue && typeof nestedValue === "object") {
387
+ collectEmbeddedStructuredSignalTexts(nestedValue, texts);
388
+ }
389
+ }
390
+ }
391
+
392
+ function extractEmbeddedStructuredSignalTextsFromJsonLine(line) {
393
+ const trimmed = cleanText(line);
394
+ if (!trimmed || !/^[{\[]/.test(trimmed)) {
395
+ return [];
396
+ }
397
+ try {
398
+ const payload = JSON.parse(trimmed);
399
+ const texts = [];
400
+ collectEmbeddedStructuredSignalTexts(payload, texts);
401
+ return texts.filter(Boolean);
402
+ } catch {
403
+ return [];
404
+ }
405
+ }
406
+
407
+ export function collectStructuredSignalCandidates(text) {
408
+ if (!text) {
409
+ return [];
410
+ }
411
+ const candidates = [];
412
+ let fenceLines = null;
413
+ for (const rawLine of String(text || "").split(/\r?\n/)) {
414
+ const embeddedTexts = extractEmbeddedStructuredSignalTextsFromJsonLine(rawLine);
415
+ for (const embeddedText of embeddedTexts) {
416
+ candidates.push(...collectStructuredSignalCandidates(embeddedText));
417
+ }
418
+ const trimmed = rawLine.trim();
419
+ if (/^```/.test(trimmed)) {
420
+ if (fenceLines === null) {
421
+ fenceLines = [];
422
+ continue;
423
+ }
424
+ appendParsedStructuredSignalCandidates(fenceLines, candidates, { requireAll: true });
425
+ fenceLines = null;
426
+ continue;
427
+ }
428
+ if (fenceLines !== null) {
429
+ if (!trimmed) {
430
+ continue;
431
+ }
432
+ fenceLines.push(rawLine);
433
+ continue;
434
+ }
435
+ const candidate = parseStructuredSignalCandidate(rawLine);
436
+ if (candidate) {
437
+ candidates.push(candidate);
438
+ }
439
+ }
440
+ if (fenceLines !== null) {
441
+ appendParsedStructuredSignalCandidates(fenceLines, candidates);
442
+ }
443
+ return candidates;
444
+ }
445
+
446
+ export function buildStructuredSignalDiagnostics(candidates) {
447
+ const diagnostics = buildEmptyStructuredSignalDiagnostics();
448
+ for (const candidate of candidates || []) {
449
+ if (!candidate?.kind || !diagnostics[candidate.kind]) {
450
+ continue;
451
+ }
452
+ const bucket = diagnostics[candidate.kind];
453
+ bucket.rawCount += 1;
454
+ if (candidate.kind === "component" && candidate.componentId) {
455
+ bucket.seenComponentIds.push(candidate.componentId);
456
+ }
457
+ for (const unknownKey of candidate.unknownKeys || []) {
458
+ if (!bucket.unknownKeysSeen.includes(unknownKey)) {
459
+ bucket.unknownKeysSeen.push(unknownKey);
460
+ }
461
+ }
462
+ if (candidate.accepted) {
463
+ bucket.acceptedCount += 1;
464
+ if (candidate.normalized) {
465
+ bucket.normalizedCount += 1;
466
+ pushLimited(bucket.normalizedSamples, {
467
+ from: candidate.rawLine,
468
+ to: candidate.normalizedLine,
469
+ });
470
+ }
471
+ continue;
472
+ }
473
+ bucket.rejectedCount += 1;
474
+ pushLimited(bucket.rejectedSamples, {
475
+ line: candidate.rawLine,
476
+ rawValues: candidate.rawValues,
477
+ unknownKeys: candidate.unknownKeys,
478
+ ...(candidate.kind === "component" && candidate.componentId ? { componentId: candidate.componentId } : {}),
479
+ });
480
+ }
481
+ diagnostics.component.seenComponentIds = Array.from(new Set(diagnostics.component.seenComponentIds)).sort();
482
+ for (const bucket of Object.values(diagnostics)) {
483
+ if (Array.isArray(bucket.unknownKeysSeen)) {
484
+ bucket.unknownKeysSeen.sort();
485
+ }
486
+ }
487
+ return diagnostics;
488
+ }
489
+
490
+ export function extractStructuredSignalPayload(text) {
491
+ const candidates = collectStructuredSignalCandidates(text);
492
+ return {
493
+ signalText: candidates
494
+ .filter((candidate) => candidate.accepted)
495
+ .map((candidate) => candidate.normalizedLine)
496
+ .join("\n"),
497
+ diagnostics: buildStructuredSignalDiagnostics(candidates),
498
+ };
499
+ }
@@ -905,10 +905,19 @@ export function evaluateOwnedSliceProven(task, agentResult, proofBundles = []) {
905
905
  }
906
906
 
907
907
  if (task.taskType === "documentation") {
908
- const validation = validateDocumentationClosureSummary(agent, agentResult);
909
- return validation.ok
910
- ? { proven: true, reason: "Documentation closure satisfied" }
911
- : { proven: false, reason: validation.detail || validation.statusCode };
908
+ const validation = validateDocumentationClosureSummary(agent, agentResult, {
909
+ allowFallbackOnEmptyRun: true,
910
+ });
911
+ if (validation.ok) {
912
+ return { proven: true, reason: "Documentation closure satisfied" };
913
+ }
914
+ // Allow fallback-eligible empty runs to pass at the task level;
915
+ // the gate engine will make the final call on whether surrounding
916
+ // state justifies auto-closure.
917
+ if (validation.eligibleForFallback) {
918
+ return { proven: true, reason: "Documentation closure fallback (empty run, deferred to gate)" };
919
+ }
920
+ return { proven: false, reason: validation.detail || validation.statusCode };
912
921
  }
913
922
 
914
923
  if (task.taskType === "security") {
package/scripts/wave.mjs CHANGED
@@ -28,6 +28,7 @@ function printHelp() {
28
28
  wave autonomous [autonomous options]
29
29
  wave feedback [feedback options]
30
30
  wave dashboard [dashboard options]
31
+ wave signal [signal helper options]
31
32
  wave local [local executor options]
32
33
  wave control [control-plane options]
33
34
  wave coord [coordination options]
@@ -110,6 +111,14 @@ if (["init", "upgrade", "self-update", "changelog", "doctor"].includes(subcomman
110
111
  console.error(`[wave] ${error instanceof Error ? error.message : String(error)}`);
111
112
  process.exit(1);
112
113
  }
114
+ } else if (subcommand === "signal") {
115
+ try {
116
+ const { runSignalCli } = await import("./wave-orchestrator/signal-cli.mjs");
117
+ await runSignalCli(rest);
118
+ } catch (error) {
119
+ console.error(`[wave] ${error instanceof Error ? error.message : String(error)}`);
120
+ process.exit(1);
121
+ }
113
122
  } else if (subcommand === "local") {
114
123
  const { runLocalExecutorCli } = await import("./wave-orchestrator/local-executor.mjs");
115
124
  try {