@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.
- package/CHANGELOG.md +52 -1
- package/LICENSE.md +21 -0
- package/README.md +20 -9
- package/docs/README.md +8 -4
- package/docs/agents/wave-security-role.md +1 -0
- package/docs/architecture/README.md +1 -1
- package/docs/concepts/operating-modes.md +1 -1
- package/docs/guides/author-and-run-waves.md +1 -1
- package/docs/guides/planner.md +2 -2
- package/docs/guides/{recommendations-0.9.1.md → recommendations-0.9.2.md} +7 -7
- package/docs/guides/recommendations-0.9.3.md +137 -0
- package/docs/guides/sandboxed-environments.md +2 -2
- package/docs/plans/current-state.md +8 -2
- package/docs/plans/end-state-architecture.md +1 -1
- package/docs/plans/examples/wave-example-design-handoff.md +1 -1
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +65 -67
- package/docs/reference/cli-reference.md +1 -1
- package/docs/reference/coordination-and-closure.md +20 -3
- package/docs/reference/corridor.md +225 -0
- package/docs/reference/npmjs-token-publishing.md +2 -2
- package/docs/reference/package-publishing-flow.md +11 -11
- package/docs/reference/runtime-config/README.md +61 -3
- package/docs/reference/sample-waves.md +5 -5
- package/docs/reference/skills.md +1 -1
- package/docs/reference/wave-control.md +358 -27
- package/docs/roadmap.md +12 -19
- package/package.json +1 -1
- package/releases/manifest.json +44 -3
- package/scripts/wave-cli-bootstrap.mjs +52 -1
- package/scripts/wave-orchestrator/agent-state.mjs +26 -9
- package/scripts/wave-orchestrator/config.mjs +199 -3
- package/scripts/wave-orchestrator/context7.mjs +231 -29
- package/scripts/wave-orchestrator/coordination.mjs +15 -1
- package/scripts/wave-orchestrator/corridor.mjs +363 -0
- package/scripts/wave-orchestrator/derived-state-engine.mjs +38 -1
- package/scripts/wave-orchestrator/gate-engine.mjs +20 -0
- package/scripts/wave-orchestrator/install.mjs +34 -1
- package/scripts/wave-orchestrator/launcher-runtime.mjs +111 -7
- package/scripts/wave-orchestrator/launcher.mjs +21 -3
- package/scripts/wave-orchestrator/planner.mjs +30 -0
- package/scripts/wave-orchestrator/projection-writer.mjs +23 -0
- package/scripts/wave-orchestrator/provider-runtime.mjs +104 -0
- package/scripts/wave-orchestrator/shared.mjs +1 -0
- package/scripts/wave-orchestrator/traces.mjs +25 -0
- package/scripts/wave-orchestrator/wave-control-client.mjs +14 -1
package/package.json
CHANGED
package/releases/manifest.json
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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]
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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 = "
|
|
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(
|
|
595
|
-
|
|
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(
|