@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chllming/wave-orchestration",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,22 +2,63 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.9.3",
7
+ "date": "2026-03-30",
8
+ "summary": "Gap-value wave-gate fix and first-time project setup UX improvements.",
9
+ "features": [
10
+ "WAVE_GATE_REGEX now accepts `gap` alongside `pass|concerns|blocked` for all five gate dimensions (architecture, integration, durability, live, docs), so agents reporting a documented gap no longer have their marker rejected entirely.",
11
+ "`validateContQaSummary` now treats `gap` dimension values as a conditional pass (`ok: true`, `statusCode: conditional-pass`) instead of a hard blocker, with detail text listing which dimensions have documented gaps.",
12
+ "The cont-QA coordination prompt now documents `gap` as a valid dimension value alongside `pass|concerns|blocked`.",
13
+ "First-time `wave launch` now auto-triggers `wave project setup` when no project profile exists, matching existing `wave draft` behavior. (Contributed by @justanothernate in #54)",
14
+ "`wave project setup` now shows descriptive help text before each prompt, explains all template and posture options inline, and adds whitespace between question groups for readability. (Contributed by @justanothernate in #54)",
15
+ "`PromptSession` gains a `describe(text)` method for writing contextual help to stderr during interactive setup flows.",
16
+ "`parseArgs` now passes the loaded config object through to `runLauncherCli`, avoiding a redundant `loadWaveConfig()` call.",
17
+ "Release docs, migration guidance, runtime-config and closure references, the manifest, and the tracked install-state fixtures now all point at the `0.9.3` surface.",
18
+ "Planner migration guidance and the `planner-agentic` bundle placeholder remain part of the shipped current-surface docs so adopted repos still have one aligned upgrade target."
19
+ ],
20
+ "manualSteps": [
21
+ "Run `pnpm exec wave doctor` and `pnpm exec wave launch --lane main --dry-run --no-dashboard` after upgrading so the repo validates against the `0.9.3` release surface.",
22
+ "Push the `v0.9.3` tag after the release commit so the GitHub publish workflow can publish the matching npm package version.",
23
+ "If your repo copied starter docs or runbooks, sync `README.md`, `docs/README.md`, `docs/plans/current-state.md`, `docs/plans/migration.md`, `docs/reference/coordination-and-closure.md`, `docs/reference/runtime-config/README.md`, and `docs/guides/recommendations-0.9.3.md` so local guidance matches the packaged release."
24
+ ],
25
+ "breaking": false
26
+ },
27
+ {
28
+ "version": "0.9.2",
29
+ "date": "2026-03-29",
30
+ "summary": "0.9.2 release cut for the documented Corridor and Wave Control surface, plus aligned publish artifacts.",
31
+ "features": [
32
+ "The packaged version now advances to `0.9.2` so the documented Corridor, Wave Control auth, and security surfaces can be tagged and published without colliding with the existing `0.9.1` npm release and git tag.",
33
+ "Release docs, migration guidance, runtime-config docs, coordination docs, Wave Control docs, package publishing docs, tracked install-state fixtures, and the release manifest now all point at the same `0.9.2` surface.",
34
+ "The shipped versioned operating guide is now `docs/guides/recommendations-0.9.2.md`, and starter install seeding plus install regression coverage now use that exact path.",
35
+ "Planner migration guidance and the `planner-agentic` bundle placeholder remain part of the shipped current-surface docs so adopted repos still have one aligned upgrade target."
36
+ ],
37
+ "manualSteps": [
38
+ "Run `pnpm exec wave doctor` and `pnpm exec wave launch --lane main --dry-run --no-dashboard` after upgrading so the repo validates against the `0.9.2` release surface.",
39
+ "Push the `v0.9.2` tag after the release commit so the GitHub publish workflow can publish the matching npm package version.",
40
+ "If your repo copied starter docs or runbooks, sync `README.md`, `docs/README.md`, `docs/plans/current-state.md`, `docs/plans/migration.md`, `docs/reference/coordination-and-closure.md`, `docs/reference/runtime-config/README.md`, `docs/reference/corridor.md`, `docs/reference/wave-control.md`, and `docs/guides/recommendations-0.9.2.md` so local guidance matches the packaged release."
41
+ ],
42
+ "breaking": false
43
+ },
5
44
  {
6
45
  "version": "0.9.1",
7
46
  "date": "2026-03-29",
8
- "summary": "Detached process-backed agent execution, sandbox-safe supervisor hardening, lower memory pressure, and 0.9.1 release-surface alignment.",
47
+ "summary": "Detached process-backed agent execution, authenticated Wave Control, Corridor-backed security context, and 0.9.1 release-surface alignment.",
9
48
  "features": [
10
49
  "Live agent execution now uses detached process runners by default instead of per-agent tmux execution sessions, which reduces tmux churn and lowers memory use during wide orchestration bursts while keeping tmux as an optional dashboard projection layer.",
11
50
  "The sandbox-facing path is now `wave submit`, `wave supervise`, `wave status`, `wave wait`, and `wave attach`, with exact-context lookup, read-side launcher-status reconciliation, progress journaling, degraded-run handling, and log-follow attach behavior suited to LEAPclaw, OpenClaw, Nemoshell, and similar short-lived exec environments.",
12
51
  "Supervisor recovery now relies on run-owned terminal artifacts and finalized progress instead of lane-global completion history, preserving the correct remaining wave range and final active wave during multi-wave reruns and launcher-loss recovery.",
13
52
  "Ordinary runs, closure runs, and resident orchestrator runs now all preserve process-runtime metadata for timeout and cleanup, while process-backed resident orchestrators terminate cleanly and rate-limit retry detection stays scoped to the current attempt output.",
53
+ "Owned `wave-control` deployments now expose the shipped auth surface: Stack-backed browser access, Wave-managed approval states and provider grants, PATs, dedicated service tokens, encrypted per-user credential storage, runtime env leasing, and the separate `services/wave-control-web` frontend.",
54
+ "Corridor is now documented as a first-class security input with `direct`, `broker`, and `hybrid` runtime modes, normalized per-wave security artifacts, owned-path matching rules, and closure gating that can fail before integration on fetch failures or matched blocking findings.",
14
55
  "Planner migration guidance and the `planner-agentic` bundle placeholder remain part of the shipped current-surface docs so adopted repos still have one aligned upgrade target.",
15
- "A dedicated setup guide now ships for sandboxed and containerized operation, including Nemoshell and Docker guidance, while README, migration docs, terminal-surface docs, runtime-config docs, coordination docs, and the renamed recommendations guide `docs/guides/recommendations-0.9.1.md` now describe the same sandbox-safe release surface."
56
+ "A dedicated setup guide now ships for sandboxed and containerized operation, and README, migration docs, terminal-surface docs, runtime-config docs, coordination docs, Wave Control docs, the new Corridor reference, and the renamed recommendations guide `docs/guides/recommendations-0.9.1.md` now describe the same current release surface."
16
57
  ],
17
58
  "manualSteps": [
18
59
  "Run `pnpm exec wave doctor` and `pnpm exec wave launch --lane main --dry-run --no-dashboard` after upgrading so the repo validates against the `0.9.1` release surface.",
19
60
  "If your repo runs Wave inside LEAPclaw, OpenClaw, Nemoshell, Docker, or another short-lived exec sandbox, move long-running orchestration to `wave supervise`, use `wave submit/status/wait/attach` from disposable clients, and set Codex sandbox defaults in `wave.config.json` instead of relying on per-command overrides.",
20
- "If your repo copied starter docs or runbooks, sync `README.md`, `docs/README.md`, `docs/plans/current-state.md`, `docs/plans/migration.md`, `docs/reference/coordination-and-closure.md`, `docs/reference/runtime-config/README.md`, `docs/guides/sandboxed-environments.md`, `docs/guides/terminal-surfaces.md`, and `docs/guides/recommendations-0.9.1.md` so local guidance matches the packaged release."
61
+ "If your repo copied starter docs or runbooks, sync `README.md`, `docs/README.md`, `docs/plans/current-state.md`, `docs/plans/migration.md`, `docs/reference/coordination-and-closure.md`, `docs/reference/runtime-config/README.md`, `docs/reference/corridor.md`, `docs/reference/wave-control.md`, `docs/guides/sandboxed-environments.md`, `docs/guides/terminal-surfaces.md`, and `docs/guides/recommendations-0.9.1.md` so local guidance matches the packaged release."
21
62
  ],
22
63
  "breaking": false
23
64
  },
