@chllming/wave-orchestration 0.9.1 → 0.9.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 (46) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/LICENSE.md +21 -0
  3. package/README.md +20 -9
  4. package/docs/README.md +8 -4
  5. package/docs/agents/wave-security-role.md +1 -0
  6. package/docs/architecture/README.md +1 -1
  7. package/docs/concepts/operating-modes.md +1 -1
  8. package/docs/guides/author-and-run-waves.md +1 -1
  9. package/docs/guides/planner.md +2 -2
  10. package/docs/guides/{recommendations-0.9.1.md → recommendations-0.9.2.md} +7 -7
  11. package/docs/guides/recommendations-0.9.3.md +137 -0
  12. package/docs/guides/sandboxed-environments.md +2 -2
  13. package/docs/plans/current-state.md +8 -2
  14. package/docs/plans/end-state-architecture.md +1 -1
  15. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  16. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  17. package/docs/plans/migration.md +65 -67
  18. package/docs/reference/cli-reference.md +1 -1
  19. package/docs/reference/coordination-and-closure.md +20 -3
  20. package/docs/reference/corridor.md +225 -0
  21. package/docs/reference/npmjs-token-publishing.md +2 -2
  22. package/docs/reference/package-publishing-flow.md +11 -11
  23. package/docs/reference/runtime-config/README.md +61 -3
  24. package/docs/reference/sample-waves.md +5 -5
  25. package/docs/reference/skills.md +1 -1
  26. package/docs/reference/wave-control.md +358 -27
  27. package/docs/roadmap.md +12 -19
  28. package/package.json +1 -1
  29. package/releases/manifest.json +44 -3
  30. package/scripts/wave-cli-bootstrap.mjs +52 -1
  31. package/scripts/wave-orchestrator/agent-state.mjs +26 -9
  32. package/scripts/wave-orchestrator/config.mjs +199 -3
  33. package/scripts/wave-orchestrator/context7.mjs +231 -29
  34. package/scripts/wave-orchestrator/coordination.mjs +15 -1
  35. package/scripts/wave-orchestrator/corridor.mjs +363 -0
  36. package/scripts/wave-orchestrator/derived-state-engine.mjs +38 -1
  37. package/scripts/wave-orchestrator/gate-engine.mjs +20 -0
  38. package/scripts/wave-orchestrator/install.mjs +34 -1
  39. package/scripts/wave-orchestrator/launcher-runtime.mjs +111 -7
  40. package/scripts/wave-orchestrator/launcher.mjs +21 -3
  41. package/scripts/wave-orchestrator/planner.mjs +30 -0
  42. package/scripts/wave-orchestrator/projection-writer.mjs +23 -0
  43. package/scripts/wave-orchestrator/provider-runtime.mjs +104 -0
  44. package/scripts/wave-orchestrator/shared.mjs +1 -0
  45. package/scripts/wave-orchestrator/traces.mjs +25 -0
  46. package/scripts/wave-orchestrator/wave-control-client.mjs +14 -1
