@chllming/wave-orchestration 0.7.1 → 0.7.2

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 (32) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +8 -8
  3. package/docs/plans/component-cutover-matrix.json +50 -3
  4. package/docs/plans/current-state.md +1 -1
  5. package/docs/plans/end-state-architecture.md +927 -0
  6. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  7. package/docs/plans/migration.md +2 -2
  8. package/docs/plans/waves/wave-1.md +376 -0
  9. package/docs/plans/waves/wave-2.md +292 -0
  10. package/docs/plans/waves/wave-3.md +342 -0
  11. package/docs/plans/waves/wave-4.md +391 -0
  12. package/docs/plans/waves/wave-5.md +382 -0
  13. package/docs/plans/waves/wave-6.md +321 -0
  14. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  15. package/docs/reference/sample-waves.md +4 -4
  16. package/package.json +1 -1
  17. package/releases/manifest.json +19 -0
  18. package/scripts/wave-orchestrator/agent-state.mjs +447 -33
  19. package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
  20. package/scripts/wave-orchestrator/control-cli.mjs +7 -1
  21. package/scripts/wave-orchestrator/coordination.mjs +11 -10
  22. package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
  23. package/scripts/wave-orchestrator/install.mjs +22 -0
  24. package/scripts/wave-orchestrator/launcher-derived-state.mjs +915 -0
  25. package/scripts/wave-orchestrator/launcher-gates.mjs +1061 -0
  26. package/scripts/wave-orchestrator/launcher-retry.mjs +873 -0
  27. package/scripts/wave-orchestrator/launcher-supervisor.mjs +704 -0
  28. package/scripts/wave-orchestrator/launcher.mjs +153 -2922
  29. package/scripts/wave-orchestrator/task-entity.mjs +557 -0
  30. package/scripts/wave-orchestrator/wave-files.mjs +11 -2
  31. package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
  32. package/wave.config.json +1 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  This repo now includes a dedicated npmjs publish workflow at [publish-npm.yml](../../.github/workflows/publish-npm.yml).
4
4
 
5
- The current `0.7.1` release procedure publishes through a repository Actions secret named `NPM_TOKEN`.
5
+ The current `0.7.2` release procedure publishes through a repository Actions secret named `NPM_TOKEN`.
6
6
 
7
7
  ## What This Repo Already Does
8
8
 
@@ -47,6 +47,6 @@ If this repo later needs private npm dependencies during CI, consider a separate
47
47
  1. Confirm [publish-npm.yml](../../.github/workflows/publish-npm.yml) is on the default branch.
48
48
  2. Confirm `NPM_TOKEN` exists in the GitHub repo secrets.
49
49
  3. Confirm the package version has been bumped and committed.
50
- 4. Push the release commit and release tag, for example `v0.7.1`.
50
+ 4. Push the release commit and release tag, for example `v0.7.2`.
51
51
  5. Verify both `publish-npm.yml` and `publish-package.yml` start from the tag push.
52
52
  6. Verify the npmjs publish completes successfully for the tagged source.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  title: "Sample Waves"
3
- summary: "Showcase-first sample waves that demonstrate the current 0.7.1 Wave surface."
3
+ summary: "Showcase-first sample waves that demonstrate the current 0.7.2 Wave surface."
4
4
  ---
5
5
 
6
6
  # Sample Waves
7
7
 
8
- This guide points to showcase-first sample waves that demonstrate the current `0.7.1` authored Wave surface.
8
+ This guide points to showcase-first sample waves that demonstrate the current `0.7.2` authored Wave surface.
9
9
 
10
10
  The examples are intentionally denser than typical production waves. Their job is to teach the current authoring and runtime surface quickly, not to be the smallest possible launch-ready files.
11
11
 
@@ -15,7 +15,7 @@ The examples are intentionally denser than typical production waves. Their job i
15
15
  Shows what a good `repo-landed` outcome looks like when one promoted component only closes honestly if desired-state records, reconcile-loop substrate, and cluster-view surfaces land together. It emphasizes maturity discipline, explicit deliverables, and shared-plan closure without drifting into `pilot-live` claims.
16
16
 
17
17
  - [Full modern sample wave](../plans/examples/wave-example-live-proof.md)