@@ -1,4 +1,13 @@
1
1
  import path from "node:path";
2
+ import fs from "node:fs";
3
+
4
+ const ALLOWLISTED_ENV_FILE_KEYS = new Set([
5
+ "CONTEXT7_API_KEY",
6
+ "CORRIDOR_API_TOKEN",
7
+ "CORRIDOR_API_KEY",
8
+ "WAVE_API_TOKEN",
9
+ "WAVE_CONTROL_AUTH_TOKEN",
10
+ ]);
2
11
 
3
12
  function stripRepoRootArg(argv) {
4
13
  const normalizedArgs = [];
@@ -22,6 +31,48 @@ function stripRepoRootArg(argv) {
22
31
  return normalizedArgs;
23
32
  }
24
33
 
34
+ function parseEnvLine(line) {
35
+ const trimmed = String(line || "").trim();
36
+ if (!trimmed || trimmed.startsWith("#")) {
37
+ return null;
38
+ }
39
+ const exportPrefix = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trim() : trimmed;
40
+ const equalsIndex = exportPrefix.indexOf("=");
41
+ if (equalsIndex <= 0) {
42
+ return null;
43
+ }
44
+ const key = exportPrefix.slice(0, equalsIndex).trim();
45
+ let value = exportPrefix.slice(equalsIndex + 1).trim();
46
+ if (!ALLOWLISTED_ENV_FILE_KEYS.has(key)) {
47
+ return null;
48
+ }
49
+ if (
50
+ (value.startsWith("\"") && value.endsWith("\"")) ||
51
+ (value.startsWith("'") && value.endsWith("'"))
52
+ ) {
53
+ value = value.slice(1, -1);
54
+ }
55
+ return { key, value };
56
+ }
57
+
58
+ function loadRepoLocalEnv() {
59
+ const repoRoot = path.resolve(process.env.WAVE_REPO_ROOT || process.cwd());
60
+ const envPath = path.join(repoRoot, ".env.local");
61
+ if (!fs.existsSync(envPath)) {
62
+ return;
63
+ }
64
+ const lines = fs.readFileSync(envPath, "utf8").split(/\r?\n/);
65
+ for (const line of lines) {
66
+ const entry = parseEnvLine(line);
67
+ if (!entry || process.env[entry.key]) {
68
+ continue;
69
+ }
70
+ process.env[entry.key] = entry.value;
71
+ }
72
+ }
73
+
25
74
  export function bootstrapWaveArgs(argv) {
26
- return stripRepoRootArg(Array.isArray(argv) ? argv : []);
75
+ const normalizedArgs = stripRepoRootArg(Array.isArray(argv) ? argv : []);
76
+ loadRepoLocalEnv();
77
+ return normalizedArgs;
27
78
  }
@@ -50,7 +50,7 @@ const WAVE_SECURITY_REGEX =
50
50
  const WAVE_DESIGN_REGEX =
51
51
  /^\[wave-design\]\s*state=(ready-for-implementation|needs-clarification|blocked)\s+decisions=(\d+)\s+assumptions=(\d+)\s+open_questions=(\d+)\s*(?:detail=(.*))?$/gim;
52
52
  const WAVE_GATE_REGEX =
53
- /^\[wave-gate\]\s*architecture=(pass|concerns|blocked)\s+integration=(pass|concerns|blocked)\s+durability=(pass|concerns|blocked)\s+live=(pass|concerns|blocked)\s+docs=(pass|concerns|blocked)\s*(?:detail=(.*))?$/gim;
53
+ /^\[wave-gate\]\s*architecture=(pass|concerns|blocked|gap)\s+integration=(pass|concerns|blocked|gap)\s+durability=(pass|concerns|blocked|gap)\s+live=(pass|concerns|blocked|gap)\s+docs=(pass|concerns|blocked|gap)\s*(?:detail=(.*))?$/gim;
54
54
  const WAVE_GAP_REGEX =
55
55
  /^\[wave-gap\]\s*kind=(architecture|integration|durability|ops|docs)\s*(?:detail=(.*))?$/gim;
56
56
  const WAVE_COMPONENT_REGEX =
@@ -1268,17 +1268,34 @@ export function validateContQaSummary(agent, summary, options = {}) {
1268
1268
  detail: summary.verdict.detail || "Verdict read from cont-QA report.",
1269
1269
  };
1270
1270
  }