@@ -0,0 +1,363 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ DEFAULT_CORRIDOR_BASE_URL,
5
+ DEFAULT_CORRIDOR_SEVERITY_THRESHOLD,
6
+ DEFAULT_WAVE_CONTROL_ENDPOINT,
7
+ } from "./config.mjs";
8
+ import { writeJsonAtomic, readJsonOrNull } from "./shared.mjs";
9
+ import {
10
+ isDefaultWaveControlEndpoint,
11
+ readJsonResponse,
12
+ resolveWaveControlAuthToken,
13
+ } from "./provider-runtime.mjs";
14
+ import {
15
+ isContEvalImplementationOwningAgent,
16
+ isDesignAgent,
17
+ isSecurityReviewAgent,
18
+ } from "./role-helpers.mjs";
19
+
20
+ const SEVERITY_RANK = {
21
+ low: 1,
22
+ medium: 2,
23
+ high: 3,
24
+ critical: 4,
25
+ };
26
+
27
+ function normalizeOwnedPath(value) {
28
+ return String(value || "").trim().replaceAll("\\", "/").replace(/\/+$/, "");
29
+ }
30
+
31
+ function isRelevantOwnedPath(value) {
32
+ const normalized = normalizeOwnedPath(value);
33
+ if (!normalized || normalized.startsWith(".tmp/")) {
34
+ return false;
35
+ }
36
+ if (normalized.startsWith("docs/")) {
37
+ return false;
38
+ }
39
+ return !/\.(?:md|txt)$/i.test(normalized);
40
+ }
41
+
42
+ function matchesOwnedPath(findingPath, ownedPath) {
43
+ const normalizedFinding = normalizeOwnedPath(findingPath);
44
+ const normalizedOwned = normalizeOwnedPath(ownedPath);
45
+ if (!normalizedFinding || !normalizedOwned) {
46
+ return false;
47
+ }
48
+ return normalizedFinding === normalizedOwned || normalizedFinding.startsWith(`${normalizedOwned}/`);
49
+ }
50
+
51
+ function shouldIncludeImplementationOwnedPaths(agent, lanePaths = {}) {
52
+ if (!agent || isSecurityReviewAgent(agent) || isDesignAgent(agent)) {
53
+ return false;
54
+ }
55
+ if (agent.agentId === lanePaths.contQaAgentId || agent.agentId === lanePaths.documentationAgentId) {
56
+ return false;
57
+ }
58
+ if (agent.agentId === lanePaths.integrationAgentId) {
59
+ return false;
60
+ }
61
+ if (agent.agentId === lanePaths.contEvalAgentId) {
62
+ return isContEvalImplementationOwningAgent(agent, {
63
+ contEvalAgentId: lanePaths.contEvalAgentId,
64
+ });
65
+ }
66
+ return true;
67
+ }
68
+
69
+ export function waveCorridorContextPath(lanePaths, waveNumber) {
70
+ return path.join(lanePaths.securityDir, `wave-${waveNumber}-corridor.json`);
71
+ }
72
+
73
+ export function readWaveCorridorContext(lanePaths, waveNumber) {
74
+ return readJsonOrNull(waveCorridorContextPath(lanePaths, waveNumber));
75
+ }
76
+
77
+ function corridorArtifactBase({ lanePaths, wave, ownedPaths, providerMode, source }) {
78
+ return {
79
+ schemaVersion: 1,
80
+ wave,
81
+ lane: lanePaths.lane,
82
+ projectId: lanePaths.project,
83
+ providerMode,
84
+ source,
85
+ requiredAtClosure: lanePaths.externalProviders?.corridor?.requiredAtClosure !== false,
86
+ severityThreshold:
87
+ lanePaths.externalProviders?.corridor?.severityThreshold || DEFAULT_CORRIDOR_SEVERITY_THRESHOLD,
88
+ fetchedAt: new Date().toISOString(),
89
+ relevantOwnedPaths: ownedPaths,
90
+ };
91
+ }
92
+
93
+ function summarizeCorridorPayload(base, guardrails, findings) {
94
+ const thresholdRank = SEVERITY_RANK[String(base.severityThreshold || "critical").toLowerCase()] || 4;
95
+ const matchedFindings = (Array.isArray(findings) ? findings : [])
96
+ .map((finding) => {
97
+ const matchedOwnedPaths = base.relevantOwnedPaths.filter((ownedPath) =>
98
+ matchesOwnedPath(finding.affectedFile, ownedPath),
99
+ );
100
+ if (matchedOwnedPaths.length === 0) {
101
+ return null;
102
+ }
103
+ return {
104
+ id: finding.id || null,
105
+ title: finding.title || null,
106
+ affectedFile: finding.affectedFile || null,
107
+ cwe: finding.cwe || null,
108
+ severity: finding.severity || null,
109
+ state: finding.state || null,
110
+ createdAt: finding.createdAt || null,
111
+ matchedOwnedPaths,
112
+ };
113
+ })
114
+ .filter(Boolean);
115
+ const blockingFindings = matchedFindings.filter((finding) => {
116
+ const rank = SEVERITY_RANK[String(finding.severity || "").toLowerCase()] || 0;
117
+ return rank >= thresholdRank;
118
+ });
119
+ return {
120
+ ...base,
121
+ ok: true,
122
+ guardrails: Array.isArray(guardrails?.reports) ? guardrails.reports : [],
123
+ matchedFindings,
124
+ blockingFindings,
125
+ blocking: blockingFindings.length > 0,
126
+ error: null,
127
+ };
128
+ }
129
+
130
+ function failureCorridorPayload(base, error) {
131
+ return {
132
+ ...base,
133
+ ok: false,
134
+ guardrails: [],
135
+ matchedFindings: [],
136
+ blockingFindings: [],
137
+ blocking: base.requiredAtClosure === true,
138
+ error: error instanceof Error ? error.message : String(error),
139
+ };
140
+ }
141
+
142
+ async function requestCorridorJson(fetchImpl, url, token) {
143
+ const response = await fetchImpl(url, {
144
+ method: "GET",
145
+ headers: {
146
+ authorization: `Bearer ${token}`,
147
+ accept: "application/json",
148
+ },
149
+ signal: AbortSignal.timeout(10000),
150
+ });
151
+ if (!response.ok) {
152
+ const payload = await readJsonResponse(response, null);
153
+ throw new Error(
154
+ `Corridor request failed (${response.status}): ${payload?.error || payload?.message || response.statusText || "unknown error"}`,
155
+ );
156
+ }
157
+ return response.json();
158
+ }
159
+
160
+ async function listCorridorFindings(fetchImpl, baseUrl, projectId, token, findingStates) {
161
+ const findings = [];
162
+ const states = findingStates.size > 0 ? [...findingStates] : [null];
163
+ for (const state of states) {
164
+ let nextUrl = new URL(`${baseUrl}/projects/${projectId}/findings`);
165
+ if (state) {
166
+ nextUrl.searchParams.set("state", state);
167
+ }
168
+ let pages = 0;
169
+ while (nextUrl && pages < 10) {
170
+ const payload = await requestCorridorJson(fetchImpl, nextUrl, token);
171
+ if (Array.isArray(payload)) {
172
+ findings.push(...payload);
173
+ break;
174
+ }
175
+ const items = Array.isArray(payload?.items)
176
+ ? payload.items
177
+ : Array.isArray(payload?.findings)
178
+ ? payload.findings
179
+ : Array.isArray(payload?.data)
180
+ ? payload.data
181
+ : [];
182
+ findings.push(...items);
183
+ if (payload?.nextPageUrl) {
184
+ nextUrl = new URL(payload.nextPageUrl);
185
+ } else if (payload?.nextCursor) {
186
+ nextUrl = new URL(`${baseUrl}/projects/${projectId}/findings`);
187
+ if (state) {
188
+ nextUrl.searchParams.set("state", state);
189
+ }
190
+ nextUrl.searchParams.set("cursor", String(payload.nextCursor));
191
+ } else if (payload?.page && payload?.totalPages && Number(payload.page) < Number(payload.totalPages)) {
192
+ nextUrl = new URL(`${baseUrl}/projects/${projectId}/findings`);
193
+ if (state) {
194
+ nextUrl.searchParams.set("state", state);
195
+ }
196
+ nextUrl.searchParams.set("page", String(Number(payload.page) + 1));
197
+ } else {
198
+ nextUrl = null;
199
+ }
200
+ pages += 1;
201
+ }
202
+ }
203
+ return findings;
204
+ }
205
+
206
+ async function fetchCorridorDirect(fetchImpl, lanePaths, ownedPaths) {
207
+ const corridor = lanePaths.externalProviders?.corridor || {};
208
+ const token =
209
+ process.env[corridor.apiTokenEnvVar || "CORRIDOR_API_TOKEN"] ||
210
+ process.env[corridor.apiKeyFallbackEnvVar || "CORRIDOR_API_KEY"] ||
211
+ "";
212
+ if (!token) {
213
+ throw new Error(
214
+ `Corridor token is missing; set ${corridor.apiTokenEnvVar || "CORRIDOR_API_TOKEN"} or ${corridor.apiKeyFallbackEnvVar || "CORRIDOR_API_KEY"}.`,
215
+ );
216
+ }
217
+ const baseUrl = String(corridor.baseUrl || DEFAULT_CORRIDOR_BASE_URL).replace(/\/$/, "");
218
+ const findingStates = new Set((corridor.findingStates || []).map((state) => String(state).trim().toLowerCase()));
219
+ const [guardrails, findings] = await Promise.all([
220
+ requestCorridorJson(fetchImpl, `${baseUrl}/projects/${corridor.projectId}/reports`, token),
221
+ listCorridorFindings(fetchImpl, baseUrl, corridor.projectId, token, findingStates),
222
+ ]);
223
+ const filteredFindings = (Array.isArray(findings) ? findings : []).filter((finding) =>
224
+ findingStates.size === 0 || findingStates.has(String(finding.state || "").trim().toLowerCase()),
225
+ );
226
+ return summarizeCorridorPayload(
227
+ corridorArtifactBase({
228
+ lanePaths,
229
+ wave: null,
230
+ ownedPaths,
231
+ providerMode: "direct",
232
+ source: "corridor-api",
233
+ }),
234
+ guardrails,
235
+ filteredFindings,
236
+ );
237
+ }
238
+
239
+ async function fetchCorridorBroker(fetchImpl, lanePaths, waveNumber, ownedPaths) {
240
+ const waveControl = lanePaths.waveControl || {};
241
+ const endpoint = String(waveControl.endpoint || DEFAULT_WAVE_CONTROL_ENDPOINT).trim();
242
+ if (!endpoint || isDefaultWaveControlEndpoint(endpoint)) {
243
+ throw new Error("Corridor broker mode requires an owned Wave Control endpoint.");
244
+ }
245
+ const authToken = resolveWaveControlAuthToken(waveControl);
246
+ if (!authToken) {
247
+ throw new Error("WAVE_API_TOKEN is not set; Corridor broker mode is unavailable.");
248
+ }
249
+ const response = await fetchImpl(`${endpoint.replace(/\/$/, "")}/providers/corridor/context`, {
250
+ method: "POST",
251
+ headers: {
252
+ authorization: `Bearer ${authToken}`,
253
+ "content-type": "application/json",
254
+ accept: "application/json",
255
+ },
256
+ body: JSON.stringify({
257
+ projectId: lanePaths.project,
258
+ wave: waveNumber,
259
+ ownedPaths,
260
+ severityThreshold:
261
+ lanePaths.externalProviders?.corridor?.severityThreshold || DEFAULT_CORRIDOR_SEVERITY_THRESHOLD,
262
+ findingStates: lanePaths.externalProviders?.corridor?.findingStates || [],
263
+ }),
264
+ });
265
+ if (!response.ok) {
266
+ const payload = await readJsonResponse(response, null);
267
+ throw new Error(
268
+ `Corridor broker request failed (${response.status}): ${payload?.error || payload?.message || response.statusText || "unknown error"}`,
269
+ );
270
+ }
271
+ return response.json();
272
+ }
273
+
274
+ export async function materializeWaveCorridorContext(
275
+ lanePaths,
276
+ waveDefinition,
277
+ {
278
+ fetchImpl = globalThis.fetch,
279
+ } = {},
280
+ ) {
281
+ const corridor = lanePaths.externalProviders?.corridor || {};
282
+ const waveNumber = waveDefinition?.wave ?? 0;
283
+ const artifactPath = waveCorridorContextPath(lanePaths, waveNumber);
284
+ if (!corridor.enabled) {
285
+ return null;
286
+ }
287
+ const ownedPaths = (Array.isArray(waveDefinition?.agents) ? waveDefinition.agents : [])
288
+ .filter((agent) => shouldIncludeImplementationOwnedPaths(agent, lanePaths))
289
+ .flatMap((agent) => (Array.isArray(agent.ownedPaths) ? agent.ownedPaths : []))
290
+ .map(normalizeOwnedPath)
291
+ .filter(isRelevantOwnedPath);
292
+ const base = corridorArtifactBase({
293
+ lanePaths,
294
+ wave: waveNumber,
295
+ ownedPaths,
296
+ providerMode: corridor.mode || "direct",
297
+ source: null,
298
+ });
299
+ if (ownedPaths.length === 0) {
300
+ const payload = {
301
+ ...base,
302
+ ok: true,
303
+ guardrails: [],
304
+ matchedFindings: [],
305
+ blockingFindings: [],
306
+ blocking: false,
307
+ error: null,
308
+ detail: "No implementation-owned paths were eligible for Corridor matching in this wave.",
309
+ };
310
+ writeJsonAtomic(artifactPath, payload);
311
+ return payload;
312
+ }
313
+ try {
314
+ let payload;
315
+ if (corridor.mode === "broker") {
316
+ payload = await fetchCorridorBroker(fetchImpl, lanePaths, waveNumber, ownedPaths);
317
+ } else if (corridor.mode === "hybrid") {
318
+ try {
319
+ payload = await fetchCorridorBroker(fetchImpl, lanePaths, waveNumber, ownedPaths);
320
+ } catch {
321
+ payload = await fetchCorridorDirect(fetchImpl, lanePaths, ownedPaths);
322
+ }
323
+ } else {
324
+ payload = await fetchCorridorDirect(fetchImpl, lanePaths, ownedPaths);
325
+ }
326
+ const mergedPayload = {
327
+ ...base,
328
+ ...payload,
329
+ wave: waveNumber,
330
+ lane: lanePaths.lane,
331
+ projectId: lanePaths.project,
332
+ relevantOwnedPaths: ownedPaths,
333
+ requiredAtClosure: corridor.requiredAtClosure !== false,
334
+ };
335
+ writeJsonAtomic(artifactPath, mergedPayload);
336
+ return mergedPayload;
337
+ } catch (error) {
338
+ const payload = failureCorridorPayload(base, error);
339
+ writeJsonAtomic(artifactPath, payload);
340
+ return payload;
341
+ }
342
+ }
343
+
344
+ export function renderCorridorPromptContext(corridorContext) {
345
+ if (!corridorContext || corridorContext.ok !== true) {
346
+ if (corridorContext?.error) {
347
+ return `Corridor provider fetch failed: ${corridorContext.error}`;
348
+ }
349
+ return "";
350
+ }
351
+ const lines = [
352
+ `Corridor source: ${corridorContext.source || corridorContext.providerMode || "unknown"}`,
353
+ `Corridor blocking: ${corridorContext.blocking ? "yes" : "no"}`,
354
+ `Corridor threshold: ${corridorContext.severityThreshold || DEFAULT_CORRIDOR_SEVERITY_THRESHOLD}`,
355
+ `Corridor matched findings: ${(corridorContext.matchedFindings || []).length}`,
356
+ ];
357
+ for (const finding of (corridorContext.blockingFindings || []).slice(0, 5)) {
358
+ lines.push(
359
+ `- ${finding.severity || "unknown"} ${finding.affectedFile || "unknown-file"}: ${finding.title || "finding"}`
360
+ );
361
+ }
362
+ return lines.join("\n");
363
+ }
@@ -47,6 +47,10 @@ import {
47
47
  describeContext7Libraries,
48
48
  loadContext7BundleIndex,
49
49
  } from "./context7.mjs";
