@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
@@ -10,6 +10,7 @@ import {
10
10
  writeJsonAtomic,
11
11
  } from "./shared.mjs";
12
12
  import { resolveEvalTargetsAgainstCatalog } from "./evals.mjs";
13
+ import { extractStructuredSignalPayload } from "./structured-signal-parser.mjs";
13
14
 
14
15
  export const EXIT_CONTRACT_COMPLETION_VALUES = ["contract", "integrated", "authoritative", "live"];
15
16
  export const EXIT_CONTRACT_DURABILITY_VALUES = ["none", "ephemeral", "durable"];
@@ -55,221 +56,6 @@ const WAVE_GAP_REGEX =
55
56
  /^\[wave-gap\]\s*kind=(architecture|integration|durability|ops|docs)\s*(?:detail=(.*))?$/gim;
56
57
  const WAVE_COMPONENT_REGEX =
57
58
  /^\[wave-component\]\s*component=([a-z0-9._-]+)\s+level=([a-z0-9._-]+)\s+state=(met|complete|gap)\s*(?:detail=(.*))?$/gim;
58
- const STRUCTURED_SIGNAL_LINE_REGEX = /^\[wave-[a-z0-9-]+(?:\]|\s|=|$).*$/i;
59
- const WRAPPED_STRUCTURED_SIGNAL_LINE_REGEX = /^`\[wave-[^`]+`$/;
60
- const STRUCTURED_SIGNAL_LIST_PREFIX_REGEX = /^(?:[-*+]|\d+\.)\s+/;
61
-
62
- const STRUCTURED_SIGNAL_KIND_BY_TAG = {
63
- proof: "proof",
64
- "doc-delta": "docDelta",
65
- "doc-closure": "docClosure",
66
- integration: "integration",
67
- eval: "eval",
68
- security: "security",
69
- design: "design",
70
- gate: "gate",
71
- gap: "gap",
72
- component: "component",
73
- };
74
-
75
- const STRUCTURED_SIGNAL_LINE_REGEX_BY_KIND = {
76
- proof: new RegExp(WAVE_PROOF_REGEX.source, "i"),
77
- docDelta: new RegExp(WAVE_DOC_DELTA_REGEX.source, "i"),
78
- docClosure: new RegExp(WAVE_DOC_CLOSURE_REGEX.source, "i"),
79
- integration: new RegExp(WAVE_INTEGRATION_REGEX.source, "i"),
80
- eval: new RegExp(WAVE_EVAL_REGEX.source, "i"),
81
- security: new RegExp(WAVE_SECURITY_REGEX.source, "i"),
82
- design: new RegExp(WAVE_DESIGN_REGEX.source, "i"),
83
- gate: new RegExp(WAVE_GATE_REGEX.source, "i"),
84
- gap: new RegExp(WAVE_GAP_REGEX.source, "i"),
85
- component: new RegExp(WAVE_COMPONENT_REGEX.source, "i"),
86
- };
87
-
88
- function buildEmptyStructuredSignalDiagnostics() {
89
- return {
90
- proof: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
91
- docDelta: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
92
- docClosure: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
93
- integration: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
94
- eval: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
95
- security: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
96
- design: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
97
- gate: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
98
- gap: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
99
- component: { rawCount: 0, acceptedCount: 0, rejectedSamples: [], seenComponentIds: [] },
100
- };
101
- }
102
-
103
- function pushRejectedStructuredSignalSample(bucket, sample) {
104
- if (!bucket || !sample || bucket.rejectedSamples.length >= 3) {
105
- return;
106
- }
107
- bucket.rejectedSamples.push(sample);
108
- }
109
-
110
- function normalizeStructuredSignalLine(line) {
111
- const trimmed = String(line || "").trim();
112
- if (!trimmed) {
113
- return null;
114
- }
115
- const withoutListPrefix = trimmed.replace(STRUCTURED_SIGNAL_LIST_PREFIX_REGEX, "").trim();
116
- if (STRUCTURED_SIGNAL_LINE_REGEX.test(withoutListPrefix)) {
117
- return withoutListPrefix;
118
- }
119
- if (WRAPPED_STRUCTURED_SIGNAL_LINE_REGEX.test(withoutListPrefix)) {
120
- return withoutListPrefix.slice(1, -1).trim();
121
- }
122
- return null;
123
- }
124
-
125
- function parseStructuredSignalCandidate(line) {
126
- const rawLine = String(line || "").trim();
127
- if (!rawLine) {
128
- return null;
129
- }
130
- const canonicalLine = normalizeStructuredSignalLine(rawLine);
131
- if (!canonicalLine) {
132
- return null;
133
- }
134
- const tagMatch = canonicalLine.match(/^\[wave-([a-z0-9-]+)(?:\]|\s|=|$)/i);
135
- if (!tagMatch) {
136
- return null;
137
- }
138
- const kind = STRUCTURED_SIGNAL_KIND_BY_TAG[String(tagMatch[1] || "").toLowerCase()] || null;
139
- const componentIdMatch = canonicalLine.match(/\bcomponent=([a-z0-9._-]+)/i);
140
- return {
141
- rawLine,
142
- canonicalLine,
143
- kind,
144
- componentId: componentIdMatch ? String(componentIdMatch[1] || "").trim() : null,
145
- };
146
- }
147
-
148
- function appendParsedStructuredSignalCandidates(lines, candidates, { requireAll = false } = {}) {
149
- const parsedCandidates = [];
150
- for (const line of lines || []) {
151
- const candidate = parseStructuredSignalCandidate(line);
152
- if (candidate) {
153
- parsedCandidates.push(candidate);
154
- continue;
155
- }
156
- if (requireAll) {
157
- return;
158
- }
159
- }
160
- candidates.push(...parsedCandidates);
161
- }
162
-
163
- function collectEmbeddedStructuredSignalTexts(value, texts) {
164
- if (!value || typeof value !== "object") {
165
- return;
166
- }
167
- if (Array.isArray(value)) {
168
- for (const item of value) {
169
- collectEmbeddedStructuredSignalTexts(item, texts);
170
- }
171
- return;
172
- }
173
- if (typeof value.text === "string") {
174
- texts.push(value.text);
175
- }
176
- if (typeof value.aggregated_output === "string") {
177
- texts.push(value.aggregated_output);
178
- }
179
- for (const nestedValue of Object.values(value)) {
180
- if (nestedValue && typeof nestedValue === "object") {
181
- collectEmbeddedStructuredSignalTexts(nestedValue, texts);
182
- }
183
- }
184
- }
185
-
186
- function extractEmbeddedStructuredSignalTextsFromJsonLine(line) {
187
- const trimmed = String(line || "").trim();
188
- if (!trimmed || !/^[{\[]/.test(trimmed)) {
189
- return [];
190
- }
191
- try {
192
- const payload = JSON.parse(trimmed);
193
- const texts = [];
194
- collectEmbeddedStructuredSignalTexts(payload, texts);
195
- return texts.filter(Boolean);
196
- } catch {
197
- return [];
198
- }
199
- }
200
-
201
- function collectStructuredSignalCandidates(text) {
202
- if (!text) {
203
- return [];
204
- }
205
- const candidates = [];
206
- let fenceLines = null;
207
- for (const rawLine of String(text || "").split(/\r?\n/)) {
208
- const embeddedTexts = extractEmbeddedStructuredSignalTextsFromJsonLine(rawLine);
209
- for (const embeddedText of embeddedTexts) {
210
- candidates.push(...collectStructuredSignalCandidates(embeddedText));
211
- }
212
- const trimmed = rawLine.trim();
213
- if (/^```/.test(trimmed)) {
214
- if (fenceLines === null) {
215
- fenceLines = [];
216
- continue;
217
- }
218
- appendParsedStructuredSignalCandidates(fenceLines, candidates, { requireAll: true });
219
- fenceLines = null;
220
- continue;
221
- }
222
- if (fenceLines !== null) {
223
- if (!trimmed) {
224
- continue;
225
- }
226
- fenceLines.push(rawLine);
227
- continue;
228
- }
229
- const candidate = parseStructuredSignalCandidate(rawLine);
230
- if (candidate) {
231
- candidates.push(candidate);
232
- }
233
- }
234
- if (fenceLines !== null) {
235
- appendParsedStructuredSignalCandidates(fenceLines, candidates);
236
- }
237
- return candidates;
238
- }
239
-
240
- function buildStructuredSignalDiagnostics(candidates) {
241
- const diagnostics = buildEmptyStructuredSignalDiagnostics();
242
- for (const candidate of candidates || []) {
243
- if (!candidate?.kind || !diagnostics[candidate.kind]) {
244
- continue;
245
- }
246
- const bucket = diagnostics[candidate.kind];
247
- bucket.rawCount += 1;
248
- if (candidate.kind === "component" && candidate.componentId) {
249
- bucket.seenComponentIds.push(candidate.componentId);
250
- }
251
- const strictRegex = STRUCTURED_SIGNAL_LINE_REGEX_BY_KIND[candidate.kind];
252
- if (strictRegex.test(candidate.canonicalLine)) {
253
- bucket.acceptedCount += 1;
254
- continue;
255
- }
256
- pushRejectedStructuredSignalSample(bucket, {
257
- line: candidate.rawLine,
258
- ...(candidate.kind === "component" && candidate.componentId ? { componentId: candidate.componentId } : {}),
259
- });
260
- }
261
- diagnostics.component.seenComponentIds = Array.from(new Set(diagnostics.component.seenComponentIds)).sort();
262
- return diagnostics;
263
- }
264
-
265
- function extractStructuredSignalPayload(text) {
266
- const candidates = collectStructuredSignalCandidates(text);
267
- return {
268
- signalText: candidates.map((candidate) => candidate.canonicalLine).join("\n"),
269
- diagnostics: buildStructuredSignalDiagnostics(candidates),
270
- };
271
- }
272
-
273
59
  function cleanText(value) {
274
60
  return String(value || "").trim();
275
61
  }
