@chllming/wave-orchestration 0.9.1 → 0.9.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 (43) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/LICENSE.md +21 -0
  3. package/README.md +18 -6
  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/sandboxed-environments.md +2 -2
  12. package/docs/plans/current-state.md +8 -2
  13. package/docs/plans/end-state-architecture.md +1 -1
  14. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  15. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  16. package/docs/plans/migration.md +42 -18
  17. package/docs/reference/cli-reference.md +1 -1
  18. package/docs/reference/coordination-and-closure.md +18 -1
  19. package/docs/reference/corridor.md +225 -0
  20. package/docs/reference/npmjs-token-publishing.md +2 -2
  21. package/docs/reference/package-publishing-flow.md +11 -11
  22. package/docs/reference/runtime-config/README.md +61 -3
  23. package/docs/reference/sample-waves.md +5 -5
  24. package/docs/reference/skills.md +1 -1
  25. package/docs/reference/wave-control.md +358 -27
  26. package/docs/roadmap.md +12 -19
  27. package/package.json +1 -1
  28. package/releases/manifest.json +22 -3
  29. package/scripts/wave-cli-bootstrap.mjs +52 -1
  30. package/scripts/wave-orchestrator/config.mjs +199 -3
  31. package/scripts/wave-orchestrator/context7.mjs +231 -29
  32. package/scripts/wave-orchestrator/coordination.mjs +14 -0
  33. package/scripts/wave-orchestrator/corridor.mjs +363 -0
  34. package/scripts/wave-orchestrator/derived-state-engine.mjs +38 -1
  35. package/scripts/wave-orchestrator/gate-engine.mjs +20 -0
  36. package/scripts/wave-orchestrator/install.mjs +34 -1
  37. package/scripts/wave-orchestrator/launcher-runtime.mjs +111 -7
  38. package/scripts/wave-orchestrator/planner.mjs +1 -0
  39. package/scripts/wave-orchestrator/projection-writer.mjs +23 -0
  40. package/scripts/wave-orchestrator/provider-runtime.mjs +104 -0
  41. package/scripts/wave-orchestrator/shared.mjs +1 -0
  42. package/scripts/wave-orchestrator/traces.mjs +25 -0
  43. package/scripts/wave-orchestrator/wave-control-client.mjs +14 -1
@@ -73,12 +73,22 @@ export const DEFAULT_CODEX_SANDBOX_MODE = "danger-full-access";
73
73
  export const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
74
74
  export const DEFAULT_CLAUDE_COMMAND = "claude";
75
75
  export const DEFAULT_OPENCODE_COMMAND = "opencode";
76
- export const DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR = "WAVE_CONTROL_AUTH_TOKEN";
76
+ export const DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR = "WAVE_API_TOKEN";
77
+ export const LEGACY_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR = "WAVE_CONTROL_AUTH_TOKEN";
77
78
  export const DEFAULT_WAVE_CONTROL_ENDPOINT = "https://wave-control.up.railway.app/api/v1";
78
79
  export const DEFAULT_WAVE_CONTROL_REPORT_MODE = "metadata-only";
79
80
  export const DEFAULT_WAVE_CONTROL_REQUEST_TIMEOUT_MS = 5000;
80
81
  export const DEFAULT_WAVE_CONTROL_FLUSH_BATCH_SIZE = 25;
81
82
  export const DEFAULT_WAVE_CONTROL_MAX_PENDING_EVENTS = 1000;