50
+ import {
51
+ readWaveCorridorContext,
52
+ waveCorridorContextPath,
53
+ } from "./corridor.mjs";
50
54
 
51
55
  export function waveCoordinationLogPath(lanePaths, waveNumber) {
52
56
  return path.join(lanePaths.coordinationDir, `wave-${waveNumber}.jsonl`);
@@ -212,6 +216,7 @@ export function buildWaveSecuritySummary({
212
216
  wave,
213
217
  attempt,
214
218
  summariesByAgentId = {},
219
+ corridorSummary = null,
215
220
  }) {
216
221
  const createdAt = toIsoTimestamp();
217
222
  const securityAgents = (wave.agents || []).filter((agent) =>
@@ -275,7 +280,9 @@ export function buildWaveSecuritySummary({
275
280
  const totalFindings = agents.reduce((sum, entry) => sum + (entry.findings || 0), 0);
276
281
  const totalApprovals = agents.reduce((sum, entry) => sum + (entry.approvals || 0), 0);
277
282
  const detail =
278
- overallState === "blocked"
283
+ corridorSummary?.blocking
284
+ ? `Corridor matched blocking findings for implementation-owned paths.`
285
+ : overallState === "blocked"
279
286
  ? `Security review blocked by ${blockedAgentIds.join(", ")}.`
280
287
  : overallState === "pending"
281
288
  ? `Security review output is incomplete for ${pendingAgentIds.join(", ")}.`
@@ -292,6 +299,17 @@ export function buildWaveSecuritySummary({
292
299
  concernAgentIds,
293
300
  blockedAgentIds,
294
301
  detail,
302
+ corridor: corridorSummary
303
+ ? {
304
+ ok: corridorSummary.ok === true,
305
+ providerMode: corridorSummary.providerMode || null,
306
+ source: corridorSummary.source || null,
307
+ blocking: corridorSummary.blocking === true,
308
+ blockingFindings: corridorSummary.blockingFindings || [],
309
+ matchedFindings: corridorSummary.matchedFindings || [],
310
+ error: corridorSummary.error || null,
311
+ }
312
+ : null,
295
313
  agents,
296
314
  createdAt,
297
315
  updatedAt: createdAt,
@@ -492,6 +510,15 @@ function buildIntegrationEvidence({
492
510
  );
493
511
  }
494
512
  }
513
+ for (const finding of securitySummary?.corridor?.matchedFindings || []) {
514
+ securityFindingEntries.push(
515
+ summarizeGap(
516
+ "corridor",
517
+ `${finding.severity || "unknown"} ${finding.affectedFile || "unknown-file"}: ${finding.title || "finding"}`,
518
+ "Corridor matched a relevant finding.",
519
+ ),
520
+ );
521
+ }
495
522
 
496
523
  return {
497
524
  openClaims: uniqueStringEntries(openClaims),
@@ -508,6 +535,13 @@ function buildIntegrationEvidence({
508
535
  securityState: securitySummary?.overallState || "not-applicable",
509
536
  securityFindings: uniqueStringEntries(securityFindingEntries),
510
537
  securityApprovals: uniqueStringEntries(securityApprovalEntries),
538
+ corridorState: securitySummary?.corridor?.blocking
539
+ ? "blocked"
540
+ : securitySummary?.corridor?.ok === false
541
+ ? "error"
542
+ : securitySummary?.corridor
543
+ ? "clear"
544
+ : "not-configured",
511
545
  };
512
546
  }
513
547
 
@@ -683,6 +717,7 @@ export function buildWaveDerivedState({
683
717
  wave,
684
718
  attempt,
685
719
  summariesByAgentId,
720
+ corridorSummary: readWaveCorridorContext(lanePaths, wave.wave),
686
721
  });
687
722
  const integrationSummary = buildWaveIntegrationSummary({
688
723
  lanePaths,
@@ -762,6 +797,8 @@ export function buildWaveDerivedState({
762
797
  dependencySnapshotMarkdownPath: waveDependencySnapshotMarkdownPath(lanePaths, wave.wave),
763
798
  securitySummary,
764
799
  securitySummaryPath: waveSecurityPath(lanePaths, wave.wave),
800
+ corridorSummary: readWaveCorridorContext(lanePaths, wave.wave),
801
+ corridorSummaryPath: waveCorridorContextPath(lanePaths, wave.wave),
765
802
  integrationSummary,
766
803
  integrationSummaryPath: waveIntegrationPath(lanePaths, wave.wave),
767
804
  integrationMarkdownPath: waveIntegrationMarkdownPath(lanePaths, wave.wave),
@@ -1414,6 +1414,25 @@ export function readWaveSecurityGatePure(wave, agentResults, options = {}) {
1414
1414
  const securityAgents = agents.filter((agent) =>
1415
1415
  isSecurityReviewAgentWithOptions(agent, options),
1416
1416
  );
1417
+ const corridorSummary = options.derivedState?.corridorSummary || null;
1418
+ if (corridorSummary?.ok === false && corridorSummary?.requiredAtClosure !== false) {
1419
+ return {
1420
+ ok: false,
1421
+ agentId: null,
1422
+ statusCode: "corridor-fetch-failed",
1423
+ detail: corridorSummary.error || "Corridor context fetch failed.",
1424
+ logPath: null,
1425
+ };
1426
+ }
1427
+ if (corridorSummary?.blocking) {
1428
+ return {
1429
+ ok: false,
1430
+ agentId: null,
1431
+ statusCode: "corridor-blocked",
1432
+ detail: `Corridor matched ${corridorSummary.blockingFindings?.length || 0} blocking finding(s) on implementation-owned paths.`,
1433
+ logPath: null,
1434
+ };
1435
+ }
1417
1436
  if (securityAgents.length === 0) {
1418
1437
  return { ok: true, agentId: null, statusCode: "pass",
1419
1438
  detail: "No security reviewer declared for this wave.", logPath: null };
@@ -1526,6 +1545,7 @@ export function buildGateSnapshotPure({ wave, agentResults, derivedState, valida
1526
1545
  contEvalAgentId: laneConfig.contEvalAgentId, mode: validationMode,
1527
1546
  evalTargets: wave.evalTargets, benchmarkCatalogPath: laneConfig.benchmarkCatalogPath });
1528
1547
  const securityGate = readWaveSecurityGatePure(wave, agentResults, {
1548
+ derivedState,
1529
1549
  securityRolePromptPath: laneConfig.securityRolePromptPath,
1530
1550
  });
1531
1551
  const contQaGate = readWaveContQaGatePure(wave, agentResults, {
@@ -16,6 +16,10 @@ import {
16
16
  WAVE_PACKAGE_NAME,
17
17
  } from "./package-version.mjs";
18
18
  import { loadWaveConfig } from "./config.mjs";
19
+ import {
20
+ isDefaultWaveControlEndpoint,
21
+ resolveWaveControlAuthToken,
22
+ } from "./provider-runtime.mjs";
19
23
  import { applyExecutorSelectionsToWave, parseWaveFiles, validateWaveDefinition } from "./wave-files.mjs";
20
24
  import { validateLaneSkillConfiguration } from "./skills.mjs";
21
25
 
@@ -65,7 +69,7 @@ export const STARTER_TEMPLATE_PATHS = [
65
69
  "docs/guides/author-and-run-waves.md",
66
70
  "docs/guides/monorepo-projects.md",
67
71
  "docs/guides/planner.md",
68
- "docs/guides/recommendations-0.9.1.md",
72
+ "docs/guides/recommendations-0.9.3.md",
69
73
  "docs/guides/sandboxed-environments.md",
70
74
  "docs/guides/signal-wrappers.md",
71
75
  "docs/guides/terminal-surfaces.md",
@@ -82,6 +86,8 @@ export const STARTER_TEMPLATE_PATHS = [
82
86
  "docs/plans/wave-orchestrator.md",
83
87
  "docs/plans/waves/wave-0.md",
84
88
  "docs/reference/cli-reference.md",
89
+ "docs/reference/coordination-and-closure.md",
90
+ "docs/reference/corridor.md",
85
91
  "docs/reference/live-proof-waves.md",
86
92
  "docs/reference/npmjs-token-publishing.md",
87
93
  "docs/reference/npmjs-trusted-publishing.md",
@@ -89,6 +95,7 @@ export const STARTER_TEMPLATE_PATHS = [
89
95
  "docs/reference/repository-guidance.md",
90
96
  "docs/reference/sample-waves.md",
91
97
  "docs/reference/skills.md",
98
+ "docs/reference/wave-control.md",
92
99
  "docs/reference/wave-planning-lessons.md",
93
100
  "skills/role-design/SKILL.md",
94
101
  "skills/role-design/skill.json",
@@ -421,6 +428,32 @@ export function runDoctor() {
421
428
  if (config) {
422
429
  try {
423
430
  const lanePaths = buildLanePaths(config.defaultLane, { config });
431
+ const context7Mode = lanePaths.externalProviders?.context7?.mode || "direct";
432
+ const corridor = lanePaths.externalProviders?.corridor || {};
433
+ const corridorMode = corridor.mode || "direct";
434
+ const usesBroker =
435
+ context7Mode === "broker" ||
436
+ context7Mode === "hybrid" ||
437
+ corridorMode === "broker" ||
438
+ corridorMode === "hybrid";
439
+ if (usesBroker && isDefaultWaveControlEndpoint(lanePaths.waveControl?.endpoint)) {
440
+ const message =
441
+ "Brokered external providers require an owned Wave Control endpoint; the packaged default endpoint must not be used as a shared broker.";
442
+ if (context7Mode === "broker" || corridorMode === "broker") {
443
+ errors.push(message);
444
+ } else {
445
+ warnings.push(message);
446
+ }
447
+ }
448
+ if (usesBroker && !resolveWaveControlAuthToken(lanePaths.waveControl)) {
449
+ const message =
450
+ "Brokered external providers require a Wave Control auth token. Set WAVE_API_TOKEN or the configured legacy auth token env var.";
451
+ if (context7Mode === "broker" || corridorMode === "broker") {
452
+ errors.push(message);
453
+ } else {
454
+ warnings.push(message);
455
+ }
456
+ }
424
457
  if (!fs.existsSync(path.join(REPO_ROOT, "wave.config.json"))) {
425
458
  errors.push("Missing wave.config.json.");
426
459
  }