@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
@@ -9,6 +9,15 @@ import {
9
9
  sleep,
10
10
  writeJsonAtomic,
11
11
  } from "./shared.mjs";
12
+ import {
13
+ DEFAULT_CONTEXT7_API_KEY_ENV_VAR,
14
+ DEFAULT_WAVE_CONTROL_ENDPOINT,
15
+ } from "./config.mjs";
16
+ import {
17
+ isDefaultWaveControlEndpoint,
18
+ readJsonResponse,
19
+ resolveWaveControlAuthToken,
20
+ } from "./provider-runtime.mjs";
12
21
 
13
22
  export const DEFAULT_CONTEXT7_BUNDLE_INDEX_PATH = path.join(
14
23
  REPO_ROOT,
@@ -277,16 +286,10 @@ function renderPrefetchedContextText({ selection, results, budget }) {
277
286
  return trimContextText(sections.join("\n\n"), budget);
278
287
  }
279
288
 
280
- async function requestContext7(fetchImpl, url, { apiKey, expectText = false, maxRetries = 3 } = {}) {
289
+ async function requestContext7(fetchImpl, request, { expectText = false, maxRetries = 3 } = {}) {
281
290
  let lastError = null;
282
291
  for (let attempt = 0; attempt < maxRetries; attempt += 1) {
283
- const response = await fetchImpl(url, {
284
- method: "GET",
285
- headers: {
286
- Authorization: `Bearer ${apiKey}`,
287
- Accept: expectText ? "text/plain, application/json" : "application/json",
288
- },
289
- });
292
+ const response = await request();
290
293
  if (response.ok) {
291
294
  return expectText ? response.text() : response.json();
292
295
  }
@@ -296,7 +299,7 @@ async function requestContext7(fetchImpl, url, { apiKey, expectText = false, max
296
299
  : 0;
297
300
  let payload = null;
298
301
  try {
299
- payload = await response.json();
302
+ payload = await readJsonResponse(response, null);
300
303
  } catch {
301
304
  payload = null;
302
305
  }
@@ -311,7 +314,193 @@ async function requestContext7(fetchImpl, url, { apiKey, expectText = false, max
311
314
  throw lastError || new Error("Context7 request failed.");
312
315
  }
313
316
 
314
- async function resolveLibraryId(fetchImpl, library, selection, apiKey) {
317
+ function buildDirectContext7Requester(fetchImpl, apiKey) {
318
+ return {
319
+ async search(params) {
320
+ const url = `${CONTEXT7_SEARCH_URL}?${params.toString()}`;
321
+ return requestContext7(
322
+ fetchImpl,
323
+ () =>
324
+ fetchImpl(url, {
325
+ method: "GET",
326
+ headers: {
327
+ Authorization: `Bearer ${apiKey}`,
328
+ Accept: "application/json",
329
+ },
330
+ }),
331
+ );
332
+ },
333
+ async context(params) {
334
+ const url = `${CONTEXT7_CONTEXT_URL}?${params.toString()}`;
335
+ return requestContext7(
336
+ fetchImpl,
337
+ () =>
338
+ fetchImpl(url, {
339
+ method: "GET",
340
+ headers: {
341
+ Authorization: `Bearer ${apiKey}`,
342
+ Accept: "text/plain, application/json",
343
+ },
344
+ }),
345
+ { expectText: true },
346
+ );
347
+ },
348
+ };
349
+ }
350
+
351
+ function buildBrokerContext7Requester(fetchImpl, lanePaths) {
352
+ const waveControl = lanePaths?.waveControl || lanePaths?.laneProfile?.waveControl || {};
353
+ const endpoint = String(waveControl.endpoint || DEFAULT_WAVE_CONTROL_ENDPOINT).trim();
354
+ if (!endpoint || isDefaultWaveControlEndpoint(endpoint)) {
355
+ throw new Error("Context7 broker mode requires an owned Wave Control endpoint.");
356
+ }
357
+ const authToken = resolveWaveControlAuthToken(waveControl);
358
+ if (!authToken) {
359
+ throw new Error("WAVE_API_TOKEN is not set; skipping Context7 broker prefetch.");
360
+ }
361
+ const baseEndpoint = endpoint.replace(/\/$/, "");
362
+ return {
363
+ async search(params) {
364
+ return requestContext7(
365
+ fetchImpl,
366
+ () =>
367
+ fetchImpl(`${baseEndpoint}/providers/context7/search?${params.toString()}`, {
368
+ method: "GET",
369
+ headers: {
370
+ authorization: `Bearer ${authToken}`,
371
+ accept: "application/json",
372
+ },
373
+ }),
374
+ );
375
+ },
376
+ async context(params) {
377
+ return requestContext7(
378
+ fetchImpl,
379
+ () =>
380
+ fetchImpl(`${baseEndpoint}/providers/context7/context?${params.toString()}`, {
381
+ method: "GET",
382
+ headers: {
383
+ authorization: `Bearer ${authToken}`,
384
+ accept: "text/plain, application/json",
385
+ },
386
+ }),
387
+ { expectText: true },
388
+ );
389
+ },
390
+ };
391
+ }
392
+
393
+ function buildHybridContext7Requester({
394
+ lanePaths,
395
+ fetchImpl,
396
+ directApiKey,
397
+ directApiKeyEnvVar,
398
+ }) {
399
+ const brokerRequester = buildBrokerContext7Requester(fetchImpl, lanePaths);
400
+ let directRequester = null;
401
+ let activeProviderMode = "broker";
402
+ let fallbackWarning = "";
403
+
404
+ const resolveDirectRequester = () => {
405
+ if (directRequester) {
406
+ return directRequester;
407
+ }
408
+ if (!directApiKey) {
409
+ throw new Error(`${directApiKeyEnvVar} is not set; skipping Context7 prefetch.`);
410
+ }
411
+ directRequester = buildDirectContext7Requester(fetchImpl, directApiKey);
412
+ return directRequester;
413
+ };
414
+
415
+ const runWithFallback = async (method, params) => {
416
+ if (activeProviderMode === "direct") {
417
+ return resolveDirectRequester()[method](params);
418
+ }
419
+ try {
420
+ return await brokerRequester[method](params);
421
+ } catch (brokerError) {
422
+ let fallbackRequester = null;
423
+ try {
424
+ fallbackRequester = resolveDirectRequester();
425
+ } catch (fallbackUnavailableError) {
426
+ throw new Error(
427
+ `Context7 broker request failed and direct fallback is unavailable: ${brokerError instanceof Error ? brokerError.message : String(brokerError)}; ${fallbackUnavailableError instanceof Error ? fallbackUnavailableError.message : String(fallbackUnavailableError)}`,
428
+ );
429
+ }
430
+ activeProviderMode = "direct";
431
+ fallbackWarning =
432
+ fallbackWarning ||
433
+ `Context7 broker request failed; fell back to direct auth: ${brokerError instanceof Error ? brokerError.message : String(brokerError)}`;
434
+ try {
435
+ return await fallbackRequester[method](params);
436
+ } catch (fallbackError) {
437
+ throw new Error(
438
+ `Context7 broker request failed and direct fallback also failed: ${brokerError instanceof Error ? brokerError.message : String(brokerError)}; ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`,
439
+ );
440
+ }
441
+ }
442
+ };
443
+
444
+ return {
445
+ requester: {
446
+ search(params) {
447
+ return runWithFallback("search", params);
448
+ },
449
+ context(params) {
450
+ return runWithFallback("context", params);
451
+ },
452
+ },
453
+ providerMode: "broker",
454
+ getProviderMode() {
455
+ return activeProviderMode;
456
+ },
457
+ getWarning() {
458
+ return fallbackWarning;
459
+ },
460
+ };
461
+ }
462
+
463
+ function resolveContext7Requester({
464
+ lanePaths,
465
+ fetchImpl,
466
+ apiKey,
467
+ apiKeyEnvVar = DEFAULT_CONTEXT7_API_KEY_ENV_VAR,
468
+ }) {
469
+ const provider = lanePaths?.externalProviders?.context7 || {};
470
+ const mode = String(provider.mode || "direct").trim().toLowerCase();
471
+ const directApiKey = apiKey || process.env[provider.apiKeyEnvVar || apiKeyEnvVar] || "";
472
+ const direct = () => {
473
+ if (!directApiKey) {
474
+ throw new Error(`${provider.apiKeyEnvVar || apiKeyEnvVar} is not set; skipping Context7 prefetch.`);
475
+ }
476
+ return {
477
+ requester: buildDirectContext7Requester(fetchImpl, directApiKey),
478
+ providerMode: "direct",
479
+ };
480
+ };
481
+ const broker = () => ({
482
+ requester: buildBrokerContext7Requester(fetchImpl, lanePaths),
483
+ providerMode: "broker",
484
+ });
485
+ if (mode === "broker") {
486
+ return broker();
487
+ }
488
+ if (mode === "hybrid") {
489
+ try {
490
+ return buildHybridContext7Requester({
491
+ lanePaths,
492
+ fetchImpl,
493
+ directApiKey,
494
+ directApiKeyEnvVar: provider.apiKeyEnvVar || apiKeyEnvVar,
495
+ });
496
+ } catch {
497
+ return direct();
498
+ }
499
+ }
500
+ return direct();
501
+ }
502
+
503
+ async function resolveLibraryId(requester, library, selection) {
315
504
  if (library.libraryId) {
316
505
  return {
317
506
  libraryId: library.libraryId,
@@ -322,9 +511,7 @@ async function resolveLibraryId(fetchImpl, library, selection, apiKey) {
322
511
  libraryName: library.libraryName,
323
512
  query: selection.query || library.queryHint || library.libraryName,
324
513
  });
325
- const results = await requestContext7(fetchImpl, `${CONTEXT7_SEARCH_URL}?${params.toString()}`, {
326
- apiKey,
327
- });
514
+ const results = await requester.search(params);
328
515
  if (!Array.isArray(results) || results.length === 0) {
329
516
  throw new Error(`Context7 search returned no matches for "${library.libraryName}".`);
330
517
  }
@@ -334,8 +521,8 @@ async function resolveLibraryId(fetchImpl, library, selection, apiKey) {
334
521
  };
335
522
  }
336
523
 
337
- async function fetchLibraryContext(fetchImpl, library, selection, apiKey) {
338
- const resolvedLibrary = await resolveLibraryId(fetchImpl, library, selection, apiKey);
524
+ async function fetchLibraryContext(requester, library, selection) {
525
+ const resolvedLibrary = await resolveLibraryId(requester, library, selection);
339
526
  const query = compactSingleLine(
340
527
  [selection.query, library.queryHint].filter(Boolean).join(". Focus: "),
341
528
  320,
@@ -345,10 +532,7 @@ async function fetchLibraryContext(fetchImpl, library, selection, apiKey) {
345
532
  query,
346
533
  type: "txt",
347
534
  });
348
- const text = await requestContext7(fetchImpl, `${CONTEXT7_CONTEXT_URL}?${params.toString()}`, {
349
- apiKey,
350
- expectText: true,
351
- });
535
+ const text = await requester.context(params);
352
536
  return {
353
537
  libraryId: resolvedLibrary.libraryId,
354
538
  libraryName: resolvedLibrary.libraryName,
@@ -360,6 +544,7 @@ async function fetchLibraryContext(fetchImpl, library, selection, apiKey) {
360
544
  export async function prefetchContext7ForSelection(
361
545
  selection,
362
546
  {
547
+ lanePaths = null,
363
548
  cacheDir,
364
549
  apiKey = process.env.CONTEXT7_API_KEY || "",
365
550
  fetchImpl = globalThis.fetch,
@@ -397,13 +582,18 @@ export async function prefetchContext7ForSelection(
397
582
  };
398
583
  }
399
584
  if (!apiKey) {
400
- return {
401
- mode: "missing-key",
402
- selection,
403
- promptText: "",
404
- snippetHash: "",
405
- warning: "CONTEXT7_API_KEY is not set; skipping Context7 prefetch.",
406
- };
585
+ const providerMode = String(lanePaths?.externalProviders?.context7?.mode || "direct")
586
+ .trim()
587
+ .toLowerCase();
588
+ if (providerMode === "direct") {
589
+ return {
590
+ mode: "missing-key",
591
+ selection,
592
+ promptText: "",
593
+ snippetHash: "",
594
+ warning: "CONTEXT7_API_KEY is not set; skipping Context7 prefetch.",
595
+ };
596
+ }
407
597
  }
408
598
 
409
599
  ensureDirectory(cacheDir);
@@ -427,9 +617,15 @@ export async function prefetchContext7ForSelection(
427
617
  }
428
618
 
429
619
  try {
620
+ const requesterState = resolveContext7Requester({
621
+ lanePaths,
622
+ fetchImpl,
623
+ apiKey,
624
+ });
625
+ const { requester } = requesterState;
430
626
  const results = [];
431
627
  for (const library of selection.libraries) {
432
- const result = await fetchLibraryContext(fetchImpl, library, selection, apiKey);
628
+ const result = await fetchLibraryContext(requester, library, selection);
433
629
  if (result.text) {
434
630
  results.push(result);
435
631
  }
@@ -450,12 +646,18 @@ export async function prefetchContext7ForSelection(
450
646
  promptText,
451
647
  snippetHash,
452
648
  });
649
+ const providerMode =
650
+ typeof requesterState.getProviderMode === "function"
651
+ ? requesterState.getProviderMode()
652
+ : requesterState.providerMode;
653
+ const warning =
654
+ typeof requesterState.getWarning === "function" ? requesterState.getWarning() : "";
453
655
  return {
454
- mode: "fetched",
656
+ mode: providerMode === "broker" ? "fetched-broker" : "fetched",
455
657
  selection,
456
658
  promptText,
457
659
  snippetHash,
458
- warning: "",
660
+ warning,
459
661
  };
460
662
  } catch (error) {
461
663
  return {
@@ -197,6 +197,8 @@ export function buildExecutionPrompt({
197
197
  inboxPath = null,
198
198
  inboxText = "",
199
199
  context7 = null,
200
+ corridorContextPath = null,
201
+ corridorContextText = "",
200
202
  componentPromotions = null,
201
203
  evalTargets = null,
202
204
  benchmarkCatalogPath = null,
@@ -215,6 +217,9 @@ export function buildExecutionPrompt({
215
217
  ? path.relative(REPO_ROOT, sharedSummaryPath)
216
218
  : null;
217
219
  const relativeInboxPath = inboxPath ? path.relative(REPO_ROOT, inboxPath) : null;
220
+ const relativeCorridorContextPath = corridorContextPath
221
+ ? path.relative(REPO_ROOT, corridorContextPath)
222
+ : null;
218
223
  const relativeSignalStatePath = signalStatePath
219
224
  ? path.relative(REPO_ROOT, signalStatePath)
220
225
  : null;
@@ -248,7 +253,7 @@ export function buildExecutionPrompt({
248
253
  ? [
249
254
  `- Because you are Agent ${contQaAgentId}, your cont-QA report must end with exactly one standalone line in the form \`Verdict: PASS\`, \`Verdict: CONCERNS\`, or \`Verdict: BLOCKED\`.`,
250
255
  "- Also emit one matching structured marker in your terminal output: `[wave-verdict] pass`, `[wave-verdict] concerns`, or `[wave-verdict] blocked`.",
251
- "- Emit one final structured gate marker: `[wave-gate] architecture=<pass|concerns|blocked> integration=<pass|concerns|blocked> durability=<pass|concerns|blocked> live=<pass|concerns|blocked> docs=<pass|concerns|blocked> detail=<short-note>`.",
256
+ "- Emit one final structured gate marker: `[wave-gate] architecture=<pass|concerns|blocked|gap> integration=<pass|concerns|blocked|gap> durability=<pass|concerns|blocked|gap> live=<pass|concerns|blocked|gap> docs=<pass|concerns|blocked|gap> detail=<short-note>`.",
252
257
  "- Only use `Verdict: PASS` when the wave is coherent enough to unblock the next wave.",
253
258
  `- Do not declare PASS until the documentation gate is closed: impacted implementation-owned docs must exist, ${sharedPlanDocList} must reflect plan-affecting outcomes, and no unresolved architecture-versus-plans drift remains.`,
254
259
  "- If shared-plan reconciliation is still active inside the wave, require the exact remaining doc delta and an explicit `closed` or `no-change` note from the documentation steward or named owner before finalizing. Do not treat ownership handoff alone as the blocker.",
@@ -537,6 +542,12 @@ export function buildExecutionPrompt({
537
542
  `Agent inbox repo-relative path: ${relativeInboxPath}`,
538
543
  ]
539
544
  : []),
545
+ ...(corridorContextPath
546
+ ? [
547
+ `Corridor context absolute path: ${corridorContextPath}`,
548
+ `Corridor context repo-relative path: ${relativeCorridorContextPath}`,
549
+ ]
550
+ : []),
540
551
  ...(signalStatePath
541
552
  ? [
542
553
  `Signal state absolute path: ${signalStatePath}`,
@@ -552,6 +563,9 @@ export function buildExecutionPrompt({
552
563
  ...(inboxText
553
564
  ? ["Current agent inbox:", "```markdown", inboxText, "```", ""]
554
565
  : []),
566
+ ...(corridorContextText
567
+ ? ["Current Corridor context:", "```text", corridorContextText, "```", ""]
568
+ : []),
555
569
  ...(signalStatePath
556
570
  ? [
557
571
  "Long-running signal loop:",