18
- Shows the combined `0.7.1` authored surface in one file: closure roles, `E0`, optional security review, delegated and pinned benchmark targets, richer executor config, `### Skills`, `### Capabilities`, `### Deliverables`, `### Exit contract`, `### Proof artifacts`, sticky retry, deploy environments, and proof-first live-wave structure.
18
+ Shows the combined `0.7.2` authored surface in one file: closure roles, `E0`, optional security review, delegated and pinned benchmark targets, richer executor config, `### Skills`, `### Capabilities`, `### Deliverables`, `### Exit contract`, `### Proof artifacts`, sticky retry, deploy environments, and proof-first live-wave structure.
19
19
 
20
20
  ## What These Examples Teach
21
21
 
@@ -38,7 +38,7 @@ The examples are intentionally denser than typical production waves. Their job i
38
38
 
39
39
  ## Feature Coverage Map
40
40
 
41
- Together these samples cover the main surfaces added or hardened for `0.7.1`:
41
+ Together these samples cover the main surfaces added or hardened for `0.7.2`:
42
42
 
43
43
  - repo-landed maturity discipline and anti-overclaim framing
44
44
  - explicit shared-plan closure for future-wave safety
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chllming/wave-orchestration",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,6 +2,25 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.7.2",
7
+ "date": "2026-03-23",
8
+ "summary": "Implementation marker parsing repair, proof-centric summary refresh, and 0.7.2 release-surface alignment.",
9
+ "features": [
10
+ "Implementation summaries now accept final `[wave-proof]`, `[wave-doc-delta]`, and `[wave-component]` markers when agents emit them as bullet-prefixed structured blocks.",
11
+ "Validation now surfaces parse-specific proof, doc-delta, and component errors when raw structured marker text was seen in the log but rejected by the strict parser.",
12
+ "Legacy proof-centric summaries are refreshed from source logs only when the stored summary is actually missing required proof, doc-delta, or owned-component markers, preserving valid historical summaries.",
13
+ "Implementation prompts now keep incomplete work inside the required final markers with `state=gap` and route unresolved issues through `wave coord post` instead of trailing `[wave-gap]` lines.",
14
+ "Shipped package metadata, README, migration guidance, sample-wave docs, and npm publishing instructions now point at the `0.7.2` release surface."
15
+ ],
16
+ "manualSteps": [
17
+ "If an older lane still contains proof-centric `.summary.json` files missing parsed markers, rerun the relevant launcher or status surfaces once so they can self-refresh from the source logs.",
18
+ "If implementation agents still carry custom prompt guidance about trailing `[wave-gap]` markers in repo-owned docs, update that guidance so final output ends with only the required implementation markers.",
19
+ "If an adopted `0.6.x` repo fails `wave doctor` after the `0.7.x` upgrade, sync the repo-owned planner starter surface (`docs/agents/wave-planner-role.md`, `skills/role-planner/`, `docs/context7/planner-agent/`, `docs/reference/wave-planning-lessons.md`, and the `planner-agentic` bundle entry) before relying on planner-aware validation.",
20
+ "Run `pnpm exec wave doctor` and `pnpm exec wave launch --lane main --dry-run --no-dashboard` after upgrading once your repo-owned wave files satisfy the current validation contract."
21
+ ],
22
+ "breaking": false
23
+ },
5
24
  {
6
25
  "version": "0.7.1",
7
26
  "date": "2026-03-23",
@@ -53,14 +53,98 @@ const WAVE_GAP_REGEX =
53
53
  /^\[wave-gap\]\s*kind=(architecture|integration|durability|ops|docs)\s*(?:detail=(.*))?$/gim;
54
54
  const WAVE_COMPONENT_REGEX =
55
55
  /^\[wave-component\]\s*component=([a-z0-9._-]+)\s+level=([a-z0-9._-]+)\s+state=(met|gap)\s*(?:detail=(.*))?$/gim;
56
- const STRUCTURED_SIGNAL_LINE_REGEX = /^\[wave-[^\]]+\].*$/;
56
+ const STRUCTURED_SIGNAL_LINE_REGEX = /^\[wave-[a-z0-9-]+(?:\]|\s|=|$).*$/i;
57
57
  const WRAPPED_STRUCTURED_SIGNAL_LINE_REGEX = /^`\[wave-[^`]+`$/;
58
+ const STRUCTURED_SIGNAL_LIST_PREFIX_REGEX = /^(?:[-*+]|\d+\.)\s+/;
58
59
 
59
- function normalizeStructuredSignalText(text) {
60
+ const STRUCTURED_SIGNAL_KIND_BY_TAG = {
61
+ proof: "proof",
62
+ "doc-delta": "docDelta",
63
+ "doc-closure": "docClosure",
64
+ integration: "integration",
65
+ eval: "eval",
66
+ security: "security",
67
+ gate: "gate",
68
+ gap: "gap",
69
+ component: "component",
70
+ };
71
+
72
+ const STRUCTURED_SIGNAL_LINE_REGEX_BY_KIND = {
73
+ proof: new RegExp(WAVE_PROOF_REGEX.source, "i"),
74
+ docDelta: new RegExp(WAVE_DOC_DELTA_REGEX.source, "i"),
75
+ docClosure: new RegExp(WAVE_DOC_CLOSURE_REGEX.source, "i"),
76
+ integration: new RegExp(WAVE_INTEGRATION_REGEX.source, "i"),
77
+ eval: new RegExp(WAVE_EVAL_REGEX.source, "i"),
78
+ security: new RegExp(WAVE_SECURITY_REGEX.source, "i"),
79
+ gate: new RegExp(WAVE_GATE_REGEX.source, "i"),
80
+ gap: new RegExp(WAVE_GAP_REGEX.source, "i"),
81
+ component: new RegExp(WAVE_COMPONENT_REGEX.source, "i"),
82
+ };
83
+
84
+ function buildEmptyStructuredSignalDiagnostics() {
85
+ return {
86
+ proof: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
87
+ docDelta: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
88
+ docClosure: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
89
+ integration: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
90
+ eval: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
91
+ security: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
92
+ gate: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
93
+ gap: { rawCount: 0, acceptedCount: 0, rejectedSamples: [] },
94
+ component: { rawCount: 0, acceptedCount: 0, rejectedSamples: [], seenComponentIds: [] },
95
+ };
96
+ }
97
+
98
+ function pushRejectedStructuredSignalSample(bucket, sample) {
99
+ if (!bucket || !sample || bucket.rejectedSamples.length >= 3) {
100
+ return;
101
+ }
102
+ bucket.rejectedSamples.push(sample);
103
+ }
104
+
105
+ function normalizeStructuredSignalLine(line) {
106
+ const trimmed = String(line || "").trim();
107
+ if (!trimmed) {
108
+ return null;
109
+ }
110
+ const withoutListPrefix = trimmed.replace(STRUCTURED_SIGNAL_LIST_PREFIX_REGEX, "").trim();
111
+ if (STRUCTURED_SIGNAL_LINE_REGEX.test(withoutListPrefix)) {
112
+ return withoutListPrefix;
113
+ }
114
+ if (WRAPPED_STRUCTURED_SIGNAL_LINE_REGEX.test(withoutListPrefix)) {
115
+ return withoutListPrefix.slice(1, -1).trim();
116
+ }
117
+ return null;
118
+ }
119
+
120
+ function parseStructuredSignalCandidate(line) {
121
+ const rawLine = String(line || "").trim();
122
+ if (!rawLine) {
123
+ return null;
124
+ }
125
+ const canonicalLine = normalizeStructuredSignalLine(rawLine);
126
+ if (!canonicalLine) {
127
+ return null;
128
+ }
129
+ const tagMatch = canonicalLine.match(/^\[wave-([a-z0-9-]+)(?:\]|\s|=|$)/i);
130
+ if (!tagMatch) {
131
+ return null;
132
+ }
133
+ const kind = STRUCTURED_SIGNAL_KIND_BY_TAG[String(tagMatch[1] || "").toLowerCase()] || null;
134
+ const componentIdMatch = canonicalLine.match(/\bcomponent=([a-z0-9._-]+)/i);
135
+ return {
136
+ rawLine,
137
+ canonicalLine,
138
+ kind,
139
+ componentId: componentIdMatch ? String(componentIdMatch[1] || "").trim() : null,
140
+ };
141
+ }
142
+
143
+ function collectStructuredSignalCandidates(text) {
60
144
  if (!text) {
61
- return "";
145
+ return [];
62
146
  }
63
- const normalizedLines = [];
147
+ const candidates = [];
64
148
  let fenceLines = null;
65
149
  for (const rawLine of String(text || "").split(/\r?\n/)) {
66
150
  const trimmed = rawLine.trim();
@@ -69,11 +153,11 @@ function normalizeStructuredSignalText(text) {
69
153
  fenceLines = [];
70
154
  continue;
71
155
  }
72
- const normalizedFenceLines = fenceLines
73
- .map((line) => normalizeStructuredSignalLine(line))
156
+ const fenceCandidates = fenceLines
157
+ .map((line) => parseStructuredSignalCandidate(line))
74
158
  .filter(Boolean);
75
- if (normalizedFenceLines.length > 0 && normalizedFenceLines.length === fenceLines.length) {
76
- normalizedLines.push(...normalizedFenceLines);
159
+ if (fenceCandidates.length > 0 && fenceCandidates.length === fenceLines.length) {
160
+ candidates.push(...fenceCandidates);
77
161
  }
78
162
  fenceLines = null;
79
163
  continue;
@@ -82,29 +166,48 @@ function normalizeStructuredSignalText(text) {
82
166
  if (!trimmed) {
83
167
  continue;
84
168
  }
85
- fenceLines.push(trimmed);
169
+ fenceLines.push(rawLine);
86
170
  continue;
87
171
  }
88
- const normalized = normalizeStructuredSignalLine(trimmed);
89
- if (normalized) {
90
- normalizedLines.push(normalized);
172
+ const candidate = parseStructuredSignalCandidate(rawLine);
173
+ if (candidate) {
174
+ candidates.push(candidate);
91
175
  }
92
176
  }
93
- return normalizedLines.join("\n");
177
+ return candidates;
94
178
  }
95
179
 
96
- function normalizeStructuredSignalLine(line) {
97
- const trimmed = String(line || "").trim();
98
- if (!trimmed) {
99
- return null;
100
- }
101
- if (STRUCTURED_SIGNAL_LINE_REGEX.test(trimmed)) {
102
- return trimmed;
103
- }
104
- if (WRAPPED_STRUCTURED_SIGNAL_LINE_REGEX.test(trimmed)) {
105
- return trimmed.slice(1, -1);
180
+ function buildStructuredSignalDiagnostics(candidates) {
181
+ const diagnostics = buildEmptyStructuredSignalDiagnostics();
182
+ for (const candidate of candidates || []) {
183
+ if (!candidate?.kind || !diagnostics[candidate.kind]) {
184
+ continue;
185
+ }
186
+ const bucket = diagnostics[candidate.kind];
187
+ bucket.rawCount += 1;
188
+ if (candidate.kind === "component" && candidate.componentId) {
189
+ bucket.seenComponentIds.push(candidate.componentId);
190
+ }
191
+ const strictRegex = STRUCTURED_SIGNAL_LINE_REGEX_BY_KIND[candidate.kind];
192
+ if (strictRegex.test(candidate.canonicalLine)) {
193
+ bucket.acceptedCount += 1;
194
+ continue;
195
+ }
196
+ pushRejectedStructuredSignalSample(bucket, {
197
+ line: candidate.rawLine,
198
+ ...(candidate.kind === "component" && candidate.componentId ? { componentId: candidate.componentId } : {}),
199
+ });
106
200
  }
107
- return null;
201
+ diagnostics.component.seenComponentIds = Array.from(new Set(diagnostics.component.seenComponentIds)).sort();
202
+ return diagnostics;
203
+ }
204
+
205
+ function extractStructuredSignalPayload(text) {
206
+ const candidates = collectStructuredSignalCandidates(text);
207
+ return {
208
+ signalText: candidates.map((candidate) => candidate.canonicalLine).join("\n"),
209
+ diagnostics: buildStructuredSignalDiagnostics(candidates),
210
+ };
108
211
  }
109
212
 
110
213
  function cleanText(value) {
@@ -335,17 +438,10 @@ export function agentSummaryPathFromStatusPath(statusPath) {
335
438
  : `${statusPath}.summary.json`;
336
439
  }
337
440
 
338
- export function readAgentExecutionSummary(summaryPathOrStatusPath) {
339
- const summaryPath = summaryPathOrStatusPath.endsWith(".summary.json")
340
- ? summaryPathOrStatusPath
341
- : agentSummaryPathFromStatusPath(summaryPathOrStatusPath);
342
- const payload = readJsonOrNull(summaryPath);
343
- return payload && typeof payload === "object" ? payload : null;
344
- }
345
-
346
441
  export function buildAgentExecutionSummary({ agent, statusRecord, logPath, reportPath = null }) {
347
442
  const logText = readFileTail(logPath, 60000);
348
- const signalText = normalizeStructuredSignalText(logText);
443
+ const structuredSignals = extractStructuredSignalPayload(logText);
444
+ const signalText = structuredSignals.signalText;
349
445
  const reportText =
350
446
  reportPath && readJsonOrNull(reportPath) === null
351
447
  ? readFileTail(reportPath, 60000)
@@ -442,6 +538,7 @@ export function buildAgentExecutionSummary({ agent, statusRecord, logPath, repor
442
538
  detail: cleanText(verdict.detail),
443
539
  }
444
540
  : null,
541
+ structuredSignalDiagnostics: structuredSignals.diagnostics,
445
542
  terminationReason: termination.reason,
446
543
  terminationHint: termination.hint,
447
544
  terminationObservedTurnLimit:
@@ -459,6 +556,113 @@ export function writeAgentExecutionSummary(summaryPathOrStatusPath, summary) {
459
556
  return summaryPath;
460
557
  }
461
558
 
559
+ function resolveStatusRecordForSummaryRead(summaryPathOrStatusPath, options = {}) {
560
+ if (options.statusRecord && typeof options.statusRecord === "object") {
561
+ return options.statusRecord;
562
+ }
563
+ const explicitStatusPath =
564
+ typeof options.statusPath === "string" && options.statusPath.trim() ? options.statusPath : null;
565
+ const derivedStatusPath =
566
+ !summaryPathOrStatusPath.endsWith(".summary.json") ? summaryPathOrStatusPath : null;
567
+ const statusPath = explicitStatusPath || derivedStatusPath;
568
+ if (!statusPath) {
569
+ return null;
570
+ }
571
+ const payload = readJsonOrNull(statusPath);
572
+ return payload && typeof payload === "object" ? payload : null;
573
+ }
574
+
575
+ function summaryNeedsStructuredSignalRefresh(payload, options = {}) {
576
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
577
+ return false;
578
+ }
579
+ if (payload.structuredSignalDiagnostics && typeof payload.structuredSignalDiagnostics === "object") {
580
+ return false;
581
+ }
582
+ const agent = options.agent;
583
+ const contract = normalizeExitContract(agent?.exitContract);
584
+ if (!contract) {
585
+ return false;
586
+ }
587
+ if (!payload.proof || !payload.docDelta) {
588
+ return true;
589
+ }
590
+ const ownedComponents = Array.isArray(agent?.components) ? agent.components : [];
591
+ if (ownedComponents.length === 0) {
592
+ return false;
593
+ }
594
+ const componentMarkers = new Map(
595
+ Array.isArray(payload.components)
596
+ ? payload.components.map((component) => [component.componentId, component])
597
+ : [],
598
+ );
599
+ return ownedComponents.some((componentId) => !componentMarkers.has(componentId));
600
+ }
601
+
602
+ function refreshExecutionSummaryIfStale(summaryPathOrStatusPath, payload, options = {}) {
603
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
604
+ return payload;
605
+ }
606
+ if (!summaryNeedsStructuredSignalRefresh(payload, options)) {
607
+ return payload;
608
+ }
609
+ if (!options.agent || !options.logPath || !fs.existsSync(options.logPath)) {
610
+ return payload;
611
+ }
612
+ const refreshed = buildAgentExecutionSummary({
613
+ agent: options.agent,
614
+ statusRecord: resolveStatusRecordForSummaryRead(summaryPathOrStatusPath, options),
615
+ logPath: options.logPath,
616
+ reportPath: options.reportPath || null,
617
+ });
618
+ writeAgentExecutionSummary(summaryPathOrStatusPath, refreshed);
619
+ return refreshed;
620
+ }
621
+
622
+ export function readAgentExecutionSummary(summaryPathOrStatusPath, options = {}) {
623
+ const summaryPath = summaryPathOrStatusPath.endsWith(".summary.json")
624
+ ? summaryPathOrStatusPath
625
+ : agentSummaryPathFromStatusPath(summaryPathOrStatusPath);
626
+ const payload = readJsonOrNull(summaryPath);
627
+ const summary = payload && typeof payload === "object" ? payload : null;
628
+ return refreshExecutionSummaryIfStale(summaryPathOrStatusPath, summary, options);
629
+ }
630
+
631
+ function structuredSignalBucket(summary, key) {
632
+ const diagnostics = summary?.structuredSignalDiagnostics;
633
+ if (!diagnostics || typeof diagnostics !== "object") {
634
+ return null;
635
+ }
636
+ const bucket = diagnostics[key];
637
+ return bucket && typeof bucket === "object" ? bucket : null;
638
+ }
639
+
640
+ function rejectedStructuredSignalLine(summary, key, predicate = null) {
641
+ const bucket = structuredSignalBucket(summary, key);
642
+ const rejected = Array.isArray(bucket?.rejectedSamples) ? bucket.rejectedSamples : [];
643
+ const match = typeof predicate === "function" ? rejected.find(predicate) : rejected[0];
644
+ return cleanText(match?.line || "");
645
+ }
646
+
647
+ function hasRejectedStructuredSignal(summary, key) {
648
+ const bucket = structuredSignalBucket(summary, key);
649
+ return Number(bucket?.rawCount || 0) > 0 && Number(bucket?.acceptedCount || 0) === 0;
650
+ }
651
+
652
+ function invalidStructuredSignalDetail(agentId, markerName, summary, key, extraDetail = "", predicate = null) {
653
+ const sample = rejectedStructuredSignalLine(summary, key, predicate);
654
+ const detailParts = [
655
+ `Saw raw ${markerName} marker text for ${agentId}, but none of it was accepted into the structured summary.`,
656
+ ];
657
+ if (extraDetail) {
658
+ detailParts.push(extraDetail);
659
+ }
660
+ if (sample) {
661
+ detailParts.push(`Rejected sample: ${sample}`);
662
+ }
663
+ return appendTerminationHint(detailParts.join(" "), summary);
664
+ }
665
+
462
666
  export function validateImplementationSummary(agent, summary) {
463
667
  const contract = normalizeExitContract(agent?.exitContract);
464
668
  if (!contract) {
@@ -472,6 +676,13 @@ export function validateImplementationSummary(agent, summary) {
472
676
  };
473
677
  }
474
678
  if (!summary.proof) {
679
+ if (hasRejectedStructuredSignal(summary, "proof")) {
680
+ return {
681
+ ok: false,
682
+ statusCode: "invalid-wave-proof-format",
683
+ detail: invalidStructuredSignalDetail(agent.agentId, "[wave-proof]", summary, "proof"),
684
+ };
685
+ }
475
686
  return {
476
687
  ok: false,
477
688
  statusCode: "missing-wave-proof",
@@ -507,6 +718,13 @@ export function validateImplementationSummary(agent, summary) {
507
718
  };
508
719
  }
509
720
  if (!summary.docDelta) {
721
+ if (hasRejectedStructuredSignal(summary, "docDelta")) {
722
+ return {
723
+ ok: false,
724
+ statusCode: "invalid-doc-delta-format",
725
+ detail: invalidStructuredSignalDetail(agent.agentId, "[wave-doc-delta]", summary, "docDelta"),
726
+ };
727
+ }
510
728
  return {
511
729
  ok: false,
512
730
  statusCode: "missing-doc-delta",
@@ -522,6 +740,10 @@ export function validateImplementationSummary(agent, summary) {
522
740
  }
523
741
  const ownedComponents = Array.isArray(agent?.components) ? agent.components : [];
524
742
  if (ownedComponents.length > 0) {
743
+ const componentDiagnostics = structuredSignalBucket(summary, "component");
744
+ const seenComponentIds = new Set(
745
+ Array.isArray(componentDiagnostics?.seenComponentIds) ? componentDiagnostics.seenComponentIds : [],
746
+ );
525
747
  const componentMarkers = new Map(
526
748
  Array.isArray(summary.components)
527
749
  ? summary.components.map((component) => [component.componentId, component])
@@ -530,6 +752,23 @@ export function validateImplementationSummary(agent, summary) {
530
752
  for (const componentId of ownedComponents) {
531
753
  const marker = componentMarkers.get(componentId);
532
754
  if (!marker) {
755
+ if (
756
+ Number(componentDiagnostics?.rawCount || 0) > 0 &&
757
+ (seenComponentIds.has(componentId) || Number(componentDiagnostics?.acceptedCount || 0) === 0)
758
+ ) {
759
+ return {
760
+ ok: false,
761
+ statusCode: "invalid-wave-component-format",
762
+ detail: invalidStructuredSignalDetail(
763
+ agent.agentId,
764
+ "[wave-component]",
765
+ summary,
766
+ "component",
767
+ `Expected a valid component marker for ${componentId}.`,
768
+ (sample) => cleanText(sample?.componentId) === componentId,
769
+ ),
770
+ };
771
+ }
533
772
  return {
534
773
  ok: false,
535
774
  statusCode: "missing-wave-component",
@@ -928,3 +1167,178 @@ export function validateContQaSummary(agent, summary, options = {}) {
928
1167
  detail: summary.verdict.detail || summary.gate.detail || "cont-QA gate passed.",
929
1168
  };
930
1169
  }
1170
+
1171
+ // ---------------------------------------------------------------------------
1172
+ // Agent Result Envelope — Wave 3
1173
+ // ---------------------------------------------------------------------------
1174
+
1175
+ import { toIsoTimestamp } from "./shared.mjs";
1176
+
1177
+ /**
1178
+ * Path to the envelope file derived from the status path.
1179
+ *
1180
+ * @param {string} statusPath - Path to the .status or .summary file
1181
+ * @returns {string} The envelope file path
1182
+ */
1183
+ export function agentEnvelopePathFromStatusPath(statusPath) {
1184
+ if (statusPath.endsWith(".summary.json")) {
1185
+ return statusPath.replace(/\.summary\.json$/i, ".envelope.json");
1186
+ }
1187
+ if (statusPath.endsWith(".status")) {
1188
+ return statusPath.replace(/\.status$/i, ".envelope.json");
1189
+ }
1190
+ return `${statusPath}.envelope.json`;
1191
+ }
1192
+
1193
+ /**
1194
+ * Build a structured result envelope from an already-parsed execution summary.
1195
+ * Pure function — the envelope is a normalized projection of the summary.
1196
+ *
1197
+ * @param {object} agent - Agent definition from wave
1198
+ * @param {object} summary - Execution summary from buildAgentExecutionSummary
1199
+ * @returns {object} AgentResultEnvelope
1200
+ */
1201
+ export function buildAgentResultEnvelope(agent, summary) {
1202
+ const safeAgent = agent || {};
1203
+ const safeSummary = summary || {};
1204
+
1205
+ // Exit contract from proof dimensions + doc delta
1206
+ const proof = safeSummary.proof || {};
1207
+ const docDelta = safeSummary.docDelta || {};
1208
+ const exitContract = {
1209
+ completion: proof.completion || null,
1210
+ durability: proof.durability || null,
1211
+ proof: proof.proof || null,
1212
+ docImpact: docDelta.state || null,
1213
+ };
1214
+
1215
+ // Proof artifacts
1216
+ const proofArtifacts = Array.isArray(safeSummary.proofArtifacts)
1217
+ ? safeSummary.proofArtifacts.map((artifact) => ({
1218
+ path: artifact.path || null,
1219
+ kind: artifact.kind || null,
1220
+ sha256: artifact.sha256 || null,
1221
+ exists: artifact.exists === true,
1222
+ }))
1223
+ : [];
1224
+
1225
+ // Deliverables
1226
+ const deliverables = Array.isArray(safeSummary.deliverables)
1227
+ ? safeSummary.deliverables.map((d) => ({
1228
+ path: d.path || null,
1229
+ exists: d.exists === true,
1230
+ }))
1231
+ : [];
1232
+
1233
+ // Components
1234
+ const components = Array.isArray(safeSummary.components)
1235
+ ? safeSummary.components.map((c) => ({
1236
+ componentId: c.componentId || null,
1237
+ level: c.level || null,
1238
+ state: c.state || null,
1239
+ }))
1240
+ : [];
1241
+
1242
+ // Gate claims from the gate marker
1243
+ const gateClaims = [];
1244
+ if (safeSummary.gate) {
1245
+ const gateKeys = ["architecture", "integration", "durability", "live", "docs"];
1246
+ for (const key of gateKeys) {
1247
+ if (safeSummary.gate[key]) {
1248
+ gateClaims.push({
1249
+ gateId: key,
1250
+ claim: safeSummary.gate[key],
1251
+ detail: safeSummary.gate.detail || null,
1252
+ });
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ // Validation outputs from proof state
1258
+ const validationOutputs = {
1259
+ testsPassed: proof.state === "met" && proof.proof != null,
1260
+ buildPassed: proof.state === "met",
1261
+ };
1262
+
1263
+ // Risk notes
1264
+ const riskNotes = Array.isArray(safeSummary.riskNotes) ? safeSummary.riskNotes : [];
1265
+
1266
+ // Unresolved blockers
1267
+ const unresolvedBlockers = Array.isArray(safeSummary.unresolvedBlockers)
1268
+ ? safeSummary.unresolvedBlockers
1269
+ : [];
1270
+
1271
+ // Docs deltas
1272
+ const docsDeltas = [];
1273
+ if (docDelta.state) {
1274
+ docsDeltas.push({
1275
+ state: docDelta.state,
1276
+ paths: Array.isArray(docDelta.paths) ? docDelta.paths : [],
1277
+ detail: docDelta.detail || null,
1278
+ });
1279
+ }
1280
+
1281
+ // Security findings
1282
+ const securityFindings = [];
1283
+ if (safeSummary.security) {
1284
+ securityFindings.push({
1285
+ state: safeSummary.security.state || null,
1286
+ findings: safeSummary.security.findings || 0,
1287
+ approvals: safeSummary.security.approvals || 0,
1288
+ detail: safeSummary.security.detail || null,
1289
+ });
1290
+ }
1291
+
1292
+ // Integration claims
1293
+ const integrationClaims = [];
1294
+ if (safeSummary.integration) {
1295
+ integrationClaims.push({
1296
+ state: safeSummary.integration.state || null,
1297
+ claims: safeSummary.integration.claims || 0,
1298
+ conflicts: safeSummary.integration.conflicts || 0,
1299
+ blockers: safeSummary.integration.blockers || 0,
1300
+ detail: safeSummary.integration.detail || null,
1301
+ });
1302
+ }
1303
+
1304
+ return {
1305
+ envelopeVersion: 1,
1306
+ agentId: safeAgent.agentId || safeSummary.agentId || null,
1307
+ exitContract,
1308
+ proofArtifacts,
1309
+ deliverables,
1310
+ components,
1311
+ gateClaims,
1312
+ validationOutputs,
1313
+ riskNotes,
1314
+ unresolvedBlockers,
1315
+ docsDeltas,
1316
+ securityFindings,
1317
+ integrationClaims,
1318
+ createdAt: toIsoTimestamp(),
1319
+ };
1320
+ }
1321
+
1322
+ /**
1323
+ * Write an agent result envelope alongside the summary file.
1324
+ *
1325
+ * @param {string} statusPath - Path to the .status file
1326
+ * @param {object} envelope - Result from buildAgentResultEnvelope
1327
+ */
1328
+ export function writeAgentResultEnvelope(statusPath, envelope) {
1329
+ const envelopePath = agentEnvelopePathFromStatusPath(statusPath);
1330
+ writeJsonAtomic(envelopePath, envelope);
1331
+ return envelopePath;
1332
+ }
1333
+
1334
+ /**
1335
+ * Read an agent result envelope if it exists.
1336
+ *
1337
+ * @param {string} statusPath - Path to the .status file
1338
+ * @returns {object|null} The envelope or null
1339
+ */
1340
+ export function readAgentResultEnvelope(statusPath) {
1341
+ const envelopePath = agentEnvelopePathFromStatusPath(statusPath);
1342
+ const payload = readJsonOrNull(envelopePath);
1343
+ return payload && typeof payload === "object" ? payload : null;
1344
+ }