@@ -712,6 +498,12 @@ function rejectedStructuredSignalLine(summary, key, predicate = null) {
712
498
  return cleanText(match?.line || "");
713
499
  }
714
500
 
501
+ function rejectedStructuredSignalSample(summary, key, predicate = null) {
502
+ const bucket = structuredSignalBucket(summary, key);
503
+ const rejected = Array.isArray(bucket?.rejectedSamples) ? bucket.rejectedSamples : [];
504
+ return (typeof predicate === "function" ? rejected.find(predicate) : rejected[0]) || null;
505
+ }
506
+
715
507
  function hasRejectedStructuredSignal(summary, key) {
716
508
  const bucket = structuredSignalBucket(summary, key);
717
509
  return Number(bucket?.rawCount || 0) > 0 && Number(bucket?.acceptedCount || 0) === 0;
@@ -731,80 +523,157 @@ function invalidStructuredSignalDetail(agentId, markerName, summary, key, extraD
731
523
  return appendTerminationHint(detailParts.join(" "), summary);
732
524
  }
733
525
 
526
+ function buildValidationResult(ok, statusCode, detail, extras = {}) {
527
+ return {
528
+ ok,
529
+ statusCode,
530
+ detail,
531
+ failureClass: ok ? null : extras.failureClass || null,
532
+ eligibleForAdjudication: ok ? false : extras.eligibleForAdjudication === true,
533
+ adjudicationHint: ok ? null : extras.adjudicationHint || null,
534
+ ...extras,
535
+ };
536
+ }
537
+
538
+ function implementationTransportEligibility(agent, summary) {
539
+ const deliverables = Array.isArray(agent?.deliverables) ? agent.deliverables : [];
540
+ const deliverableState = new Map(
541
+ Array.isArray(summary?.deliverables)
542
+ ? summary.deliverables.map((deliverable) => [deliverable.path, deliverable])
543
+ : [],
544
+ );
545
+ const deliverablesPresent = deliverables.every((deliverablePath) => deliverableState.get(deliverablePath)?.exists === true);
546
+ const proofArtifacts = Array.isArray(agent?.proofArtifacts) ? agent.proofArtifacts : [];
547
+ const artifactState = new Map(
548
+ Array.isArray(summary?.proofArtifacts)
549
+ ? summary.proofArtifacts.map((artifact) => [artifact.path, artifact])
550
+ : [],
551
+ );
552
+ const proofArtifactsPresent = proofArtifacts
553
+ .filter((proofArtifact) => proofArtifactRequiredForAgent(agent, proofArtifact))
554
+ .every((proofArtifact) => artifactState.get(proofArtifact.path)?.exists === true);
555
+ const exitCodeZero = Number(summary?.exitCode) === 0;
556
+ return {
557
+ deliverablesPresent,
558
+ proofArtifactsPresent,
559
+ exitCodeZero,
560
+ eligibleForAdjudication: exitCodeZero && deliverablesPresent && proofArtifactsPresent,
561
+ };
562
+ }
563
+
564
+ function implementationInvalidMarkerEligibility(agent, summary, key, predicate = null) {
565
+ const transportEligibility = implementationTransportEligibility(agent, summary);
566
+ const sample = rejectedStructuredSignalSample(summary, key, predicate);
567
+ return {
568
+ ...transportEligibility,
569
+ rejectedSample: sample,
570
+ eligibleForAdjudication: transportEligibility.eligibleForAdjudication && Boolean(sample),
571
+ };
572
+ }
573
+
734
574
  export function validateImplementationSummary(agent, summary) {
735
575
  const contract = normalizeExitContract(agent?.exitContract);
736
576
  if (!contract) {
737
- return { ok: true, statusCode: "pass", detail: "No exit contract declared." };
577
+ return buildValidationResult(true, "pass", "No exit contract declared.");
738
578
  }
739
579
  if (!summary) {
740
- return {
741
- ok: false,
742
- statusCode: "missing-summary",
743
- detail: `Missing execution summary for ${agent.agentId}.`,
744
- };
580
+ return buildValidationResult(false, "missing-summary", `Missing execution summary for ${agent.agentId}.`, {
581
+ failureClass: "state-failure",
582
+ });
745
583
  }
746
584
  if (!summary.proof) {
747
585
  if (hasRejectedStructuredSignal(summary, "proof")) {
748
- return {
749
- ok: false,
750
- statusCode: "invalid-wave-proof-format",
751
- detail: invalidStructuredSignalDetail(agent.agentId, "[wave-proof]", summary, "proof"),
752
- };
586
+ const transportEligibility = implementationInvalidMarkerEligibility(agent, summary, "proof");
587
+ return buildValidationResult(
588
+ false,
589
+ "invalid-wave-proof-format",
590
+ invalidStructuredSignalDetail(agent.agentId, "[wave-proof]", summary, "proof"),
591
+ {
592
+ failureClass: "transport-failure",
593
+ eligibleForAdjudication: transportEligibility.eligibleForAdjudication,
594
+ adjudicationHint: transportEligibility.eligibleForAdjudication
595
+ ? "Malformed proof marker with exit 0 and landed artifacts is eligible for deterministic adjudication."
596
+ : null,
597
+ },
598
+ );
753
599
  }
754
- return {
755
- ok: false,
756
- statusCode: "missing-wave-proof",
757
- detail: appendTerminationHint(`Missing [wave-proof] marker for ${agent.agentId}.`, summary),
758
- };
600
+ return buildValidationResult(
601
+ false,
602
+ "missing-wave-proof",
603
+ appendTerminationHint(`Missing [wave-proof] marker for ${agent.agentId}.`, summary),
604
+ {
605
+ failureClass: "transport-failure",
606
+ eligibleForAdjudication: false,
607
+ adjudicationHint: null,
608
+ },
609
+ );
759
610
  }
760
611
  if (summary.proof.state !== "met") {
761
- return {
762
- ok: false,
763
- statusCode: "wave-proof-gap",
764
- detail: `Agent ${agent.agentId} reported a proof gap${summary.proof.detail ? `: ${summary.proof.detail}` : "."}`,
765
- };
612
+ return buildValidationResult(
613
+ false,
614
+ "wave-proof-gap",
615
+ `Agent ${agent.agentId} reported a proof gap${summary.proof.detail ? `: ${summary.proof.detail}` : "."}`,
616
+ { failureClass: "semantic-failure" },
617
+ );
766
618
  }
767
619
  if (!meetsOrExceeds(summary.proof.completion, contract.completion, COMPLETION_ORDER)) {
768
- return {
769
- ok: false,
770
- statusCode: "completion-gap",
771
- detail: `Agent ${agent.agentId} only proved ${summary.proof.completion}; exit contract requires ${contract.completion}.`,
772
- };
620
+ return buildValidationResult(
621
+ false,
622
+ "completion-gap",
623
+ `Agent ${agent.agentId} only proved ${summary.proof.completion}; exit contract requires ${contract.completion}.`,
624
+ { failureClass: "semantic-failure" },
625
+ );
773
626
  }
774
627
  if (!meetsOrExceeds(summary.proof.durability, contract.durability, DURABILITY_ORDER)) {
775
- return {
776
- ok: false,
777
- statusCode: "durability-gap",
778
- detail: `Agent ${agent.agentId} only proved ${summary.proof.durability} durability; exit contract requires ${contract.durability}.`,
779
- };
628
+ return buildValidationResult(
629
+ false,
630
+ "durability-gap",
631
+ `Agent ${agent.agentId} only proved ${summary.proof.durability} durability; exit contract requires ${contract.durability}.`,
632
+ { failureClass: "semantic-failure" },
633
+ );
780
634
  }
781
635
  if (!meetsOrExceeds(summary.proof.proof, contract.proof, PROOF_ORDER)) {
782
- return {
783
- ok: false,
784
- statusCode: "proof-level-gap",
785
- detail: `Agent ${agent.agentId} only proved ${summary.proof.proof}; exit contract requires ${contract.proof}.`,
786
- };
636
+ return buildValidationResult(
637
+ false,
638
+ "proof-level-gap",
639
+ `Agent ${agent.agentId} only proved ${summary.proof.proof}; exit contract requires ${contract.proof}.`,
640
+ { failureClass: "semantic-failure" },
641
+ );
787
642
  }
788
643
  if (!summary.docDelta) {
789
644
  if (hasRejectedStructuredSignal(summary, "docDelta")) {
790
- return {
791
- ok: false,
792
- statusCode: "invalid-doc-delta-format",
793
- detail: invalidStructuredSignalDetail(agent.agentId, "[wave-doc-delta]", summary, "docDelta"),
794
- };
645
+ const transportEligibility = implementationInvalidMarkerEligibility(agent, summary, "docDelta");
646
+ return buildValidationResult(
647
+ false,
648
+ "invalid-doc-delta-format",
649
+ invalidStructuredSignalDetail(agent.agentId, "[wave-doc-delta]", summary, "docDelta"),
650
+ {
651
+ failureClass: "transport-failure",
652
+ eligibleForAdjudication: transportEligibility.eligibleForAdjudication,
653
+ adjudicationHint: transportEligibility.eligibleForAdjudication
654
+ ? "Malformed doc-delta marker with exit 0 and landed artifacts is eligible for deterministic adjudication."
655
+ : null,
656
+ },
657
+ );
795
658
  }
796
- return {
797
- ok: false,
798
- statusCode: "missing-doc-delta",
799
- detail: appendTerminationHint(`Missing [wave-doc-delta] marker for ${agent.agentId}.`, summary),
800
- };
659
+ return buildValidationResult(
660
+ false,
661
+ "missing-doc-delta",
662
+ appendTerminationHint(`Missing [wave-doc-delta] marker for ${agent.agentId}.`, summary),
663
+ {
664
+ failureClass: "transport-failure",
665
+ eligibleForAdjudication: false,
666
+ adjudicationHint: null,
667
+ },
668
+ );
801
669
  }
802
670
  if (!meetsOrExceeds(summary.docDelta.state, contract.docImpact, DOC_IMPACT_ORDER)) {
803
- return {
804
- ok: false,
805
- statusCode: "doc-impact-gap",
806
- detail: `Agent ${agent.agentId} only reported ${summary.docDelta.state} doc impact; exit contract requires ${contract.docImpact}.`,
807
- };
671
+ return buildValidationResult(
672
+ false,
673
+ "doc-impact-gap",
674
+ `Agent ${agent.agentId} only reported ${summary.docDelta.state} doc impact; exit contract requires ${contract.docImpact}.`,
675
+ { failureClass: "semantic-failure" },
676
+ );
808
677
  }
809
678
  const ownedComponents = Array.isArray(agent?.components) ? agent.components : [];
810
679
  if (ownedComponents.length > 0) {
@@ -824,10 +693,16 @@ export function validateImplementationSummary(agent, summary) {
824
693
  Number(componentDiagnostics?.rawCount || 0) > 0 &&
825
694
  (seenComponentIds.has(componentId) || Number(componentDiagnostics?.acceptedCount || 0) === 0)
826
695
  ) {
827
- return {
828
- ok: false,
829
- statusCode: "invalid-wave-component-format",
830
- detail: invalidStructuredSignalDetail(
696
+ const transportEligibility = implementationInvalidMarkerEligibility(
697
+ agent,
698
+ summary,
699
+ "component",
700
+ (sample) => cleanText(sample?.componentId) === componentId,
701
+ );
702
+ return buildValidationResult(
703
+ false,
704
+ "invalid-wave-component-format",
705
+ invalidStructuredSignalDetail(
831
706
  agent.agentId,
832
707
  "[wave-component]",
833
708
  summary,
@@ -835,30 +710,43 @@ export function validateImplementationSummary(agent, summary) {
835
710
  `Expected a valid component marker for ${componentId}.`,
836
711
  (sample) => cleanText(sample?.componentId) === componentId,
837
712
  ),
838
- };
713
+ {
714
+ failureClass: "transport-failure",
715
+ eligibleForAdjudication: transportEligibility.eligibleForAdjudication,
716
+ adjudicationHint: transportEligibility.eligibleForAdjudication
717
+ ? "Malformed component marker with exit 0 and landed artifacts is eligible for deterministic adjudication."
718
+ : null,
719
+ },
720
+ );
839
721
  }
840
- return {
841
- ok: false,
842
- statusCode: "missing-wave-component",
843
- detail: `Missing [wave-component] marker for ${agent.agentId} component ${componentId}.`,
844
- };
722
+ return buildValidationResult(
723
+ false,
724
+ "missing-wave-component",
725
+ `Missing [wave-component] marker for ${agent.agentId} component ${componentId}.`,
726
+ {
727
+ failureClass: "transport-failure",
728
+ eligibleForAdjudication: false,
729
+ adjudicationHint: null,
730
+ },
731
+ );
845
732
  }
846
733
  const expectedLevel = agent?.componentTargets?.[componentId] || null;
847
734
  if (expectedLevel && marker.level !== expectedLevel) {
848
- return {
849
- ok: false,
850
- statusCode: "component-level-mismatch",
851
- detail: `Agent ${agent.agentId} reported ${componentId} at ${marker.level}; wave requires ${expectedLevel}.`,
852
- };
735
+ return buildValidationResult(
736
+ false,
737
+ "component-level-mismatch",
738
+ `Agent ${agent.agentId} reported ${componentId} at ${marker.level}; wave requires ${expectedLevel}.`,
739
+ { failureClass: "semantic-failure" },
740
+ );
853
741
  }
854
742
  if (marker.state !== "met") {
855
- return {
856
- ok: false,
857
- statusCode: "component-gap",
858
- detail:
859
- marker.detail ||
743
+ return buildValidationResult(
744
+ false,
745
+ "component-gap",
746
+ marker.detail ||
860
747
  `Agent ${agent.agentId} reported a component gap for ${componentId}.`,
861
- };
748
+ { failureClass: "semantic-failure" },
749
+ );
862
750
  }
863
751
  }
864
752
  }
@@ -872,18 +760,20 @@ export function validateImplementationSummary(agent, summary) {
872
760
  for (const deliverablePath of deliverables) {
873
761
  const deliverable = deliverableState.get(deliverablePath);
874
762
  if (!deliverable) {
875
- return {
876
- ok: false,
877
- statusCode: "missing-deliverable-summary",
878
- detail: `Missing deliverable presence record for ${agent.agentId} path ${deliverablePath}.`,
879
- };
763
+ return buildValidationResult(
764
+ false,
765
+ "missing-deliverable-summary",
766
+ `Missing deliverable presence record for ${agent.agentId} path ${deliverablePath}.`,
767
+ { failureClass: "artifact-failure" },
768
+ );
880
769
  }
881
770
  if (deliverable.exists !== true) {
882
- return {
883
- ok: false,
884
- statusCode: "missing-deliverable",
885
- detail: `Agent ${agent.agentId} did not land required deliverable ${deliverablePath}.`,
886
- };
771
+ return buildValidationResult(
772
+ false,
773
+ "missing-deliverable",
774
+ `Agent ${agent.agentId} did not land required deliverable ${deliverablePath}.`,
775
+ { failureClass: "artifact-failure" },
776
+ );
887
777
  }
888
778
  }
889
779
  }
@@ -900,30 +790,48 @@ export function validateImplementationSummary(agent, summary) {
900
790
  }
901
791
  const artifact = artifactState.get(proofArtifact.path);
902
792
  if (!artifact) {
903
- return {
904
- ok: false,
905
- statusCode: "missing-proof-artifact-summary",
906
- detail: `Missing proof artifact presence record for ${agent.agentId} path ${proofArtifact.path}.`,
907
- };
793
+ return buildValidationResult(
794
+ false,
795
+ "missing-proof-artifact-summary",
796
+ `Missing proof artifact presence record for ${agent.agentId} path ${proofArtifact.path}.`,
797
+ { failureClass: "artifact-failure" },
798
+ );
908
799
  }
909
800
  if (artifact.exists !== true) {
910
- return {
911
- ok: false,
912
- statusCode: "missing-proof-artifact",
913
- detail: `Agent ${agent.agentId} did not land required proof artifact ${proofArtifact.path}.`,
914
- };
801
+ return buildValidationResult(
802
+ false,
803
+ "missing-proof-artifact",
804
+ `Agent ${agent.agentId} did not land required proof artifact ${proofArtifact.path}.`,
805
+ { failureClass: "artifact-failure" },
806
+ );
915
807
  }
916
808
  }
917
809
  }