83
+ export const WAVE_CONTROL_RUNTIME_CREDENTIAL_PROVIDERS = ["anthropic", "openai"];
84
+ export const EXTERNAL_PROVIDER_MODES = ["direct", "broker", "hybrid"];
85
+ export const DEFAULT_CONTEXT7_API_KEY_ENV_VAR = "CONTEXT7_API_KEY";
86
+ export const DEFAULT_CORRIDOR_API_TOKEN_ENV_VAR = "CORRIDOR_API_TOKEN";
87
+ export const DEFAULT_CORRIDOR_API_KEY_FALLBACK_ENV_VAR = "CORRIDOR_API_KEY";
88
+ export const DEFAULT_CORRIDOR_BASE_URL = "https://app.corridor.dev/api";
89
+ export const DEFAULT_CORRIDOR_SEVERITY_THRESHOLD = "critical";
90
+ export const DEFAULT_CORRIDOR_FINDING_STATES = ["open", "potential"];
91
+ export const CORRIDOR_SEVERITY_LEVELS = ["low", "medium", "high", "critical"];
82
92
  export const DEFAULT_WAVE_CONTROL_SELECTED_ARTIFACT_KINDS = [
83
93
  "trace-run-metadata",
84
94
  "trace-quality",
@@ -295,6 +305,103 @@ function normalizeOptionalJsonObject(value, label) {
295
305
  throw new Error(`${label} must be a JSON object`);
296
306
  }
297
307
 
308
+ function normalizeExternalProviderMode(value, label, fallback = "direct") {
309
+ const normalized = String(value || fallback)
310
+ .trim()
311
+ .toLowerCase();
312
+ if (!EXTERNAL_PROVIDER_MODES.includes(normalized)) {
313
+ throw new Error(`${label} must be one of: ${EXTERNAL_PROVIDER_MODES.join(", ")}`);
314
+ }
315
+ return normalized;
316
+ }
317
+
318
+ function normalizeCorridorSeverity(value, label, fallback = DEFAULT_CORRIDOR_SEVERITY_THRESHOLD) {
319
+ const normalized = String(value || fallback)
320
+ .trim()
321
+ .toLowerCase();
322
+ if (!CORRIDOR_SEVERITY_LEVELS.includes(normalized)) {
323
+ throw new Error(`${label} must be one of: ${CORRIDOR_SEVERITY_LEVELS.join(", ")}`);
324
+ }
325
+ return normalized;
326
+ }
327
+
328
+ function normalizeExternalProviders(rawExternalProviders = {}, label = "externalProviders") {
329
+ const externalProviders =
330
+ rawExternalProviders &&
331
+ typeof rawExternalProviders === "object" &&
332
+ !Array.isArray(rawExternalProviders)
333
+ ? rawExternalProviders
334
+ : {};
335
+ const context7 =
336
+ externalProviders.context7 &&
337
+ typeof externalProviders.context7 === "object" &&
338
+ !Array.isArray(externalProviders.context7)
339
+ ? externalProviders.context7
340
+ : {};
341
+ const corridor =
342
+ externalProviders.corridor &&
343
+ typeof externalProviders.corridor === "object" &&
344
+ !Array.isArray(externalProviders.corridor)
345
+ ? externalProviders.corridor
346
+ : {};
347
+ const context7Mode = normalizeExternalProviderMode(
348
+ context7.mode,
349
+ `${label}.context7.mode`,
350
+ "direct",
351
+ );
352
+ const corridorMode = normalizeExternalProviderMode(
353
+ corridor.mode,
354
+ `${label}.corridor.mode`,
355
+ "direct",
356
+ );
357
+ const normalized = {
358
+ context7: {
359
+ mode: context7Mode,
360
+ apiKeyEnvVar:
361
+ normalizeOptionalString(
362
+ context7.apiKeyEnvVar,
363
+ DEFAULT_CONTEXT7_API_KEY_ENV_VAR,
364
+ ) || DEFAULT_CONTEXT7_API_KEY_ENV_VAR,
365
+ },
366
+ corridor: {
367
+ enabled: normalizeOptionalBoolean(corridor.enabled, false),
368
+ mode: corridorMode,
369
+ baseUrl: normalizeOptionalString(corridor.baseUrl, DEFAULT_CORRIDOR_BASE_URL),
370
+ apiTokenEnvVar:
371
+ normalizeOptionalString(
372
+ corridor.apiTokenEnvVar,
373
+ DEFAULT_CORRIDOR_API_TOKEN_ENV_VAR,
374
+ ) || DEFAULT_CORRIDOR_API_TOKEN_ENV_VAR,
375
+ apiKeyFallbackEnvVar:
376
+ normalizeOptionalString(
377
+ corridor.apiKeyFallbackEnvVar,
378
+ DEFAULT_CORRIDOR_API_KEY_FALLBACK_ENV_VAR,
379
+ ) || DEFAULT_CORRIDOR_API_KEY_FALLBACK_ENV_VAR,
380
+ teamId: normalizeOptionalString(corridor.teamId, null),
381
+ projectId: normalizeOptionalString(corridor.projectId, null),
382
+ severityThreshold: normalizeCorridorSeverity(
383
+ corridor.severityThreshold,
384
+ `${label}.corridor.severityThreshold`,
385
+ ),
386
+ findingStates: normalizeOptionalStringArray(
387
+ corridor.findingStates,
388
+ DEFAULT_CORRIDOR_FINDING_STATES,
389
+ ),
390
+ requiredAtClosure: normalizeOptionalBoolean(corridor.requiredAtClosure, true),
391
+ },
392
+ };
393
+ if (
394
+ normalized.corridor.enabled &&
395
+ normalized.corridor.mode === "direct" &&
396
+ (!normalized.corridor.teamId || !normalized.corridor.projectId)
397
+ ) {
398
+ throw new Error(
399
+ `${label}.corridor.teamId and ${label}.corridor.projectId are required when corridor is enabled in direct mode`,
400
+ );
401
+ }
402
+ return normalized;
403
+ }
404
+
298
405
  function normalizeExecutorBudget(rawBudget = {}, label = "budget") {
299
406
  const budget =
300
407
  rawBudget && typeof rawBudget === "object" && !Array.isArray(rawBudget) ? rawBudget : {};
@@ -578,6 +685,31 @@ function normalizeWaveControl(rawWaveControl = {}, label = "waveControl") {
578
685
  rawWaveControl && typeof rawWaveControl === "object" && !Array.isArray(rawWaveControl)
579
686
  ? rawWaveControl
580
687
  : {};
688
+ const credentials = Array.isArray(waveControl.credentials) ? waveControl.credentials : [];
689
+ const normalizedCredentials = [];
690
+ const seenCredentialEnvVars = new Set();
691
+ for (const [index, entry] of credentials.entries()) {
692
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
693
+ throw new Error(`${label}.credentials[${index}] must be an object with id and envVar.`);
694
+ }
695
+ const id = String(entry.id || "").trim().toLowerCase();
696
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) {
697
+ throw new Error(
698
+ `${label}.credentials[${index}].id must match /^[a-z0-9][a-z0-9._-]*$/.`,
699
+ );
700
+ }
701
+ const envVar = String(entry.envVar || "").trim().toUpperCase();
702
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(envVar)) {
703
+ throw new Error(
704
+ `${label}.credentials[${index}].envVar must match /^[A-Z_][A-Z0-9_]*$/.`,
705
+ );
706
+ }
707
+ if (seenCredentialEnvVars.has(envVar)) {
708
+ throw new Error(`${label}.credentials contains duplicate envVar mappings for ${envVar}.`);
709
+ }
710
+ seenCredentialEnvVars.add(envVar);
711
+ normalizedCredentials.push({ id, envVar });
712
+ }
581
713
  const reportMode = normalizeWaveControlReportMode(
582
714
  waveControl.reportMode,
583
715
  `${label}.reportMode`,
@@ -591,8 +723,36 @@ function normalizeWaveControl(rawWaveControl = {}, label = "waveControl") {
591
723
  workspaceId: normalizeOptionalString(waveControl.workspaceId, null),
592
724
  projectId: normalizeOptionalString(waveControl.projectId, null),
593
725
  authTokenEnvVar:
594
- normalizeOptionalString(waveControl.authTokenEnvVar, DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR) ||
595
- DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR,
726
+ normalizeOptionalString(
727
+ waveControl.authTokenEnvVar,
728
+ DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR,
729
+ ) || DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR,
730
+ authTokenEnvVars: Array.from(
731
+ new Set(
732
+ normalizeOptionalStringArray(
733
+ waveControl.authTokenEnvVars,
734
+ [
735
+ normalizeOptionalString(
736
+ waveControl.authTokenEnvVar,
737
+ DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR,
738
+ ) || DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR,
739
+ LEGACY_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR,
740
+ ],
741
+ ),
742
+ ),
743
+ ),
744
+ credentialProviders: normalizeOptionalStringArray(waveControl.credentialProviders, []).map(
745
+ (providerId, index) => {
746
+ const normalized = String(providerId || "").trim().toLowerCase();
747
+ if (!WAVE_CONTROL_RUNTIME_CREDENTIAL_PROVIDERS.includes(normalized)) {
748
+ throw new Error(
749
+ `${label}.credentialProviders[${index}] must be one of: ${WAVE_CONTROL_RUNTIME_CREDENTIAL_PROVIDERS.join(", ")}`,
750
+ );
751
+ }
752
+ return normalized;
753
+ },
754
+ ),
755
+ credentials: normalizedCredentials,
596
756
  reportMode,
597
757
  uploadArtifactKinds: normalizeOptionalStringArray(
598
758
  waveControl.uploadArtifactKinds,
@@ -1109,6 +1269,7 @@ export function loadWaveConfig(configPath = DEFAULT_WAVE_CONFIG_PATH) {
1109
1269
  capabilityRouting: rawProject.capabilityRouting || {},
1110
1270
  runtimePolicy: rawProject.runtimePolicy || {},
1111
1271
  waveControl: rawProject.waveControl || {},
1272
+ externalProviders: rawProject.externalProviders || {},
1112
1273
  lanes: projectLanes,
1113
1274
  explicit: Boolean(rawProjects),
1114
1275
  },
@@ -1130,6 +1291,10 @@ export function loadWaveConfig(configPath = DEFAULT_WAVE_CONFIG_PATH) {
1130
1291
  capabilityRouting: normalizeCapabilityRouting(rawConfig.capabilityRouting),
1131
1292
  runtimePolicy: normalizeRuntimePolicy(rawConfig.runtimePolicy),
1132
1293
  waveControl: normalizeWaveControl(rawConfig.waveControl, "waveControl"),
1294
+ externalProviders: normalizeExternalProviders(
1295
+ rawConfig.externalProviders,
1296
+ "externalProviders",
1297
+ ),
1133
1298
  sharedPlanDocs,
1134
1299
  lanes: legacyLanes,
1135
1300
  projects,
@@ -1182,6 +1347,21 @@ export function resolveProjectProfile(config, projectInput = config.defaultProje
1182
1347
  ...config.runtimePolicy,
1183
1348
  ...(projectConfig.runtimePolicy || {}),
1184
1349
  }),
1350
+ externalProviders: normalizeExternalProviders(
1351
+ {
1352
+ ...config.externalProviders,
1353
+ ...(projectConfig.externalProviders || {}),
1354
+ context7: {
1355
+ ...(config.externalProviders?.context7 || {}),
1356
+ ...(projectConfig.externalProviders?.context7 || {}),
1357
+ },
1358
+ corridor: {
1359
+ ...(config.externalProviders?.corridor || {}),
1360
+ ...(projectConfig.externalProviders?.corridor || {}),
1361
+ },
1362
+ },
1363
+ `projects.${projectId}.externalProviders`,
1364
+ ),
1185
1365
  waveControl: normalizeWaveControl(
1186
1366
  {
1187
1367
  ...config.waveControl,
@@ -1256,6 +1436,21 @@ export function resolveLaneProfile(config, laneInput = config.defaultLane, proje
1256
1436
  },
1257
1437
  `${lane}.waveControl`,
1258
1438
  );
1439
+ const externalProviders = normalizeExternalProviders(
1440
+ {
1441
+ ...projectProfile.externalProviders,
1442
+ ...(laneConfig.externalProviders || {}),
1443
+ context7: {
1444
+ ...(projectProfile.externalProviders?.context7 || {}),
1445
+ ...(laneConfig.externalProviders?.context7 || {}),
1446
+ },
1447
+ corridor: {
1448
+ ...(projectProfile.externalProviders?.corridor || {}),
1449
+ ...(laneConfig.externalProviders?.corridor || {}),
1450
+ },
1451
+ },
1452
+ `${lane}.externalProviders`,
1453
+ );
1259
1454
  return {
1260
1455
  projectId: projectProfile.projectId,
1261
1456
  projectName: projectProfile.projectName,
@@ -1276,6 +1471,7 @@ export function resolveLaneProfile(config, laneInput = config.defaultLane, proje
1276
1471
  skills,
1277
1472
  capabilityRouting,
1278
1473
  runtimePolicy,
1474
+ externalProviders,
1279
1475
  waveControl,
1280
1476
  paths: {
1281
1477
  terminalsPath: normalizeRepoRelativePath(
@@ -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;
@@ -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:",