@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
@@ -124,6 +124,24 @@ export function normalizePhaseState(value) {
124
124
  : null;
125
125
  }
126
126
 
127
+ function deriveWaveProjectionTriplet(status) {
128
+ const normalized = String(status || "").trim().toLowerCase();
129
+ if (["running", "retrying"].includes(normalized)) {
130
+ return { executionState: "active", closureState: "evaluating", controllerState: "active" };
131
+ }
132
+ if (normalized === "completed") {
133
+ return { executionState: "settled", closureState: "passed", controllerState: "idle" };
134
+ }
135
+ if (["blocked", "failed", "timed_out"].includes(normalized)) {
136
+ return {
137
+ executionState: "settled",
138
+ closureState: "blocked",
139
+ controllerState: normalized === "blocked" ? "relaunch-planned" : "stale",
140
+ };
141
+ }
142
+ return { executionState: "pending", closureState: "pending", controllerState: "idle" };
143
+ }
144
+
127
145
  export function buildWaveDashboardState({
128
146
  lane,
129
147
  wave,
@@ -134,12 +152,16 @@ export function buildWaveDashboardState({
134
152
  agentRuns,
135
153
  }) {
136
154
  const now = toIsoTimestamp();
155
+ const projectionStates = deriveWaveProjectionTriplet("running");
137
156
  return normalizeWaveDashboardState({
138
157
  lane,
139
158
  wave,
140
159
  waveFile,
141
160
  runTag,
142
161
  status: "running",
162
+ executionState: projectionStates.executionState,
163
+ closureState: projectionStates.closureState,
164
+ controllerState: projectionStates.controllerState,
143
165
  attempt: 0,
144
166
  maxAttempts,
145
167
  startedAt: now,
@@ -192,10 +214,14 @@ export function buildGlobalDashboardState({
192
214
  feedbackRequestsDir,
193
215
  }) {
194
216
  const now = toIsoTimestamp();
217
+ const projectionStates = deriveWaveProjectionTriplet("running");
195
218
  return normalizeGlobalDashboardState({
196
219
  lane,
197
220
  runId: Math.random().toString(16).slice(2, 14),
198
221
  status: "running",
222
+ executionState: projectionStates.executionState,
223
+ closureState: projectionStates.closureState,
224
+ controllerState: projectionStates.controllerState,
199
225
  startedAt: now,
200
226
  updatedAt: now,
201
227
  options: {
@@ -223,6 +249,9 @@ export function buildGlobalDashboardState({
223
249
  wave: wave.wave,
224
250
  waveFile: wave.file,
225
251
  status: "pending",
252
+ executionState: "pending",
253
+ closureState: "pending",
254
+ controllerState: "idle",
226
255
  attempt: 0,
227
256
  maxAttempts: options.maxRetriesPerWave + 1,
228
257
  dashboardPath: path.relative(
@@ -260,8 +289,12 @@ export function buildGlobalDashboardState({
260
289
 
261
290
  export function writeWaveDashboard(dashboardPath, state) {
262
291
  ensureDirectory(path.dirname(dashboardPath));
292
+ const projectionStates = deriveWaveProjectionTriplet(state?.status);
263
293
  const normalized = normalizeWaveDashboardState({
264
294
  ...state,
295
+ executionState: state?.executionState || projectionStates.executionState,
296
+ closureState: state?.closureState || projectionStates.closureState,
297
+ controllerState: state?.controllerState || projectionStates.controllerState,
265
298
  updatedAt: toIsoTimestamp(),
266
299
  });
267
300
  Object.assign(state, normalized);
@@ -270,8 +303,12 @@ export function writeWaveDashboard(dashboardPath, state) {
270
303
 
271
304
  export function writeGlobalDashboard(globalDashboardPath, state) {
272
305
  ensureDirectory(path.dirname(globalDashboardPath));
306
+ const projectionStates = deriveWaveProjectionTriplet(state?.status);
273
307
  const normalized = normalizeGlobalDashboardState({
274
308
  ...state,
309
+ executionState: state?.executionState || projectionStates.executionState,
310
+ closureState: state?.closureState || projectionStates.closureState,
311
+ controllerState: state?.controllerState || projectionStates.controllerState,
275
312
  updatedAt: toIsoTimestamp(),
276
313
  });
277
314
  Object.assign(state, normalized);
@@ -316,6 +353,9 @@ export function syncGlobalWaveFromWaveDashboard(globalState, waveDashboard) {
316
353
  return;
317
354
  }
318
355
  entry.status = waveDashboard.status;
356
+ entry.executionState = waveDashboard.executionState || null;
357
+ entry.closureState = waveDashboard.closureState || null;
358
+ entry.controllerState = waveDashboard.controllerState || null;
319
359
  entry.attempt = waveDashboard.attempt;
320
360
  entry.startedAt = waveDashboard.startedAt;
321
361
  if (WAVE_TERMINAL_STATES.has(waveDashboard.status)) {
@@ -430,6 +430,9 @@ function buildIntegrationEvidence({
430
430
  ) {
431
431
  const validation = validateImplementationSummary(agent, summary);
432
432
  if (!validation.ok) {
433
+ if (validation.failureClass === "transport-failure" && validation.eligibleForAdjudication) {
434
+ continue;
435
+ }
433
436
  const entry = summarizeGap(agent.agentId, validation.detail, "Implementation validation failed.");
434
437
  if (["missing-doc-delta", "doc-impact-gap", "invalid-doc-delta-format"].includes(validation.statusCode)) {
435
438
  docGapEntries.push(entry);
@@ -57,6 +57,11 @@ import {
57
57
  evaluateDocumentationAutoClosure,
58
58
  evaluateInferredIntegrationClosure,
59
59
  } from "./closure-policy.mjs";
60
+ import {
61
+ evaluateClosureAdjudication,
62
+ persistClosureAdjudication,
63
+ closureAdjudicationPath,
64
+ } from "./closure-adjudicator.mjs";
60
65
 
61
66
  function contradictionList(value) {
62
67
  if (value instanceof Map) {
@@ -142,6 +147,34 @@ function validateEnvelopeForRun(runInfo, envelope, options = {}) {
142
147
  };
143
148
  }
144
149
 
150
+ function adjudicatedGateResult(validation, runInfo, summary, adjudication, adjudicationPathValue = null) {
151
+ if (adjudication.status === "pass") {
152
+ return {
153
+ ok: true,
154
+ agentId: runInfo.agent.agentId,
155
+ statusCode: validation.statusCode,
156
+ detail: adjudication.detail,
157
+ logPath: summary?.logPath || path.relative(REPO_ROOT, runInfo.logPath),
158
+ adjudicated: true,
159
+ adjudicationStatus: adjudication.status,
160
+ adjudicationArtifactPath: adjudicationPathValue ? path.relative(REPO_ROOT, adjudicationPathValue) : null,
161
+ failureClass: validation.failureClass || null,
162
+ };
163
+ }
164
+ return {
165
+ ok: false,
166
+ agentId: runInfo.agent.agentId,
167
+ statusCode: adjudication.status === "ambiguous" ? "awaiting-adjudication" : validation.statusCode,
168
+ detail: adjudication.detail || validation.detail,
169
+ logPath: summary?.logPath || path.relative(REPO_ROOT, runInfo.logPath),
170
+ adjudicated: true,
171
+ adjudicationStatus: adjudication.status,
172
+ adjudicationArtifactPath: adjudicationPathValue ? path.relative(REPO_ROOT, adjudicationPathValue) : null,
173
+ failureClass: validation.failureClass || null,
174
+ eligibleForAdjudication: validation.eligibleForAdjudication === true,
175
+ };
176
+ }
177
+
145
178
 
146
179
  // --- Laddered gate modes (0.9.4) ---
147
180
  export function resolveGateMode(waveNumber, thresholds) {
@@ -612,13 +645,14 @@ export function readWaveImplementationGate(wave, agentRuns, options = {}) {
612
645
  logPath: path.relative(REPO_ROOT, runInfo.logPath),
613
646
  };
614
647
  }
648
+ const statusRecord = runInfo?.statusPath ? readStatusRecordIfPresent(runInfo.statusPath) : null;
615
649
  const summary = envelopeResult.valid
616
650
  ? projectLegacySummaryFromEnvelope(
617
651
  envelopeResult.envelope,
618
652
  buildEnvelopeReadOptions(
619
653
  runInfo,
620
654
  wave,
621
- runInfo?.statusPath ? readStatusRecordIfPresent(runInfo.statusPath) : null,
655
+ statusRecord,
622
656
  resolveRunReportPath(wave, runInfo, options),
623
657
  ),
624
658
  )
@@ -628,12 +662,45 @@ export function readWaveImplementationGate(wave, agentRuns, options = {}) {
628
662
  });
629
663
  const validation = validateImplementationSummary(runInfo.agent, summary);
630
664
  if (!validation.ok) {
665
+ if (validation.eligibleForAdjudication) {
666
+ const adjudication = evaluateClosureAdjudication({
667
+ wave,
668
+ lanePaths: options.lanePaths || null,
669
+ gate: validation,
670
+ summary,
671
+ derivedState: options.derivedState || null,
672
+ agentRun: runInfo,
673
+ envelope: envelopeResult.valid ? envelopeResult.envelope : null,
674
+ });
675
+ let adjudicationArtifactPath = null;
676
+ if (options.lanePaths?.statusDir) {
677
+ const persisted = persistClosureAdjudication({
678
+ lanePaths: options.lanePaths,
679
+ waveNumber: wave.wave,
680
+ attempt: statusRecord?.attempt || envelopeResult.envelope?.attempt || 1,
681
+ agentId: runInfo.agent.agentId,
682
+ payload: {
683
+ status: adjudication.status,
684
+ failureClass: validation.failureClass,
685
+ reason: adjudication.reason,
686
+ detail: adjudication.detail,
687
+ evidence: adjudication.evidence,
688
+ synthesizedSignals: adjudication.synthesizedSignals,
689
+ },
690
+ });
691
+ adjudicationArtifactPath = persisted.filePath;
692
+ }
693
+ return adjudicatedGateResult(validation, runInfo, summary, adjudication, adjudicationArtifactPath);
694
+ }
631
695
  return {
632
696
  ok: false,
633
697
  agentId: runInfo.agent.agentId,
634
698
  statusCode: validation.statusCode,
635
699
  detail: validation.detail,
636
700
  logPath: summary?.logPath || path.relative(REPO_ROOT, runInfo.logPath),
701
+ failureClass: validation.failureClass || null,
702
+ eligibleForAdjudication: validation.eligibleForAdjudication === true,
703
+ adjudicationHint: validation.adjudicationHint || null,
637
704
  };
638
705
  }
639
706
  }
@@ -922,7 +989,29 @@ export function readWaveDocumentationGate(wave, agentRuns, options = {}) {
922
989
  mode,
923
990
  securityRolePromptPath: options.securityRolePromptPath,
924
991
  });
925
- const validation = validateDocumentationClosureSummary(docRun.agent, summary);
992
+ const validation = validateDocumentationClosureSummary(docRun.agent, summary, {
993
+ allowFallbackOnEmptyRun: true,
994
+ });
995
+ // Fallback: when doc steward had an empty run but integration/QA passed,
996
+ // auto-close instead of blocking the wave.
997
+ if (!validation.ok && validation.eligibleForFallback) {
998
+ const integrationSummary = options.derivedState?.integrationSummary;
999
+ const integrationReady =
1000
+ integrationSummary?.recommendation === "ready-for-doc-closure";
1001
+ const contQaPassed =
1002
+ options.derivedState?.ledger?.contQa?.verdict === "pass" ||
1003
+ options.derivedState?.ledger?.contQa?.verdict === "PASS";
1004
+ if (integrationReady || contQaPassed) {
1005
+ return {
1006
+ ok: true,
1007
+ agentId: docRun.agent.agentId,
1008
+ statusCode: "fallback-auto-closed",
1009
+ detail: `Documentation steward ${docRun.agent.agentId} had an empty run. Auto-closed because ${integrationReady ? "integration is ready-for-doc-closure" : "cont-QA passed"}.`,
1010
+ logPath: summary?.logPath || path.relative(REPO_ROOT, docRun.logPath),
1011
+ docClosureState: "no-change",
1012
+ };
1013
+ }
1014
+ }
926
1015
  return {
927
1016
  ok: validation.ok,
928
1017
  agentId: docRun.agent.agentId,
@@ -1311,12 +1400,37 @@ export function readWaveImplementationGatePure(wave, agentResults, options = {})
1311
1400
  const summary = agentResults?.[agent.agentId] || null;
1312
1401
  const validation = validateImplementationSummary(agent, summary);
1313
1402
  if (!validation.ok) {
1403
+ if (validation.eligibleForAdjudication) {
1404
+ const adjudication = evaluateClosureAdjudication({
1405
+ wave,
1406
+ lanePaths: options.lanePaths || null,
1407
+ gate: validation,
1408
+ summary,
1409
+ derivedState: options.derivedState || null,
1410
+ agentRun: { agent, logPath: summary?.logPath ? path.resolve(REPO_ROOT, summary.logPath) : null },
1411
+ envelope: null,
1412
+ });
1413
+ return {
1414
+ ok: adjudication.status === "pass",
1415
+ agentId: agent.agentId,
1416
+ statusCode: adjudication.status === "ambiguous" ? "awaiting-adjudication" : validation.statusCode,
1417
+ detail: adjudication.detail || validation.detail,
1418
+ logPath: summary?.logPath || null,
1419
+ adjudicated: true,
1420
+ adjudicationStatus: adjudication.status,
1421
+ failureClass: validation.failureClass || null,
1422
+ eligibleForAdjudication: true,
1423
+ };
1424
+ }
1314
1425
  return {
1315
1426
  ok: false,
1316
1427
  agentId: agent.agentId,
1317
1428
  statusCode: validation.statusCode,
1318
1429
  detail: validation.detail,
1319
1430
  logPath: summary?.logPath || null,
1431
+ failureClass: validation.failureClass || null,
1432
+ eligibleForAdjudication: validation.eligibleForAdjudication === true,
1433
+ adjudicationHint: validation.adjudicationHint || null,
1320
1434
  };
1321
1435
  }
1322
1436
  }
@@ -1501,7 +1615,30 @@ export function readWaveDocumentationGatePure(wave, agentResults, options = {})
1501
1615
  }
1502
1616
  const summary = agentResults?.[documentationAgentId] || null;
1503
1617
  const agent = { agentId: documentationAgentId };
1504
- const validation = validateDocumentationClosureSummary(agent, summary);
1618
+ const validation = validateDocumentationClosureSummary(agent, summary, {
1619
+ allowFallbackOnEmptyRun: true,
1620
+ });
1621
+ // When the doc steward had an empty run (broker collision, no output) but
1622
+ // integration is already ready-for-doc-closure, auto-close documentation
1623
+ // instead of failing the entire wave.
1624
+ if (!validation.ok && validation.eligibleForFallback) {
1625
+ const integrationSummary = options.derivedState?.integrationSummary;
1626
+ const integrationReady =
1627
+ integrationSummary?.recommendation === "ready-for-doc-closure";
1628
+ const contQaPassed =
1629
+ options.derivedState?.ledger?.contQa?.verdict === "pass" ||
1630
+ options.derivedState?.ledger?.contQa?.verdict === "PASS";
1631
+ if (integrationReady || contQaPassed) {
1632
+ return {
1633
+ ok: true,
1634
+ agentId: documentationAgentId,
1635
+ statusCode: "fallback-auto-closed",
1636
+ detail: `Documentation steward ${documentationAgentId} had an empty run. Auto-closed because ${integrationReady ? "integration is ready-for-doc-closure" : "cont-QA passed"}.`,
1637
+ logPath: summary?.logPath || null,
1638
+ docClosureState: "no-change",
1639
+ };
1640
+ }
1641
+ }
1505
1642
  return { ok: validation.ok, agentId: documentationAgentId, statusCode: validation.statusCode,
1506
1643
  detail: validation.detail, logPath: summary?.logPath || null };
1507
1644
  }
@@ -69,7 +69,7 @@ export const STARTER_TEMPLATE_PATHS = [
69
69
  "docs/guides/author-and-run-waves.md",
70
70
  "docs/guides/monorepo-projects.md",
71
71
  "docs/guides/planner.md",
72
- "docs/guides/recommendations-0.9.13.md",
72
+ "docs/guides/recommendations-0.9.15.md",
73
73
  "docs/guides/sandboxed-environments.md",
74
74
  "docs/guides/signal-wrappers.md",
75
75
  "docs/guides/terminal-surfaces.md",
@@ -715,6 +715,12 @@ function failuresAreRecoverable(failures) {
715
715
  return Array.isArray(failures) && failures.length > 0 && failures.every((failure) => failure?.recoverable);
716
716
  }
717
717
 
718
+ function failuresAwaitAdjudication(failures) {
719
+ return Array.isArray(failures) && failures.length > 0 && failures.every(
720
+ (failure) => String(failure?.statusCode || "").trim().toLowerCase() === "awaiting-adjudication",
721
+ );
722
+ }
723
+
718
724
  function appendRepairCoordinationRequests({
719
725
  coordinationLogPath,
720
726
  lanePaths,
@@ -771,16 +777,23 @@ export async function runLauncherCli(argv) {
771
777
  const projectId = options.project || config.defaultProject;
772
778
  if (!readProjectProfile({ config, project: projectId })) {
773
779
  const { stderr: stderrStream } = await import("node:process");
774
- stderrStream.write(
775
- "\n No project profile found — running first-time setup.\n" +
776
- " You can re-run this later with: wave project setup\n\n",
777
- );
778
- const { runPlannerCli } = await import("./planner.mjs");
779
- await runPlannerCli(["project", "setup", ...(projectId ? ["--project", projectId] : [])]);
780
- // Re-read the terminal surface from the newly created profile.
781
- const freshProfile = readProjectProfile({ config, project: projectId });
782
- if (freshProfile) {
783
- options.terminalSurface = resolveDefaultTerminalSurface(freshProfile);
780
+ if (options.dryRun) {
781
+ stderrStream.write(
782
+ "\n No project profile found — continuing dry-run with config defaults.\n" +
783
+ " Live launches will still prompt for first-time setup, or you can run: wave project setup\n\n",
784
+ );
785
+ } else {
786
+ stderrStream.write(
787
+ "\n No project profile found running first-time setup.\n" +
788
+ " You can re-run this later with: wave project setup\n\n",
789
+ );
790
+ const { runPlannerCli } = await import("./planner.mjs");
791
+ await runPlannerCli(["project", "setup", ...(projectId ? ["--project", projectId] : [])]);
792
+ // Re-read the terminal surface from the newly created profile.
793
+ const freshProfile = readProjectProfile({ config, project: projectId });
794
+ if (freshProfile) {
795
+ options.terminalSurface = resolveDefaultTerminalSurface(freshProfile);
796
+ }
784
797
  }
785
798
  }
786
799
 
@@ -1907,6 +1920,8 @@ export async function runLauncherCli(argv) {
1907
1920
  if (failures.length === 0) {
1908
1921
  const implementationGate = readWaveImplementationGate(wave, agentRuns, {
1909
1922
  securityRolePromptPath: lanePaths.securityRolePromptPath,
1923
+ lanePaths,
1924
+ derivedState,
1910
1925
  });
1911
1926
  if (!implementationGate.ok) {
1912
1927
  failures = [
@@ -2449,6 +2464,30 @@ export async function runLauncherCli(argv) {
2449
2464
  throw error;
2450
2465
  }
2451
2466
 
2467
+ if (failuresAwaitAdjudication(failures)) {
2468
+ clearWaveRelaunchPlan(lanePaths, wave.wave);
2469
+ dashboardState.status = "blocked";
2470
+ for (const failure of failures) {
2471
+ setWaveDashboardAgent(dashboardState, failure.agentId, {
2472
+ state: "failed",
2473
+ detail: failure.detail || "Closure is awaiting deterministic adjudication.",
2474
+ });
2475
+ }
2476
+ flushDashboards();
2477
+ appendCoordination({
2478
+ event: "wave_adjudication_pending",
2479
+ waves: [wave.wave],
2480
+ status: "blocked",
2481
+ details: failures.map((failure) => `${failure.agentId || "wave"}:${failure.statusCode}`).join(", "),
2482
+ actionRequested: `Lane ${lanePaths.lane} owners should inspect the persisted closure adjudication artifact before any rerun decision.`,
2483
+ });
2484
+ const error = new Error(
2485
+ `Wave ${wave.wave} is waiting on deterministic closure adjudication; no retry was queued.`,
2486
+ );
2487
+ error.exitCode = 43;
2488
+ throw error;
2489
+ }
2490
+
2452
2491
  const failedAgentIds = new Set(failures.map((failure) => failure.agentId));
2453
2492
  const failedList = Array.from(failedAgentIds).join(", ");
2454
2493
  recordAttemptState(lanePaths, wave.wave, attempt, "failed", {
@@ -0,0 +1,271 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ensureDirectory } from "./shared.mjs";
4
+ import {
5
+ EXIT_CONTRACT_COMPLETION_VALUES,
6
+ EXIT_CONTRACT_DURABILITY_VALUES,
7
+ EXIT_CONTRACT_PROOF_VALUES,
8
+ EXIT_CONTRACT_DOC_IMPACT_VALUES,
9
+ } from "./agent-state.mjs";
10
+ import { parseStructuredSignalCandidate } from "./structured-signal-parser.mjs";
11
+
12
+ const PROOF_STATES = ["met", "complete", "gap"];
13
+ const DOC_CLOSURE_STATES = ["closed", "no-change", "delta"];
14
+ const INTEGRATION_STATES = ["ready-for-doc-closure", "needs-more-work"];
15
+ const COMPONENT_ID_REGEX = /^[a-z0-9._-]+$/;
16
+ const COMPONENT_LEVEL_REGEX = /^[a-z0-9._-]+$/;
17
+ const UNSAFE_KEY_VALUE_FRAGMENT_REGEX = /(^|\s)[a-z][a-z0-9_-]*=/i;
18
+ const PARSER_KIND_BY_SIGNAL_KIND = {
19
+ proof: "proof",
20
+ "doc-delta": "docDelta",
21
+ component: "component",
22
+ integration: "integration",
23
+ "doc-closure": "docClosure",
24
+ };
25
+
26
+ function printUsage() {
27
+ console.log(`Usage:
28
+ wave signal proof --completion <level> --durability <level> --proof <level> --state <met|complete|gap> [--detail <text>] [--json] [--append-file <path>]
29
+ wave signal doc-delta --state <none|owned|shared-plan> [--path <file> ...] [--detail <text>] [--json] [--append-file <path>]
30
+ wave signal component --id <component> --level <level> --state <met|complete|gap> [--detail <text>] [--json] [--append-file <path>]
31
+ wave signal integration --state <ready-for-doc-closure|needs-more-work> --claims <n> --conflicts <n> --blockers <n> [--detail <text>] [--json] [--append-file <path>]
32
+ wave signal doc-closure --state <closed|no-change|delta> [--path <file> ...] [--detail <text>] [--json] [--append-file <path>]
33
+ `);
34
+ }
35
+
36
+ function normalizeState(value) {
37
+ return String(value || "").trim().toLowerCase() === "complete" ? "met" : String(value || "").trim();
38
+ }
39
+
40
+ function cleanText(value) {
41
+ return String(value || "").trim();
42
+ }
43
+
44
+ function validateAllowedValue(label, value, allowedValues) {
45
+ const normalized = cleanText(value).toLowerCase();
46
+ if (!allowedValues.includes(normalized)) {
47
+ throw new Error(`${label} must be one of: ${allowedValues.join(", ")}`);
48
+ }
49
+ return normalized;
50
+ }
51
+
52
+ function validateSafeField(label, value, { allowCommas = true } = {}) {
53
+ const normalized = cleanText(value);
54
+ if (!normalized) {
55
+ return normalized;
56
+ }
57
+ if (/[\r\n]/.test(normalized)) {
58
+ throw new Error(`${label} cannot contain newlines`);
59
+ }
60
+ if (!allowCommas && normalized.includes(",")) {
61
+ throw new Error(`${label} cannot contain commas`);
62
+ }
63
+ if (UNSAFE_KEY_VALUE_FRAGMENT_REGEX.test(normalized)) {
64
+ throw new Error(`${label} cannot contain embedded key=value fragments`);
65
+ }
66
+ return normalized;
67
+ }
68
+
69
+ function validatePathList(paths) {
70
+ return paths.map((item, index) =>
71
+ validateSafeField(`path ${index + 1}`, item, { allowCommas: false }),
72
+ );
73
+ }
74
+
75
+ function parseArgs(argv) {
76
+ const kind = String(argv[0] || "").trim().toLowerCase();
77
+ const options = {
78
+ completion: "",
79
+ durability: "",
80
+ proof: "",
81
+ state: "",
82
+ detail: "",
83
+ componentId: "",
84
+ level: "",
85
+ paths: [],
86
+ claims: "0",
87
+ conflicts: "0",
88
+ blockers: "0",
89
+ appendFile: "",
90
+ json: false,
91
+ };
92
+ for (let index = 1; index < argv.length; index += 1) {
93
+ const arg = argv[index];
94
+ if (arg === "--completion") {
95
+ options.completion = String(argv[++index] || "").trim();
96
+ } else if (arg === "--durability") {
97
+ options.durability = String(argv[++index] || "").trim();
98
+ } else if (arg === "--proof") {
99
+ options.proof = String(argv[++index] || "").trim();
100
+ } else if (arg === "--state") {
101
+ options.state = String(argv[++index] || "").trim();
102
+ } else if (arg === "--detail") {
103
+ options.detail = String(argv[++index] || "").trim();
104
+ } else if (arg === "--id") {
105
+ options.componentId = String(argv[++index] || "").trim();
106
+ } else if (arg === "--level") {
107
+ options.level = String(argv[++index] || "").trim();
108
+ } else if (arg === "--path") {
109
+ options.paths.push(String(argv[++index] || "").trim());
110
+ } else if (arg === "--claims") {
111
+ options.claims = String(argv[++index] || "0").trim();
112
+ } else if (arg === "--conflicts") {
113
+ options.conflicts = String(argv[++index] || "0").trim();
114
+ } else if (arg === "--blockers") {
115
+ options.blockers = String(argv[++index] || "0").trim();
116
+ } else if (arg === "--append-file") {
117
+ options.appendFile = String(argv[++index] || "").trim();
118
+ } else if (arg === "--json") {
119
+ options.json = true;
120
+ } else if (arg === "--help" || arg === "-h") {
121
+ return { help: true, kind, options };
122
+ } else if (arg) {
123
+ throw new Error(`Unknown argument: ${arg}`);
124
+ }
125
+ }
126
+ return { help: false, kind, options };
127
+ }
128
+
129
+ function buildLine(kind, options) {
130
+ if (kind === "proof") {
131
+ return [
132
+ "[wave-proof]",
133
+ `completion=${options.completion}`,
134
+ `durability=${options.durability}`,
135
+ `proof=${options.proof}`,
136
+ `state=${normalizeState(options.state)}`,
137
+ ...(options.detail ? [`detail=${options.detail}`] : []),
138
+ ].join(" ");
139
+ }
140
+ if (kind === "doc-delta") {
141
+ return [
142
+ "[wave-doc-delta]",
143
+ `state=${options.state}`,
144
+ ...(options.paths.length > 0 ? [`paths=${options.paths.join(",")}`] : []),
145
+ ...(options.detail ? [`detail=${options.detail}`] : []),
146
+ ].join(" ");
147
+ }
148
+ if (kind === "component") {
149
+ return [
150
+ "[wave-component]",
151
+ `component=${options.componentId}`,
152
+ `level=${options.level}`,
153
+ `state=${normalizeState(options.state)}`,
154
+ ...(options.detail ? [`detail=${options.detail}`] : []),
155
+ ].join(" ");
156
+ }
157
+ if (kind === "integration") {
158
+ return [
159
+ "[wave-integration]",
160
+ `state=${options.state}`,
161
+ `claims=${options.claims}`,
162
+ `conflicts=${options.conflicts}`,
163
+ `blockers=${options.blockers}`,
164
+ ...(options.detail ? [`detail=${options.detail}`] : []),
165
+ ].join(" ");
166
+ }
167
+ if (kind === "doc-closure") {
168
+ return [
169
+ "[wave-doc-closure]",
170
+ `state=${options.state}`,
171
+ ...(options.paths.length > 0 ? [`paths=${options.paths.join(",")}`] : []),
172
+ ...(options.detail ? [`detail=${options.detail}`] : []),
173
+ ].join(" ");
174
+ }
175
+ throw new Error(`Unknown signal kind: ${kind}`);
176
+ }
177
+
178
+ function assertCanonicalRoundTrip(kind, line) {
179
+ const candidate = parseStructuredSignalCandidate(line);
180
+ const parserKind = PARSER_KIND_BY_SIGNAL_KIND[kind] || kind;
181
+ if (!candidate?.accepted || candidate.kind !== parserKind || candidate.normalizedLine !== line) {
182
+ throw new Error(`wave signal ${kind} could not round-trip through the shipped structured-signal parser`);
183
+ }
184
+ }
185
+
186
+ function validate(kind, options) {
187
+ if (kind === "proof") {
188
+ if (!options.completion || !options.durability || !options.proof || !options.state) {
189
+ throw new Error("wave signal proof requires --completion, --durability, --proof, and --state");
190
+ }
191
+ options.completion = validateAllowedValue("--completion", options.completion, EXIT_CONTRACT_COMPLETION_VALUES);
192
+ options.durability = validateAllowedValue("--durability", options.durability, EXIT_CONTRACT_DURABILITY_VALUES);
193
+ options.proof = validateAllowedValue("--proof", options.proof, EXIT_CONTRACT_PROOF_VALUES);
194
+ options.state = validateAllowedValue("--state", options.state, PROOF_STATES);
195
+ options.detail = validateSafeField("--detail", options.detail);
196
+ return;
197
+ }
198
+ if (kind === "doc-delta" || kind === "doc-closure") {
199
+ if (!options.state) {
200
+ throw new Error(`wave signal ${kind} requires --state`);
201
+ }
202
+ options.state = validateAllowedValue(
203
+ "--state",
204
+ options.state,
205
+ kind === "doc-delta" ? EXIT_CONTRACT_DOC_IMPACT_VALUES : DOC_CLOSURE_STATES,
206
+ );
207
+ options.paths = validatePathList(options.paths);
208
+ options.detail = validateSafeField("--detail", options.detail);
209
+ return;
210
+ }
211
+ if (kind === "component") {
212
+ if (!options.componentId || !options.level || !options.state) {
213
+ throw new Error("wave signal component requires --id, --level, and --state");
214
+ }
215
+ options.componentId = validateSafeField("--id", options.componentId, { allowCommas: false });
216
+ options.level = validateSafeField("--level", options.level, { allowCommas: false }).toLowerCase();
217
+ options.state = validateAllowedValue("--state", options.state, PROOF_STATES);
218
+ options.detail = validateSafeField("--detail", options.detail);
219
+ if (!COMPONENT_ID_REGEX.test(options.componentId)) {
220
+ throw new Error("--id must use a canonical component token like runtime-render-snapshot");
221
+ }
222
+ if (!COMPONENT_LEVEL_REGEX.test(options.level)) {
223
+ throw new Error("--level must use a canonical component level token");
224
+ }
225
+ return;
226
+ }
227
+ if (kind === "integration") {
228
+ if (!options.state) {
229
+ throw new Error("wave signal integration requires --state");
230
+ }
231
+ options.state = validateAllowedValue("--state", options.state, INTEGRATION_STATES);
232
+ options.detail = validateSafeField("--detail", options.detail);
233
+ for (const field of ["claims", "conflicts", "blockers"]) {
234
+ const value = Number.parseInt(options[field], 10);
235
+ if (!Number.isFinite(value) || value < 0) {
236
+ throw new Error(`--${field} must be a non-negative integer`);
237
+ }
238
+ options[field] = String(value);
239
+ }
240
+ return;
241
+ }
242
+ throw new Error(`Unknown signal kind: ${kind}`);
243
+ }
244
+
245
+ export async function runSignalCli(argv) {
246
+ if (["--help", "-h", "help"].includes(String(argv[0] || "").trim().toLowerCase())) {
247
+ printUsage();
248
+ return;
249
+ }
250
+ const parsed = parseArgs(argv);
251
+ if (parsed.help || !parsed.kind) {
252
+ printUsage();
253
+ return;
254
+ }
255
+ validate(parsed.kind, parsed.options);
256
+ const line = buildLine(parsed.kind, parsed.options);
257
+ assertCanonicalRoundTrip(parsed.kind, line);
258
+ if (parsed.options.appendFile) {
259
+ ensureDirectory(path.dirname(parsed.options.appendFile));
260
+ fs.appendFileSync(parsed.options.appendFile, `${line}\n`, "utf8");
261
+ }
262
+ if (parsed.options.json) {
263
+ console.log(JSON.stringify({
264
+ kind: parsed.kind,
265
+ line,
266
+ appendFile: parsed.options.appendFile || null,
267
+ }, null, 2));
268
+ return;
269
+ }
270
+ console.log(line);
271
+ }