918
- return {
919
- ok: true,
920
- statusCode: "pass",
921
- detail: `Exit contract satisfied for ${agent.agentId}.`,
922
- };
810
+ return buildValidationResult(true, "pass", `Exit contract satisfied for ${agent.agentId}.`);
923
811
  }
924
812
 
925
- export function validateDocumentationClosureSummary(agent, summary) {
813
+ export function validateDocumentationClosureSummary(agent, summary, options = {}) {
926
814
  if (!summary?.docClosure) {
815
+ // When the agent had exit-code 0 but produced no structured output (e.g. credential
816
+ // broker collision, empty run), allow a graceful fallback instead of hard-failing
817
+ // the entire wave. The caller (gate-engine) decides whether the surrounding
818
+ // integration/QA state justifies auto-closing documentation.
819
+ const emptyRun = !summary || (
820
+ !summary.proof && !summary.component && !summary.integration &&
821
+ !summary.verdict && !summary.docClosure && !summary.security &&
822
+ (summary.rawSignalCount === 0 || summary.rawSignalCount === undefined)
823
+ );
824
+ if (options.allowFallbackOnEmptyRun && emptyRun) {
825
+ return {
826
+ ok: false,
827
+ statusCode: "missing-doc-closure-empty-run",
828
+ detail: appendTerminationHint(
829
+ `Documentation steward ${agent?.agentId || "A9"} produced no output (empty run). Eligible for fallback auto-closure.`,
830
+ summary,
831
+ ),
832
+ eligibleForFallback: true,
833
+ };
834
+ }
927
835
  return {
928
836
  ok: false,
929
837
  statusCode: "missing-doc-closure",
@@ -5,9 +5,10 @@ import {
5
5
  } from "./wave-control-schema.mjs";
6
6
 
7
7
  export const MANIFEST_SCHEMA_VERSION = 1;
8
- export const GLOBAL_DASHBOARD_SCHEMA_VERSION = 1;
9
- export const WAVE_DASHBOARD_SCHEMA_VERSION = 1;
8
+ export const GLOBAL_DASHBOARD_SCHEMA_VERSION = 2;
9
+ export const WAVE_DASHBOARD_SCHEMA_VERSION = 2;
10
10
  export const RELAUNCH_PLAN_SCHEMA_VERSION = 1;
11
+ export const CLOSURE_ADJUDICATION_SCHEMA_VERSION = 1;
11
12
  export const RETRY_OVERRIDE_SCHEMA_VERSION = 1;
12
13
  export const ASSIGNMENT_SNAPSHOT_SCHEMA_VERSION = 1;
13
14
  export const DEPENDENCY_SNAPSHOT_SCHEMA_VERSION = 1;
@@ -19,6 +20,7 @@ export const MANIFEST_KIND = "wave-manifest";
19
20
  export const GLOBAL_DASHBOARD_KIND = "global-dashboard";
20
21
  export const WAVE_DASHBOARD_KIND = "wave-dashboard";
21
22
  export const RELAUNCH_PLAN_KIND = "wave-relaunch-plan";
23
+ export const CLOSURE_ADJUDICATION_KIND = "wave-closure-adjudication";
22
24
  export const RETRY_OVERRIDE_KIND = "wave-retry-override";
23
25
  export const ASSIGNMENT_SNAPSHOT_KIND = "wave-assignment-snapshot";
24
26
  export const DEPENDENCY_SNAPSHOT_KIND = "wave-dependency-snapshot";
@@ -129,6 +131,39 @@ export function writeRelaunchPlan(filePath, payload, defaults = {}) {
129
131
  return normalized;
130
132
  }
131
133
 
134
+ export function normalizeClosureAdjudication(payload, defaults = {}) {
135
+ const source = isPlainObject(payload) ? payload : {};
136
+ return {
137
+ schemaVersion: CLOSURE_ADJUDICATION_SCHEMA_VERSION,
138
+ kind: CLOSURE_ADJUDICATION_KIND,
139
+ lane: normalizeText(source.lane, normalizeText(defaults.lane, null)),
140
+ wave: normalizeInteger(source.wave, normalizeInteger(defaults.wave, null)),
141
+ attempt: normalizeInteger(source.attempt, normalizeInteger(defaults.attempt, null)),
142
+ agentId: normalizeText(source.agentId, normalizeText(defaults.agentId, null)),
143
+ status: normalizeText(source.status, normalizeText(defaults.status, null)),
144
+ failureClass: normalizeText(source.failureClass, normalizeText(defaults.failureClass, null)),
145
+ reason: normalizeText(source.reason, normalizeText(defaults.reason, null)),
146
+ detail: normalizeText(source.detail, normalizeText(defaults.detail, null)),
147
+ evidence: Array.isArray(source.evidence) ? cloneJson(source.evidence) : [],
148
+ synthesizedSignals: Array.isArray(source.synthesizedSignals) ? cloneJson(source.synthesizedSignals) : [],
149
+ createdAt: normalizeText(source.createdAt, normalizeText(defaults.createdAt, toIsoTimestamp())),
150
+ };
151
+ }
152
+
153
+ export function readClosureAdjudication(filePath, defaults = {}) {
154
+ const payload = readJsonOrNull(filePath);
155
+ if (!payload) {
156
+ return null;
157
+ }
158
+ return normalizeClosureAdjudication(payload, defaults);
159
+ }
160
+
161
+ export function writeClosureAdjudication(filePath, payload, defaults = {}) {
162
+ const normalized = normalizeClosureAdjudication(payload, defaults);
163
+ writeJsonAtomic(filePath, normalized);
164
+ return normalized;
165
+ }
166
+
132
167
  export function normalizeRetryOverride(payload, defaults = {}) {
133
168
  const source = isPlainObject(payload) ? payload : {};
134
169
  return {