1271
+ const hardBlockers = [];
1272
+ const documentedGaps = [];
1271
1273
  for (const key of ["architecture", "integration", "durability", "live", "docs"]) {
1272
- if (summary.gate[key] !== "pass") {
1273
- return {
1274
- ok: false,
1275
- statusCode: `gate-${key}-${summary.gate[key]}`,
1276
- detail:
1277
- summary.gate.detail ||
1278
- `Final cont-QA gate did not pass ${key}; got ${summary.gate[key]}.`,
1279
- };
1274
+ if (summary.gate[key] === "gap") {
1275
+ documentedGaps.push(key);
1276
+ } else if (summary.gate[key] !== "pass") {
1277
+ hardBlockers.push(key);
1280
1278
  }
1281
1279
  }
1280
+ if (hardBlockers.length > 0) {
1281
+ const key = hardBlockers[0];
1282
+ return {
1283
+ ok: false,
1284
+ statusCode: `gate-${key}-${summary.gate[key]}`,
1285
+ detail:
1286
+ summary.gate.detail ||
1287
+ `Final cont-QA gate did not pass ${key}; got ${summary.gate[key]}.`,
1288
+ };
1289
+ }
1290
+ if (documentedGaps.length > 0) {
1291
+ return {
1292
+ ok: true,
1293
+ statusCode: "conditional-pass",
1294
+ detail:
1295
+ summary.gate.detail ||
1296
+ `cont-QA gate passed with documented gaps in: ${documentedGaps.join(", ")}.`,
1297
+ };
1298
+ }
1282
1299
  return {
1283
1300
  ok: true,
1284
1301
  statusCode: "pass",
@@ -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(