@chllming/wave-orchestration 0.7.1 → 0.7.3

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 +30 -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 +36 -0
  18. package/scripts/wave-orchestrator/agent-state.mjs +462 -35
  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.3` 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.3`.
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.3 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.3` 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.3` 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.3`:
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.3",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,6 +2,42 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.7.3",
7
+ "date": "2026-03-23",
8
+ "summary": "Unmatched-fence structured-signal recovery, stale-summary rebuild hardening, and 0.7.3 release-surface alignment.",
9
+ "features": [
10
+ "Implementation summaries now recover final `[wave-proof]`, `[wave-doc-delta]`, and `[wave-component]` markers even when the log tail ends inside a malformed unmatched fenced block.",
11
+ "Legacy proof-centric summaries now refresh from source logs when required proof, doc-delta, or owned-component fields are still missing, even if stale `structuredSignalDiagnostics` data already exists.",
12
+ "Regression coverage now exercises unmatched end-of-tail fence handling and stale diagnostics-backed summary refresh directly.",
13
+ "Shipped package metadata, README, migration guidance, sample-wave docs, and npm publishing instructions now point at the `0.7.3` release surface."
14
+ ],
15
+ "manualSteps": [
16
+ "If an older lane still contains proof-centric `.summary.json` files with missing proof/doc/component fields, rerun the relevant launcher or status surface once after upgrading so the summaries can self-refresh from the source logs.",
17
+ "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.",
18
+ "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."
19
+ ],
20
+ "breaking": false
21
+ },
22
+ {
23
+ "version": "0.7.2",
24
+ "date": "2026-03-23",
25
+ "summary": "Implementation marker parsing repair, proof-centric summary refresh, and 0.7.2 release-surface alignment.",
26
+ "features": [
27
+ "Implementation summaries now accept final `[wave-proof]`, `[wave-doc-delta]`, and `[wave-component]` markers when agents emit them as bullet-prefixed structured blocks.",
28
+ "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.",
29
+ "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.",
30
+ "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.",
31
+ "Shipped package metadata, README, migration guidance, sample-wave docs, and npm publishing instructions now point at the `0.7.2` release surface."
32
+ ],
33
+ "manualSteps": [
34
+ "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.",
35
+ "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.",
36
+ "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.",
37
+ "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."
38
+ ],
39
+ "breaking": false
40
+ },
5
41
  {
6
42
  "version": "0.7.1",
7
43
  "date": "2026-03-23",
@@ -53,14 +53,113 @@ 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 appendParsedStructuredSignalCandidates(lines, candidates, { requireAll = false } = {}) {
144
+ const parsedCandidates = [];
145
+ for (const line of lines || []) {
146
+ const candidate = parseStructuredSignalCandidate(line);
147
+ if (candidate) {
148
+ parsedCandidates.push(candidate);
149
+ continue;
150
+ }
151
+ if (requireAll) {
152
+ return;
153
+ }
154
+ }
155
+ candidates.push(...parsedCandidates);
156
+ }
157
+
158
+ function collectStructuredSignalCandidates(text) {
60
159
  if (!text) {
61
- return "";
160
+ return [];
62
161
  }
63
- const normalizedLines = [];
162
+ const candidates = [];
64
163
  let fenceLines = null;
65
164
  for (const rawLine of String(text || "").split(/\r?\n/)) {
66
165
  const trimmed = rawLine.trim();
@@ -69,12 +168,7 @@ function normalizeStructuredSignalText(text) {
69
168
  fenceLines = [];
70
169
  continue;
71
170
  }
72
- const normalizedFenceLines = fenceLines
73
- .map((line) => normalizeStructuredSignalLine(line))
74
- .filter(Boolean);
75
- if (normalizedFenceLines.length > 0 && normalizedFenceLines.length === fenceLines.length) {
76
- normalizedLines.push(...normalizedFenceLines);
77
- }
171
+ appendParsedStructuredSignalCandidates(fenceLines, candidates, { requireAll: true });
78
172
  fenceLines = null;
79
173
  continue;
80
174
  }
@@ -82,29 +176,51 @@ function normalizeStructuredSignalText(text) {
82
176
  if (!trimmed) {
83
177
  continue;
84
178
  }
85
- fenceLines.push(trimmed);
179
+ fenceLines.push(rawLine);
86
180
  continue;
87
181
  }
88
- const normalized = normalizeStructuredSignalLine(trimmed);
89
- if (normalized) {
90
- normalizedLines.push(normalized);
182
+ const candidate = parseStructuredSignalCandidate(rawLine);
183
+ if (candidate) {
184
+ candidates.push(candidate);
91
185
  }
92
186
  }
93
- return normalizedLines.join("\n");
187
+ if (fenceLines !== null) {
188
+ appendParsedStructuredSignalCandidates(fenceLines, candidates);
189
+ }
190
+ return candidates;
94
191
  }
95
192
 
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);
193
+ function buildStructuredSignalDiagnostics(candidates) {
194
+ const diagnostics = buildEmptyStructuredSignalDiagnostics();
195
+ for (const candidate of candidates || []) {
196
+ if (!candidate?.kind || !diagnostics[candidate.kind]) {
197
+ continue;
198
+ }
199
+ const bucket = diagnostics[candidate.kind];
200
+ bucket.rawCount += 1;
201
+ if (candidate.kind === "component" && candidate.componentId) {
202
+ bucket.seenComponentIds.push(candidate.componentId);
203
+ }
204
+ const strictRegex = STRUCTURED_SIGNAL_LINE_REGEX_BY_KIND[candidate.kind];
205
+ if (strictRegex.test(candidate.canonicalLine)) {
206
+ bucket.acceptedCount += 1;
207
+ continue;
208
+ }
209
+ pushRejectedStructuredSignalSample(bucket, {
210
+ line: candidate.rawLine,
211
+ ...(candidate.kind === "component" && candidate.componentId ? { componentId: candidate.componentId } : {}),
212
+ });
106
213
  }
107
- return null;
214
+ diagnostics.component.seenComponentIds = Array.from(new Set(diagnostics.component.seenComponentIds)).sort();
215
+ return diagnostics;
216
+ }
217
+
218
+ function extractStructuredSignalPayload(text) {
219
+ const candidates = collectStructuredSignalCandidates(text);
220
+ return {
221
+ signalText: candidates.map((candidate) => candidate.canonicalLine).join("\n"),
222
+ diagnostics: buildStructuredSignalDiagnostics(candidates),
223
+ };
108
224
  }
109
225
 
110
226
  function cleanText(value) {
@@ -335,17 +451,10 @@ export function agentSummaryPathFromStatusPath(statusPath) {
335
451
  : `${statusPath}.summary.json`;
336
452
  }
337
453
 
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
454
  export function buildAgentExecutionSummary({ agent, statusRecord, logPath, reportPath = null }) {
347
455
  const logText = readFileTail(logPath, 60000);
348
- const signalText = normalizeStructuredSignalText(logText);
456
+ const structuredSignals = extractStructuredSignalPayload(logText);
457
+ const signalText = structuredSignals.signalText;
349
458
  const reportText =
350
459
  reportPath && readJsonOrNull(reportPath) === null
351
460
  ? readFileTail(reportPath, 60000)
@@ -442,6 +551,7 @@ export function buildAgentExecutionSummary({ agent, statusRecord, logPath, repor
442
551
  detail: cleanText(verdict.detail),
443
552
  }
444
553
  : null,
554
+ structuredSignalDiagnostics: structuredSignals.diagnostics,
445
555
  terminationReason: termination.reason,
446
556
  terminationHint: termination.hint,
447
557
  terminationObservedTurnLimit:
@@ -459,6 +569,113 @@ export function writeAgentExecutionSummary(summaryPathOrStatusPath, summary) {
459
569
  return summaryPath;
460
570
  }
461
571
 
572
+ function resolveStatusRecordForSummaryRead(summaryPathOrStatusPath, options = {}) {
573
+ if (options.statusRecord && typeof options.statusRecord === "object") {
574
+ return options.statusRecord;
575
+ }
576
+ const explicitStatusPath =
577
+ typeof options.statusPath === "string" && options.statusPath.trim() ? options.statusPath : null;
578
+ const derivedStatusPath =
579
+ !summaryPathOrStatusPath.endsWith(".summary.json") ? summaryPathOrStatusPath : null;
580
+ const statusPath = explicitStatusPath || derivedStatusPath;
581
+ if (!statusPath) {
582
+ return null;
583
+ }
584
+ const payload = readJsonOrNull(statusPath);
585
+ return payload && typeof payload === "object" ? payload : null;
586
+ }
587
+
588
+ function summaryNeedsStructuredSignalRefresh(payload, options = {}) {
589
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
590
+ return false;
591
+ }
592
+ const agent = options.agent;
593
+ const contract = normalizeExitContract(agent?.exitContract);
594
+ if (!contract) {
595
+ return false;
596
+ }
597
+ const missingProofOrDocDelta = !payload.proof || !payload.docDelta;
598
+ const ownedComponents = Array.isArray(agent?.components) ? agent.components : [];
599
+ const componentMarkers = new Map(
600
+ Array.isArray(payload.components)
601
+ ? payload.components.map((component) => [component.componentId, component])
602
+ : [],
603
+ );
604
+ const missingOwnedComponents =
605
+ ownedComponents.length > 0 && ownedComponents.some((componentId) => !componentMarkers.has(componentId));
606
+ if (missingProofOrDocDelta || missingOwnedComponents) {
607
+ return true;
608
+ }
609
+ if (payload.structuredSignalDiagnostics && typeof payload.structuredSignalDiagnostics === "object") {
610
+ return false;
611
+ }
612
+ return false;
613
+ }
614
+
615
+ function refreshExecutionSummaryIfStale(summaryPathOrStatusPath, payload, options = {}) {
616
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
617
+ return payload;
618
+ }
619
+ if (!summaryNeedsStructuredSignalRefresh(payload, options)) {
620
+ return payload;
621
+ }
622
+ if (!options.agent || !options.logPath || !fs.existsSync(options.logPath)) {
623
+ return payload;
624
+ }
625
+ const refreshed = buildAgentExecutionSummary({
626
+ agent: options.agent,
627
+ statusRecord: resolveStatusRecordForSummaryRead(summaryPathOrStatusPath, options),
628
+ logPath: options.logPath,
629
+ reportPath: options.reportPath || null,
630
+ });
631
+ writeAgentExecutionSummary(summaryPathOrStatusPath, refreshed);
632
+ return refreshed;
633
+ }
634
+
635
+ export function readAgentExecutionSummary(summaryPathOrStatusPath, options = {}) {
636
+ const summaryPath = summaryPathOrStatusPath.endsWith(".summary.json")
637
+ ? summaryPathOrStatusPath
638
+ : agentSummaryPathFromStatusPath(summaryPathOrStatusPath);
639
+ const payload = readJsonOrNull(summaryPath);
640
+ const summary = payload && typeof payload === "object" ? payload : null;
641
+ return refreshExecutionSummaryIfStale(summaryPathOrStatusPath, summary, options);
642
+ }
643
+
644
+ function structuredSignalBucket(summary, key) {
645
+ const diagnostics = summary?.structuredSignalDiagnostics;
646
+ if (!diagnostics || typeof diagnostics !== "object") {
647
+ return null;
648
+ }
649
+ const bucket = diagnostics[key];
650
+ return bucket && typeof bucket === "object" ? bucket : null;
651
+ }
652
+
653
+ function rejectedStructuredSignalLine(summary, key, predicate = null) {
654
+ const bucket = structuredSignalBucket(summary, key);
655
+ const rejected = Array.isArray(bucket?.rejectedSamples) ? bucket.rejectedSamples : [];
656
+ const match = typeof predicate === "function" ? rejected.find(predicate) : rejected[0];
657
+ return cleanText(match?.line || "");
658
+ }
659
+
660
+ function hasRejectedStructuredSignal(summary, key) {
661
+ const bucket = structuredSignalBucket(summary, key);
662
+ return Number(bucket?.rawCount || 0) > 0 && Number(bucket?.acceptedCount || 0) === 0;
663
+ }
664
+
665
+ function invalidStructuredSignalDetail(agentId, markerName, summary, key, extraDetail = "", predicate = null) {
666
+ const sample = rejectedStructuredSignalLine(summary, key, predicate);
667
+ const detailParts = [
668
+ `Saw raw ${markerName} marker text for ${agentId}, but none of it was accepted into the structured summary.`,
669
+ ];
670
+ if (extraDetail) {
671
+ detailParts.push(extraDetail);
672
+ }
673
+ if (sample) {
674
+ detailParts.push(`Rejected sample: ${sample}`);
675
+ }
676
+ return appendTerminationHint(detailParts.join(" "), summary);
677
+ }
678
+
462
679
  export function validateImplementationSummary(agent, summary) {
463
680
  const contract = normalizeExitContract(agent?.exitContract);
464
681
  if (!contract) {
@@ -472,6 +689,13 @@ export function validateImplementationSummary(agent, summary) {
472
689
  };
473
690
  }
474
691
  if (!summary.proof) {
692
+ if (hasRejectedStructuredSignal(summary, "proof")) {
693
+ return {
694
+ ok: false,
695
+ statusCode: "invalid-wave-proof-format",
696
+ detail: invalidStructuredSignalDetail(agent.agentId, "[wave-proof]", summary, "proof"),
697
+ };
698
+ }
475
699
  return {
476
700
  ok: false,
477
701
  statusCode: "missing-wave-proof",
@@ -507,6 +731,13 @@ export function validateImplementationSummary(agent, summary) {
507
731
  };
508
732
  }
509
733
  if (!summary.docDelta) {
734
+ if (hasRejectedStructuredSignal(summary, "docDelta")) {
735
+ return {
736
+ ok: false,
737
+ statusCode: "invalid-doc-delta-format",
738
+ detail: invalidStructuredSignalDetail(agent.agentId, "[wave-doc-delta]", summary, "docDelta"),
739
+ };
740
+ }
510
741
  return {
511
742
  ok: false,
512
743
  statusCode: "missing-doc-delta",
@@ -522,6 +753,10 @@ export function validateImplementationSummary(agent, summary) {
522
753
  }
523
754
  const ownedComponents = Array.isArray(agent?.components) ? agent.components : [];
524
755
  if (ownedComponents.length > 0) {
756
+ const componentDiagnostics = structuredSignalBucket(summary, "component");
757
+ const seenComponentIds = new Set(
758
+ Array.isArray(componentDiagnostics?.seenComponentIds) ? componentDiagnostics.seenComponentIds : [],
759
+ );
525
760
  const componentMarkers = new Map(
526
761
  Array.isArray(summary.components)
527
762
  ? summary.components.map((component) => [component.componentId, component])
@@ -530,6 +765,23 @@ export function validateImplementationSummary(agent, summary) {
530
765
  for (const componentId of ownedComponents) {
531
766
  const marker = componentMarkers.get(componentId);
532
767
  if (!marker) {
768
+ if (
769
+ Number(componentDiagnostics?.rawCount || 0) > 0 &&
770
+ (seenComponentIds.has(componentId) || Number(componentDiagnostics?.acceptedCount || 0) === 0)
771
+ ) {
772
+ return {
773
+ ok: false,
774
+ statusCode: "invalid-wave-component-format",
775
+ detail: invalidStructuredSignalDetail(
776
+ agent.agentId,
777
+ "[wave-component]",
778
+ summary,
779
+ "component",
780
+ `Expected a valid component marker for ${componentId}.`,
781
+ (sample) => cleanText(sample?.componentId) === componentId,
782
+ ),
783
+ };
784
+ }
533
785
  return {
534
786
  ok: false,
535
787
  statusCode: "missing-wave-component",
@@ -928,3 +1180,178 @@ export function validateContQaSummary(agent, summary, options = {}) {
928
1180
  detail: summary.verdict.detail || summary.gate.detail || "cont-QA gate passed.",
929
1181
  };
930
1182
  }
1183
+
1184
+ // ---------------------------------------------------------------------------
1185
+ // Agent Result Envelope — Wave 3
1186
+ // ---------------------------------------------------------------------------
1187
+
1188
+ import { toIsoTimestamp } from "./shared.mjs";
1189
+
1190
+ /**
1191
+ * Path to the envelope file derived from the status path.
1192
+ *
1193
+ * @param {string} statusPath - Path to the .status or .summary file
1194
+ * @returns {string} The envelope file path
1195
+ */
1196
+ export function agentEnvelopePathFromStatusPath(statusPath) {
1197
+ if (statusPath.endsWith(".summary.json")) {
1198
+ return statusPath.replace(/\.summary\.json$/i, ".envelope.json");
1199
+ }
1200
+ if (statusPath.endsWith(".status")) {
1201
+ return statusPath.replace(/\.status$/i, ".envelope.json");
1202
+ }
1203
+ return `${statusPath}.envelope.json`;
1204
+ }
1205
+
1206
+ /**
1207
+ * Build a structured result envelope from an already-parsed execution summary.
1208
+ * Pure function — the envelope is a normalized projection of the summary.
1209
+ *
1210
+ * @param {object} agent - Agent definition from wave
1211
+ * @param {object} summary - Execution summary from buildAgentExecutionSummary
1212
+ * @returns {object} AgentResultEnvelope
1213
+ */
1214
+ export function buildAgentResultEnvelope(agent, summary) {
1215
+ const safeAgent = agent || {};
1216
+ const safeSummary = summary || {};
1217
+
1218
+ // Exit contract from proof dimensions + doc delta
1219
+ const proof = safeSummary.proof || {};
1220
+ const docDelta = safeSummary.docDelta || {};
1221
+ const exitContract = {
1222
+ completion: proof.completion || null,
1223
+ durability: proof.durability || null,
1224
+ proof: proof.proof || null,
1225
+ docImpact: docDelta.state || null,
1226
+ };
1227
+
1228
+ // Proof artifacts
1229
+ const proofArtifacts = Array.isArray(safeSummary.proofArtifacts)
1230
+ ? safeSummary.proofArtifacts.map((artifact) => ({
1231
+ path: artifact.path || null,
1232
+ kind: artifact.kind || null,
1233
+ sha256: artifact.sha256 || null,
1234
+ exists: artifact.exists === true,
1235
+ }))
1236
+ : [];
1237
+
1238
+ // Deliverables
1239
+ const deliverables = Array.isArray(safeSummary.deliverables)
1240
+ ? safeSummary.deliverables.map((d) => ({
1241
+ path: d.path || null,
1242
+ exists: d.exists === true,
1243
+ }))
1244
+ : [];
1245
+
1246
+ // Components
1247
+ const components = Array.isArray(safeSummary.components)
1248
+ ? safeSummary.components.map((c) => ({
1249
+ componentId: c.componentId || null,
1250
+ level: c.level || null,
1251
+ state: c.state || null,
1252
+ }))
1253
+ : [];
1254
+
1255
+ // Gate claims from the gate marker
1256
+ const gateClaims = [];
1257
+ if (safeSummary.gate) {
1258
+ const gateKeys = ["architecture", "integration", "durability", "live", "docs"];
1259
+ for (const key of gateKeys) {
1260
+ if (safeSummary.gate[key]) {
1261
+ gateClaims.push({
1262
+ gateId: key,
1263
+ claim: safeSummary.gate[key],
1264
+ detail: safeSummary.gate.detail || null,
1265
+ });
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ // Validation outputs from proof state
1271
+ const validationOutputs = {
1272
+ testsPassed: proof.state === "met" && proof.proof != null,
1273
+ buildPassed: proof.state === "met",
1274
+ };
1275
+
1276
+ // Risk notes
1277
+ const riskNotes = Array.isArray(safeSummary.riskNotes) ? safeSummary.riskNotes : [];
1278
+
1279
+ // Unresolved blockers
1280
+ const unresolvedBlockers = Array.isArray(safeSummary.unresolvedBlockers)
1281
+ ? safeSummary.unresolvedBlockers
1282
+ : [];
1283
+
1284
+ // Docs deltas
1285
+ const docsDeltas = [];
1286
+ if (docDelta.state) {
1287
+ docsDeltas.push({
1288
+ state: docDelta.state,
1289
+ paths: Array.isArray(docDelta.paths) ? docDelta.paths : [],
1290
+ detail: docDelta.detail || null,
1291
+ });
1292
+ }
1293
+
1294
+ // Security findings
1295
+ const securityFindings = [];
1296
+ if (safeSummary.security) {
1297
+ securityFindings.push({
1298
+ state: safeSummary.security.state || null,
1299
+ findings: safeSummary.security.findings || 0,
1300
+ approvals: safeSummary.security.approvals || 0,
1301
+ detail: safeSummary.security.detail || null,
1302
+ });
1303
+ }
1304
+
1305
+ // Integration claims
1306
+ const integrationClaims = [];
1307
+ if (safeSummary.integration) {
1308
+ integrationClaims.push({
1309
+ state: safeSummary.integration.state || null,
1310
+ claims: safeSummary.integration.claims || 0,
1311
+ conflicts: safeSummary.integration.conflicts || 0,
1312
+ blockers: safeSummary.integration.blockers || 0,
1313
+ detail: safeSummary.integration.detail || null,
1314
+ });
1315
+ }
1316
+
1317
+ return {
1318
+ envelopeVersion: 1,
1319
+ agentId: safeAgent.agentId || safeSummary.agentId || null,
1320
+ exitContract,
1321
+ proofArtifacts,
1322
+ deliverables,
1323
+ components,
1324
+ gateClaims,
1325
+ validationOutputs,
1326
+ riskNotes,
1327
+ unresolvedBlockers,
1328
+ docsDeltas,
1329
+ securityFindings,
1330
+ integrationClaims,
1331
+ createdAt: toIsoTimestamp(),
1332
+ };
1333
+ }
1334
+
1335
+ /**
1336
+ * Write an agent result envelope alongside the summary file.
1337
+ *
1338
+ * @param {string} statusPath - Path to the .status file
1339
+ * @param {object} envelope - Result from buildAgentResultEnvelope
1340
+ */
1341
+ export function writeAgentResultEnvelope(statusPath, envelope) {
1342
+ const envelopePath = agentEnvelopePathFromStatusPath(statusPath);
1343
+ writeJsonAtomic(envelopePath, envelope);
1344
+ return envelopePath;
1345
+ }
1346
+
1347
+ /**
1348
+ * Read an agent result envelope if it exists.
1349
+ *
1350
+ * @param {string} statusPath - Path to the .status file
1351
+ * @returns {object|null} The envelope or null
1352
+ */
1353
+ export function readAgentResultEnvelope(statusPath) {
1354
+ const envelopePath = agentEnvelopePathFromStatusPath(statusPath);
1355
+ const payload = readJsonOrNull(envelopePath);
1356
+ return payload && typeof payload === "object" ? payload : null;
1357
+ }