@chllming/wave-orchestration 0.9.0 → 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.
- package/CHANGELOG.md +57 -0
- package/LICENSE.md +21 -0
- package/README.md +133 -20
- package/docs/README.md +12 -4
- package/docs/agents/wave-security-role.md +1 -0
- package/docs/architecture/README.md +1498 -0
- package/docs/concepts/operating-modes.md +2 -2
- package/docs/guides/author-and-run-waves.md +14 -4
- package/docs/guides/planner.md +2 -2
- package/docs/guides/{recommendations-0.9.0.md → recommendations-0.9.2.md} +8 -7
- package/docs/guides/sandboxed-environments.md +158 -0
- package/docs/guides/terminal-surfaces.md +14 -12
- package/docs/plans/current-state.md +11 -3
- package/docs/plans/end-state-architecture.md +3 -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 +70 -19
- package/docs/plans/sandbox-end-state-architecture.md +153 -0
- package/docs/reference/cli-reference.md +71 -7
- package/docs/reference/coordination-and-closure.md +18 -1
- package/docs/reference/corridor.md +225 -0
- package/docs/reference/github-packages-setup.md +1 -1
- package/docs/reference/migration-0.2-to-0.5.md +9 -7
- package/docs/reference/npmjs-token-publishing.md +53 -0
- package/docs/reference/npmjs-trusted-publishing.md +4 -50
- package/docs/reference/package-publishing-flow.md +272 -0
- 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 +39 -204
- package/package.json +1 -1
- package/releases/manifest.json +38 -0
- package/scripts/wave-cli-bootstrap.mjs +52 -1
- package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
- package/scripts/wave-orchestrator/agent-state.mjs +0 -1
- package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
- package/scripts/wave-orchestrator/autonomous.mjs +47 -14
- package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
- package/scripts/wave-orchestrator/config.mjs +199 -3
- package/scripts/wave-orchestrator/context7.mjs +231 -29
- package/scripts/wave-orchestrator/control-cli.mjs +42 -5
- package/scripts/wave-orchestrator/coordination.mjs +14 -0
- package/scripts/wave-orchestrator/corridor.mjs +363 -0
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +115 -43
- package/scripts/wave-orchestrator/derived-state-engine.mjs +44 -4
- package/scripts/wave-orchestrator/gate-engine.mjs +126 -38
- package/scripts/wave-orchestrator/install.mjs +46 -0
- package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +290 -75
- package/scripts/wave-orchestrator/launcher.mjs +201 -53
- package/scripts/wave-orchestrator/ledger.mjs +7 -2
- package/scripts/wave-orchestrator/planner.mjs +1 -0
- package/scripts/wave-orchestrator/projection-writer.mjs +36 -1
- package/scripts/wave-orchestrator/provider-runtime.mjs +104 -0
- package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
- package/scripts/wave-orchestrator/retry-control.mjs +3 -3
- package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
- package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
- package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
- package/scripts/wave-orchestrator/shared.mjs +1 -0
- package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
- package/scripts/wave-orchestrator/terminals.mjs +12 -32
- package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
- package/scripts/wave-orchestrator/traces.mjs +25 -0
- package/scripts/wave-orchestrator/wave-control-client.mjs +14 -1
- package/scripts/wave-orchestrator/wave-files.mjs +38 -5
- package/scripts/wave.mjs +13 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { appendCoordinationRecord } from "./coordination-store.mjs";
|
|
2
3
|
import {
|
|
3
4
|
parseStructuredSignalsFromLog,
|
|
4
5
|
refreshWaveDashboardAgentStates,
|
|
@@ -14,8 +15,13 @@ import {
|
|
|
14
15
|
readWaveIntegrationBarrier as readWaveIntegrationBarrierDefault,
|
|
15
16
|
readWaveSecurityGate as readWaveSecurityGateDefault,
|
|
16
17
|
} from "./gate-engine.mjs";
|
|
18
|
+
import { applyLaunchResultToRun } from "./launcher-runtime.mjs";
|
|
17
19
|
import { REPO_ROOT, toIsoTimestamp } from "./shared.mjs";
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isSecurityReviewAgentForLane,
|
|
22
|
+
resolveAgentClosureRoleKeys,
|
|
23
|
+
resolveWaveRoleBindings,
|
|
24
|
+
} from "./role-helpers.mjs";
|
|
19
25
|
import { summarizeResolvedSkills } from "./skills.mjs";
|
|
20
26
|
|
|
21
27
|
function failureResultFromGate(gate, fallbackLogPath) {
|
|
@@ -57,6 +63,64 @@ function recordClosureGateFailure({
|
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
function isForwardableClosureGap(gate) {
|
|
67
|
+
return gate?.statusCode === "wave-proof-gap";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function forwardedClosureGapRecord({
|
|
71
|
+
stage,
|
|
72
|
+
wave,
|
|
73
|
+
lanePaths,
|
|
74
|
+
gate,
|
|
75
|
+
attempt,
|
|
76
|
+
targetAgentIds = [],
|
|
77
|
+
}) {
|
|
78
|
+
return {
|
|
79
|
+
id: `wave-${wave.wave}-closure-gap-${stage.key}-${gate.agentId}-attempt-${attempt || 1}`,
|
|
80
|
+
kind: "blocker",
|
|
81
|
+
lane: lanePaths.lane,
|
|
82
|
+
wave: wave.wave,
|
|
83
|
+
agentId: gate.agentId,
|
|
84
|
+
status: "open",
|
|
85
|
+
priority: "high",
|
|
86
|
+
blocking: true,
|
|
87
|
+
blockerSeverity: "closure-critical",
|
|
88
|
+
summary: `${stage.label} reported a proof gap and was forwarded to later closure stages.`,
|
|
89
|
+
detail: gate.detail,
|
|
90
|
+
artifactRefs: gate.logPath ? [gate.logPath] : [],
|
|
91
|
+
targets: targetAgentIds.map((agentId) => `agent:${agentId}`),
|
|
92
|
+
attempt: attempt || 1,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stageRequiresRun(stage, wave, lanePaths) {
|
|
97
|
+
switch (stage.key) {
|
|
98
|
+
case "integration":
|
|
99
|
+
case "documentation":
|
|
100
|
+
case "cont-qa":
|
|
101
|
+
return true;
|
|
102
|
+
case "cont-eval":
|
|
103
|
+
return Array.isArray(wave?.agents) && wave.agents.some((agent) => agent?.agentId === stage.agentId);
|
|
104
|
+
case "security-review":
|
|
105
|
+
return (
|
|
106
|
+
Array.isArray(wave?.agents) &&
|
|
107
|
+
wave.agents.some((agent) => isSecurityReviewAgentForLane(agent, lanePaths))
|
|
108
|
+
);
|
|
109
|
+
default:
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function missingClosureRunGate(stage) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
agentId: stage.agentId,
|
|
118
|
+
statusCode: "missing-closure-run",
|
|
119
|
+
detail: `${stage.label} is required for this wave but no matching closure run was provided.`,
|
|
120
|
+
logPath: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
60
124
|
export async function runClosureSweepPhase({
|
|
61
125
|
lanePaths,
|
|
62
126
|
wave,
|
|
@@ -113,10 +177,24 @@ export async function runClosureSweepPhase({
|
|
|
113
177
|
? readWaveContQaGateFn
|
|
114
178
|
: readWaveContQaGateDefault;
|
|
115
179
|
const stagedRuns = planClosureStages({ lanePaths, wave, closureRuns });
|
|
180
|
+
const forwardedFailures = [];
|
|
116
181
|
const { contQaAgentId, contEvalAgentId, integrationAgentId, documentationAgentId } =
|
|
117
182
|
resolveWaveRoleBindings(wave, lanePaths);
|
|
118
|
-
for (const stage of stagedRuns) {
|
|
183
|
+
for (const [stageIndex, stage] of stagedRuns.entries()) {
|
|
119
184
|
if (stage.runs.length === 0) {
|
|
185
|
+
if (stageRequiresRun(stage, wave, lanePaths)) {
|
|
186
|
+
const gate = missingClosureRunGate(stage);
|
|
187
|
+
recordClosureGateFailure({
|
|
188
|
+
wave,
|
|
189
|
+
lanePaths,
|
|
190
|
+
gate,
|
|
191
|
+
label: stage.label,
|
|
192
|
+
recordCombinedEvent,
|
|
193
|
+
appendCoordination,
|
|
194
|
+
actionRequested: stage.actionRequested,
|
|
195
|
+
});
|
|
196
|
+
return failureResultFromGate(gate, null);
|
|
197
|
+
}
|
|
120
198
|
continue;
|
|
121
199
|
}
|
|
122
200
|
for (const runInfo of stage.runs) {
|
|
@@ -138,6 +216,7 @@ export async function runClosureSweepPhase({
|
|
|
138
216
|
promptPath: runInfo.promptPath,
|
|
139
217
|
logPath: runInfo.logPath,
|
|
140
218
|
statusPath: runInfo.statusPath,
|
|
219
|
+
runtimePath: runInfo.runtimePath,
|
|
141
220
|
messageBoardPath: runInfo.messageBoardPath,
|
|
142
221
|
messageBoardSnapshot: runInfo.messageBoardSnapshot || "",
|
|
143
222
|
sharedSummaryPath: runInfo.sharedSummaryPath,
|
|
@@ -157,19 +236,18 @@ export async function runClosureSweepPhase({
|
|
|
157
236
|
attempt: dashboardState?.attempt || 1,
|
|
158
237
|
},
|
|
159
238
|
});
|
|
160
|
-
runInfo
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
launchResult?.skills || summarizeResolvedSkills(runInfo.agent.skillsResolved);
|
|
239
|
+
applyLaunchResultToRun(runInfo, launchResult, {
|
|
240
|
+
attempt: dashboardState?.attempt || null,
|
|
241
|
+
fallbackExecutorId: runInfo.agent.executorResolved?.id || null,
|
|
242
|
+
fallbackSkills: summarizeResolvedSkills(runInfo.agent.skillsResolved),
|
|
243
|
+
});
|
|
166
244
|
setWaveDashboardAgent(dashboardState, runInfo.agent.agentId, {
|
|
167
245
|
state: "running",
|
|
168
246
|
detail: `Closure sweep launched${launchResult?.context7?.mode ? ` (${launchResult.context7.mode})` : ""}`,
|
|
169
247
|
});
|
|
170
248
|
recordCombinedEvent({
|
|
171
249
|
agentId: runInfo.agent.agentId,
|
|
172
|
-
message: `Closure sweep launched
|
|
250
|
+
message: `Closure sweep launched via ${launchResult?.sessionBackend || "process"} backend`,
|
|
173
251
|
});
|
|
174
252
|
flushDashboards();
|
|
175
253
|
const result = await waitForWaveCompletionFn(
|
|
@@ -228,6 +306,43 @@ export async function runClosureSweepPhase({
|
|
|
228
306
|
contQaAgentId,
|
|
229
307
|
});
|
|
230
308
|
if (!gate.ok) {
|
|
309
|
+
if (isForwardableClosureGap(gate)) {
|
|
310
|
+
const targetAgentIds = stagedRuns
|
|
311
|
+
.slice(stageIndex + 1)
|
|
312
|
+
.flatMap((candidate) => candidate.runs.map((run) => run.agent.agentId))
|
|
313
|
+
.filter(Boolean);
|
|
314
|
+
forwardedFailures.push({
|
|
315
|
+
agentId: gate.agentId,
|
|
316
|
+
statusCode: gate.statusCode,
|
|
317
|
+
logPath: gate.logPath || (stage.runs[0]?.logPath ? path.relative(REPO_ROOT, stage.runs[0].logPath) : null),
|
|
318
|
+
detail: gate.detail,
|
|
319
|
+
});
|
|
320
|
+
appendCoordinationRecord(
|
|
321
|
+
coordinationLogPath,
|
|
322
|
+
forwardedClosureGapRecord({
|
|
323
|
+
stage,
|
|
324
|
+
wave,
|
|
325
|
+
lanePaths,
|
|
326
|
+
gate,
|
|
327
|
+
attempt: dashboardState?.attempt || 1,
|
|
328
|
+
targetAgentIds,
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
recordCombinedEvent({
|
|
332
|
+
level: "warn",
|
|
333
|
+
agentId: gate.agentId,
|
|
334
|
+
message: `${stage.label} reported a proof gap; continuing later closure stages with the gap as input.`,
|
|
335
|
+
});
|
|
336
|
+
appendCoordination({
|
|
337
|
+
event: "closure_gap_forwarded",
|
|
338
|
+
waves: [wave.wave],
|
|
339
|
+
status: "blocked",
|
|
340
|
+
details: `agent=${gate.agentId}; reason=${gate.statusCode}; ${gate.detail}`,
|
|
341
|
+
actionRequested: `Lane ${lanePaths.lane} owners should resolve the forwarded closure proof gap after downstream closure evidence is collected.`,
|
|
342
|
+
});
|
|
343
|
+
refreshDerivedState?.(dashboardState?.attempt || 0);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
231
346
|
recordClosureGateFailure({
|
|
232
347
|
wave,
|
|
233
348
|
lanePaths,
|
|
@@ -243,18 +358,21 @@ export async function runClosureSweepPhase({
|
|
|
243
358
|
);
|
|
244
359
|
}
|
|
245
360
|
}
|
|
246
|
-
return { failures:
|
|
361
|
+
return { failures: forwardedFailures, timedOut: false };
|
|
247
362
|
}
|
|
248
363
|
|
|
249
364
|
export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
365
|
+
const roleBindings = resolveWaveRoleBindings(wave, lanePaths);
|
|
250
366
|
const { contQaAgentId, contEvalAgentId, integrationAgentId, documentationAgentId } =
|
|
251
|
-
|
|
367
|
+
roleBindings;
|
|
368
|
+
const runHasRole = (run, roleKey) =>
|
|
369
|
+
resolveAgentClosureRoleKeys(run.agent, roleBindings, lanePaths).includes(roleKey);
|
|
252
370
|
return [
|
|
253
371
|
{
|
|
254
372
|
key: "cont-eval",
|
|
255
373
|
agentId: contEvalAgentId,
|
|
256
374
|
label: "cont-EVAL gate",
|
|
257
|
-
runs: closureRuns.filter((run) => run
|
|
375
|
+
runs: closureRuns.filter((run) => runHasRole(run, "cont-eval")),
|
|
258
376
|
actionRequested:
|
|
259
377
|
`Lane ${lanePaths.lane} owners should resolve cont-EVAL tuning gaps before integration closure.`,
|
|
260
378
|
},
|
|
@@ -262,7 +380,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
262
380
|
key: "security-review",
|
|
263
381
|
agentId: "security",
|
|
264
382
|
label: "Security review",
|
|
265
|
-
runs: closureRuns.filter((run) =>
|
|
383
|
+
runs: closureRuns.filter((run) => runHasRole(run, "security-review")),
|
|
266
384
|
actionRequested:
|
|
267
385
|
`Lane ${lanePaths.lane} owners should resolve blocked security findings or missing approvals before integration closure.`,
|
|
268
386
|
},
|
|
@@ -270,7 +388,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
270
388
|
key: "integration",
|
|
271
389
|
agentId: integrationAgentId,
|
|
272
390
|
label: "Integration gate",
|
|
273
|
-
runs: closureRuns.filter((run) => run
|
|
391
|
+
runs: closureRuns.filter((run) => runHasRole(run, "integration")),
|
|
274
392
|
actionRequested:
|
|
275
393
|
`Lane ${lanePaths.lane} owners should resolve integration contradictions or blockers before documentation and cont-QA closure.`,
|
|
276
394
|
},
|
|
@@ -278,7 +396,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
278
396
|
key: "documentation",
|
|
279
397
|
agentId: documentationAgentId,
|
|
280
398
|
label: "Documentation closure",
|
|
281
|
-
runs: closureRuns.filter((run) => run
|
|
399
|
+
runs: closureRuns.filter((run) => runHasRole(run, "documentation")),
|
|
282
400
|
actionRequested:
|
|
283
401
|
`Lane ${lanePaths.lane} owners should resolve the shared-plan or component-matrix closure state before cont-QA progression.`,
|
|
284
402
|
},
|
|
@@ -286,7 +404,7 @@ export function planClosureStages({ lanePaths, wave, closureRuns }) {
|
|
|
286
404
|
key: "cont-qa",
|
|
287
405
|
agentId: contQaAgentId,
|
|
288
406
|
label: "cont-QA gate",
|
|
289
|
-
runs: closureRuns.filter((run) => run
|
|
407
|
+
runs: closureRuns.filter((run) => runHasRole(run, "cont-qa")),
|
|
290
408
|
actionRequested:
|
|
291
409
|
`Lane ${lanePaths.lane} owners should resolve the cont-QA gate before wave progression.`,
|
|
292
410
|
},
|
|
@@ -320,7 +438,10 @@ function evaluateClosureStage({
|
|
|
320
438
|
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
321
439
|
});
|
|
322
440
|
case "security-review":
|
|
323
|
-
return readWaveSecurityGateFn(wave, closureRuns, {
|
|
441
|
+
return readWaveSecurityGateFn(wave, closureRuns, {
|
|
442
|
+
mode: "live",
|
|
443
|
+
securityRolePromptPath: lanePaths?.securityRolePromptPath,
|
|
444
|
+
});
|
|
324
445
|
case "integration":
|
|
325
446
|
return readWaveIntegrationBarrierFn(
|
|
326
447
|
wave,
|
|
@@ -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(
|