@chllming/wave-orchestration 0.5.1

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 (68) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +549 -0
  3. package/docs/agents/wave-deploy-verifier-role.md +34 -0
  4. package/docs/agents/wave-documentation-role.md +30 -0
  5. package/docs/agents/wave-evaluator-role.md +43 -0
  6. package/docs/agents/wave-infra-role.md +34 -0
  7. package/docs/agents/wave-integration-role.md +32 -0
  8. package/docs/agents/wave-launcher-role.md +37 -0
  9. package/docs/context7/bundles.json +91 -0
  10. package/docs/plans/component-cutover-matrix.json +112 -0
  11. package/docs/plans/component-cutover-matrix.md +49 -0
  12. package/docs/plans/context7-wave-orchestrator.md +130 -0
  13. package/docs/plans/current-state.md +44 -0
  14. package/docs/plans/master-plan.md +16 -0
  15. package/docs/plans/migration.md +23 -0
  16. package/docs/plans/wave-orchestrator.md +254 -0
  17. package/docs/plans/waves/wave-0.md +165 -0
  18. package/docs/reference/github-packages-setup.md +52 -0
  19. package/docs/reference/migration-0.2-to-0.5.md +622 -0
  20. package/docs/reference/npmjs-trusted-publishing.md +55 -0
  21. package/docs/reference/repository-guidance.md +18 -0
  22. package/docs/reference/runtime-config/README.md +85 -0
  23. package/docs/reference/runtime-config/claude.md +105 -0
  24. package/docs/reference/runtime-config/codex.md +81 -0
  25. package/docs/reference/runtime-config/opencode.md +93 -0
  26. package/docs/research/agent-context-sources.md +57 -0
  27. package/docs/roadmap.md +626 -0
  28. package/package.json +53 -0
  29. package/releases/manifest.json +101 -0
  30. package/scripts/context7-api-check.sh +21 -0
  31. package/scripts/context7-export-env.sh +52 -0
  32. package/scripts/research/agent-context-archive.mjs +472 -0
  33. package/scripts/research/generate-agent-context-indexes.mjs +85 -0
  34. package/scripts/research/import-agent-context-archive.mjs +793 -0
  35. package/scripts/research/manifests/harness-and-blackboard-2026-03-21.mjs +201 -0
  36. package/scripts/wave-autonomous.mjs +13 -0
  37. package/scripts/wave-cli-bootstrap.mjs +27 -0
  38. package/scripts/wave-dashboard.mjs +11 -0
  39. package/scripts/wave-human-feedback.mjs +11 -0
  40. package/scripts/wave-launcher.mjs +11 -0
  41. package/scripts/wave-local-executor.mjs +13 -0
  42. package/scripts/wave-orchestrator/agent-state.mjs +416 -0
  43. package/scripts/wave-orchestrator/autonomous.mjs +367 -0
  44. package/scripts/wave-orchestrator/clarification-triage.mjs +605 -0
  45. package/scripts/wave-orchestrator/config.mjs +848 -0
  46. package/scripts/wave-orchestrator/context7.mjs +464 -0
  47. package/scripts/wave-orchestrator/coord-cli.mjs +286 -0
  48. package/scripts/wave-orchestrator/coordination-store.mjs +987 -0
  49. package/scripts/wave-orchestrator/coordination.mjs +768 -0
  50. package/scripts/wave-orchestrator/dashboard-renderer.mjs +254 -0
  51. package/scripts/wave-orchestrator/dashboard-state.mjs +473 -0
  52. package/scripts/wave-orchestrator/dep-cli.mjs +219 -0
  53. package/scripts/wave-orchestrator/docs-queue.mjs +75 -0
  54. package/scripts/wave-orchestrator/executors.mjs +385 -0
  55. package/scripts/wave-orchestrator/feedback.mjs +372 -0
  56. package/scripts/wave-orchestrator/install.mjs +540 -0
  57. package/scripts/wave-orchestrator/launcher.mjs +3879 -0
  58. package/scripts/wave-orchestrator/ledger.mjs +332 -0
  59. package/scripts/wave-orchestrator/local-executor.mjs +263 -0
  60. package/scripts/wave-orchestrator/replay.mjs +246 -0
  61. package/scripts/wave-orchestrator/roots.mjs +10 -0
  62. package/scripts/wave-orchestrator/routing-state.mjs +542 -0
  63. package/scripts/wave-orchestrator/shared.mjs +405 -0
  64. package/scripts/wave-orchestrator/terminals.mjs +209 -0
  65. package/scripts/wave-orchestrator/traces.mjs +1094 -0
  66. package/scripts/wave-orchestrator/wave-files.mjs +1923 -0
  67. package/scripts/wave.mjs +103 -0
  68. package/wave.config.json +115 -0
@@ -0,0 +1,848 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { WORKSPACE_ROOT } from "./roots.mjs";
4
+
5
+ const REPO_ROOT = WORKSPACE_ROOT;
6
+
7
+ export const DEFAULT_WAVE_CONFIG_PATH = path.join(REPO_ROOT, "wave.config.json");
8
+ export const DEFAULT_WAVE_LANE = "main";
9
+ export const DEFAULT_EVALUATOR_AGENT_ID = "A0";
10
+ export const DEFAULT_INTEGRATION_AGENT_ID = "A8";
11
+ export const DEFAULT_DOCUMENTATION_AGENT_ID = "A9";
12
+ export const DEFAULT_ROLE_PROMPT_DIR = "docs/agents";
13
+ export const DEFAULT_EVALUATOR_ROLE_PROMPT_PATH = "docs/agents/wave-evaluator-role.md";
14
+ export const DEFAULT_INTEGRATION_ROLE_PROMPT_PATH = "docs/agents/wave-integration-role.md";
15
+ export const DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH =
16
+ "docs/agents/wave-documentation-role.md";
17
+ export const DEFAULT_TERMINALS_PATH = ".vscode/terminals.json";
18
+ export const DEFAULT_DOCS_DIR = "docs";
19
+ export const DEFAULT_STATE_ROOT = ".tmp";
20
+ export const DEFAULT_ORCHESTRATOR_STATE_DIR = ".tmp/wave-orchestrator";
21
+ export const DEFAULT_CONTEXT7_BUNDLE_INDEX_PATH = "docs/context7/bundles.json";
22
+ export const DEFAULT_COMPONENT_CUTOVER_MATRIX_DOC_PATH = "docs/plans/component-cutover-matrix.md";
23
+ export const DEFAULT_COMPONENT_CUTOVER_MATRIX_JSON_PATH = "docs/plans/component-cutover-matrix.json";
24
+ export const DEFAULT_REQUIRED_PROMPT_REFERENCES = [
25
+ "docs/reference/repository-guidance.md",
26
+ "docs/research/agent-context-sources.md",
27
+ ];
28
+ export const SUPPORTED_EXECUTOR_MODES = ["codex", "claude", "opencode", "local"];
29
+ export const DEFAULT_EXECUTOR_MODE = "codex";
30
+ export const DEFAULT_CODEX_COMMAND = "codex";
31
+ export const DEFAULT_CODEX_SANDBOX_MODE = "danger-full-access";
32
+ export const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
33
+ export const DEFAULT_CLAUDE_COMMAND = "claude";
34
+ export const DEFAULT_OPENCODE_COMMAND = "opencode";
35
+
36
+ export function normalizeExecutorMode(value, label = "executor") {
37
+ const normalized = String(value || "")
38
+ .trim()
39
+ .toLowerCase();
40
+ if (!SUPPORTED_EXECUTOR_MODES.includes(normalized)) {
41
+ throw new Error(
42
+ `${label} must be one of: ${SUPPORTED_EXECUTOR_MODES.join(", ")} (got: ${normalized || "empty"})`,
43
+ );
44
+ }
45
+ return normalized;
46
+ }
47
+
48
+ export function normalizeCodexSandboxMode(value, flagName = "--codex-sandbox") {
49
+ const normalized = String(value || "")
50
+ .trim()
51
+ .toLowerCase();
52
+ if (!CODEX_SANDBOX_MODES.includes(normalized)) {
53
+ throw new Error(
54
+ `${flagName} must be one of: ${CODEX_SANDBOX_MODES.join(", ")} (got: ${normalized || "empty"})`,
55
+ );
56
+ }
57
+ return normalized;
58
+ }
59
+
60
+ function sanitizeLaneName(value) {
61
+ const lane = String(value || "")
62
+ .trim()
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9_-]+/g, "-")
65
+ .replace(/-+/g, "-")
66
+ .replace(/^-+|-+$/g, "");
67
+ if (!lane) {
68
+ throw new Error("Lane name is required");
69
+ }
70
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(lane)) {
71
+ throw new Error(`Invalid lane: ${value}`);
72
+ }
73
+ return lane;
74
+ }
75
+
76
+ function readJsonOrNull(filePath) {
77
+ try {
78
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function normalizeRepoRelativePath(value, label) {
85
+ const raw = String(value || "")
86
+ .trim()
87
+ .replaceAll("\\", "/")
88
+ .replace(/^\.\/+/, "")
89
+ .replace(/\/+/g, "/")
90
+ .replace(/\/$/, "");
91
+ if (!raw) {
92
+ throw new Error(`${label} is required`);
93
+ }
94
+ if (raw.startsWith("/") || raw.startsWith("../") || raw.includes("/../")) {
95
+ throw new Error(`${label} must stay within the repository: ${value}`);
96
+ }
97
+ return raw;
98
+ }
99
+
100
+ function normalizeOptionalPathArray(values, label) {
101
+ if (!Array.isArray(values)) {
102
+ return null;
103
+ }
104
+ return values.map((entry, index) =>
105
+ normalizeRepoRelativePath(entry, `${label}[${index}]`),
106
+ );
107
+ }
108
+
109
+ function normalizeOptionalStringArray(values, fallback = []) {
110
+ if (!Array.isArray(values)) {
111
+ return fallback;
112
+ }
113
+ return values
114
+ .map((value) => String(value || "").trim())
115
+ .filter(Boolean);
116
+ }
117
+
118
+ function normalizeOptionalString(value, fallback = null) {
119
+ const normalized = String(value ?? "")
120
+ .trim();
121
+ return normalized || fallback;
122
+ }
123
+
124
+ function normalizeOptionalPositiveInt(value, label, fallback = null) {
125
+ if (value === null || value === undefined || value === "") {
126
+ return fallback;
127
+ }
128
+ const parsed = Number.parseInt(String(value), 10);
129
+ if (!Number.isFinite(parsed) || parsed <= 0) {
130
+ throw new Error(`${label} must be a positive integer, got: ${value}`);
131
+ }
132
+ return parsed;
133
+ }
134
+
135
+ function normalizeOptionalBoolean(value, fallback = false) {
136
+ if (value === undefined) {
137
+ return fallback;
138
+ }
139
+ if (typeof value === "boolean") {
140
+ return value;
141
+ }
142
+ const normalized = String(value || "")
143
+ .trim()
144
+ .toLowerCase();
145
+ if (["true", "1", "yes", "on"].includes(normalized)) {
146
+ return true;
147
+ }
148
+ if (["false", "0", "no", "off"].includes(normalized)) {
149
+ return false;
150
+ }
151
+ throw new Error(`Invalid boolean value: ${value}`);
152
+ }
153
+
154
+ function normalizeOptionalStringOrStringArray(value, label) {
155
+ if (value === undefined || value === null || value === "") {
156
+ return [];
157
+ }
158
+ if (Array.isArray(value)) {
159
+ return value
160
+ .map((entry, index) => normalizeOptionalString(entry, null))
161
+ .filter(Boolean)
162
+ .map((entry, index) => {
163
+ if (!entry) {
164
+ throw new Error(`${label}[${index}] is required`);
165
+ }
166
+ return entry;
167
+ });
168
+ }
169
+ const normalized = normalizeOptionalString(value, null);
170
+ return normalized ? [normalized] : [];
171
+ }
172
+
173
+ function normalizeExecutorModeArray(value, label) {
174
+ if (value === undefined || value === null || value === "") {
175
+ return [];
176
+ }
177
+ const list = Array.isArray(value)
178
+ ? value
179
+ : String(value)
180
+ .split(",")
181
+ .map((entry) => entry.trim())
182
+ .filter(Boolean);
183
+ return list.map((entry, index) => normalizeExecutorMode(entry, `${label}[${index}]`));
184
+ }
185
+
186
+ function normalizeOptionalJsonObject(value, label) {
187
+ if (value === undefined || value === null || value === "") {
188
+ return null;
189
+ }
190
+ if (typeof value === "object" && !Array.isArray(value)) {
191
+ return JSON.parse(JSON.stringify(value));
192
+ }
193
+ throw new Error(`${label} must be a JSON object`);
194
+ }
195
+
196
+ function normalizeExecutorBudget(rawBudget = {}, label = "budget") {
197
+ const budget =
198
+ rawBudget && typeof rawBudget === "object" && !Array.isArray(rawBudget) ? rawBudget : {};
199
+ const turns = normalizeOptionalPositiveInt(budget.turns, `${label}.turns`, null);
200
+ const minutes = normalizeOptionalPositiveInt(budget.minutes, `${label}.minutes`, null);
201
+ if (turns === null && minutes === null) {
202
+ return null;
203
+ }
204
+ return { turns, minutes };
205
+ }
206
+
207
+ function normalizeThreshold(value, fallback) {
208
+ if (value === null) {
209
+ return null;
210
+ }
211
+ if (value === undefined) {
212
+ return fallback;
213
+ }
214
+ const parsed = Number.parseInt(String(value), 10);
215
+ if (!Number.isFinite(parsed) || parsed < 0) {
216
+ throw new Error(`Invalid wave threshold: ${value}`);
217
+ }
218
+ return parsed;
219
+ }
220
+
221
+ function defaultDocsDirForLane(lane, defaultLane, repoMode) {
222
+ if (repoMode === "single-repo" && lane === defaultLane) {
223
+ return DEFAULT_DOCS_DIR;
224
+ }
225
+ return `${DEFAULT_DOCS_DIR}/${lane}`;
226
+ }
227
+
228
+ function defaultPlansDir(docsDir) {
229
+ return `${docsDir}/plans`;
230
+ }
231
+
232
+ function defaultWavesDir(plansDir) {
233
+ return `${plansDir}/waves`;
234
+ }
235
+
236
+ function defaultComponentCutoverMatrixDocPath(plansDir) {
237
+ return `${plansDir}/component-cutover-matrix.md`;
238
+ }
239
+
240
+ function defaultComponentCutoverMatrixJsonPath(plansDir) {
241
+ return `${plansDir}/component-cutover-matrix.json`;
242
+ }
243
+
244
+ function defaultSharedPlanDocs(plansDir) {
245
+ return ["current-state.md", "master-plan.md", "migration.md"].map(
246
+ (fileName) => `${plansDir}/${fileName}`,
247
+ );
248
+ }
249
+
250
+ function normalizeRoles(rawRoles = {}) {
251
+ const rolePromptDir = normalizeRepoRelativePath(
252
+ rawRoles.rolePromptDir || DEFAULT_ROLE_PROMPT_DIR,
253
+ "roles.rolePromptDir",
254
+ );
255
+ return {
256
+ rolePromptDir,
257
+ evaluatorAgentId: String(rawRoles.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID).trim(),
258
+ integrationAgentId: String(rawRoles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID).trim(),
259
+ documentationAgentId: String(
260
+ rawRoles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID,
261
+ ).trim(),
262
+ evaluatorRolePromptPath: normalizeRepoRelativePath(
263
+ rawRoles.evaluatorRolePromptPath || DEFAULT_EVALUATOR_ROLE_PROMPT_PATH,
264
+ "roles.evaluatorRolePromptPath",
265
+ ),
266
+ integrationRolePromptPath: normalizeRepoRelativePath(
267
+ rawRoles.integrationRolePromptPath || DEFAULT_INTEGRATION_ROLE_PROMPT_PATH,
268
+ "roles.integrationRolePromptPath",
269
+ ),
270
+ documentationRolePromptPath: normalizeRepoRelativePath(
271
+ rawRoles.documentationRolePromptPath || DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH,
272
+ "roles.documentationRolePromptPath",
273
+ ),
274
+ };
275
+ }
276
+
277
+ function normalizeValidation(rawValidation = {}) {
278
+ return {
279
+ requiredPromptReferences: normalizeOptionalStringArray(
280
+ rawValidation.requiredPromptReferences,
281
+ DEFAULT_REQUIRED_PROMPT_REFERENCES,
282
+ ),
283
+ requireDocumentationStewardFromWave: normalizeThreshold(
284
+ rawValidation.requireDocumentationStewardFromWave,
285
+ 0,
286
+ ),
287
+ requireContext7DeclarationsFromWave: normalizeThreshold(
288
+ rawValidation.requireContext7DeclarationsFromWave,
289
+ 6,
290
+ ),
291
+ requireExitContractsFromWave: normalizeThreshold(
292
+ rawValidation.requireExitContractsFromWave,
293
+ 6,
294
+ ),
295
+ requireIntegrationStewardFromWave: normalizeThreshold(
296
+ rawValidation.requireIntegrationStewardFromWave,
297
+ null,
298
+ ),
299
+ requireComponentPromotionsFromWave: normalizeThreshold(
300
+ rawValidation.requireComponentPromotionsFromWave,
301
+ 0,
302
+ ),
303
+ requireAgentComponentsFromWave: normalizeThreshold(
304
+ rawValidation.requireAgentComponentsFromWave,
305
+ 0,
306
+ ),
307
+ };
308
+ }
309
+
310
+ function normalizeCapabilityRouting(rawCapabilityRouting = {}) {
311
+ const preferredAgentsInput =
312
+ rawCapabilityRouting && typeof rawCapabilityRouting === "object"
313
+ ? rawCapabilityRouting.preferredAgents
314
+ : null;
315
+ const preferredAgents =
316
+ preferredAgentsInput &&
317
+ typeof preferredAgentsInput === "object" &&
318
+ !Array.isArray(preferredAgentsInput)
319
+ ? Object.fromEntries(
320
+ Object.entries(preferredAgentsInput).map(([capability, agentIds]) => [
321
+ String(capability || "")
322
+ .trim()
323
+ .toLowerCase(),
324
+ normalizeOptionalStringArray(agentIds, []),
325
+ ]),
326
+ )
327
+ : {};
328
+ return {
329
+ preferredAgents,
330
+ };
331
+ }
332
+
333
+ function normalizeRuntimeMixTargets(rawRuntimeMixTargets = {}) {
334
+ if (
335
+ !rawRuntimeMixTargets ||
336
+ typeof rawRuntimeMixTargets !== "object" ||
337
+ Array.isArray(rawRuntimeMixTargets)
338
+ ) {
339
+ return {};
340
+ }
341
+ return Object.fromEntries(
342
+ Object.entries(rawRuntimeMixTargets).map(([executorId, rawCount]) => {
343
+ const normalizedExecutor = normalizeExecutorMode(
344
+ executorId,
345
+ `runtimeMixTargets.${executorId}`,
346
+ );
347
+ const count = Number.parseInt(String(rawCount), 10);
348
+ if (!Number.isFinite(count) || count < 0) {
349
+ throw new Error(`runtimeMixTargets.${executorId} must be a non-negative integer`);
350
+ }
351
+ return [normalizedExecutor, count];
352
+ }),
353
+ );
354
+ }
355
+
356
+ function normalizeDefaultExecutorByRole(rawDefaultExecutorByRole = {}) {
357
+ if (
358
+ !rawDefaultExecutorByRole ||
359
+ typeof rawDefaultExecutorByRole !== "object" ||
360
+ Array.isArray(rawDefaultExecutorByRole)
361
+ ) {
362
+ return {};
363
+ }
364
+ return Object.fromEntries(
365
+ Object.entries(rawDefaultExecutorByRole).map(([role, executorId]) => [
366
+ String(role || "")
367
+ .trim()
368
+ .toLowerCase(),
369
+ normalizeExecutorMode(executorId, `defaultExecutorByRole.${role}`),
370
+ ]),
371
+ );
372
+ }
373
+
374
+ function normalizeRuntimePolicy(rawRuntimePolicy = {}) {
375
+ const runtimePolicy =
376
+ rawRuntimePolicy && typeof rawRuntimePolicy === "object" && !Array.isArray(rawRuntimePolicy)
377
+ ? rawRuntimePolicy
378
+ : {};
379
+ return {
380
+ runtimeMixTargets: normalizeRuntimeMixTargets(runtimePolicy.runtimeMixTargets),
381
+ defaultExecutorByRole: normalizeDefaultExecutorByRole(
382
+ runtimePolicy.defaultExecutorByRole,
383
+ ),
384
+ fallbackExecutorOrder: normalizeExecutorModeArray(
385
+ runtimePolicy.fallbackExecutorOrder,
386
+ "runtimePolicy.fallbackExecutorOrder",
387
+ ),
388
+ };
389
+ }
390
+
391
+ function normalizeClaudePromptMode(value, label = "executors.claude.appendSystemPromptMode") {
392
+ const normalized = String(value || "append")
393
+ .trim()
394
+ .toLowerCase();
395
+ if (!["append", "replace"].includes(normalized)) {
396
+ throw new Error(`${label} must be "append" or "replace"`);
397
+ }
398
+ return normalized;
399
+ }
400
+
401
+ function normalizeClaudeOutputFormat(value, label = "executors.claude.outputFormat") {
402
+ const normalized = String(value || "text")
403
+ .trim()
404
+ .toLowerCase();
405
+ if (!["text", "json", "stream-json"].includes(normalized)) {
406
+ throw new Error(`${label} must be one of: text, json, stream-json`);
407
+ }
408
+ return normalized;
409
+ }
410
+
411
+ function normalizeOpenCodeFormat(value, label = "executors.opencode.format") {
412
+ const normalized = String(value || "default")
413
+ .trim()
414
+ .toLowerCase();
415
+ if (!["default", "json"].includes(normalized)) {
416
+ throw new Error(`${label} must be one of: default, json`);
417
+ }
418
+ return normalized;
419
+ }
420
+
421
+ function normalizeExecutorProfile(rawProfile = {}, label = "executors.profiles.<profile>") {
422
+ if (!rawProfile || typeof rawProfile !== "object" || Array.isArray(rawProfile)) {
423
+ throw new Error(`${label} must be an object`);
424
+ }
425
+ return {
426
+ id:
427
+ rawProfile.id === undefined || rawProfile.id === null || rawProfile.id === ""
428
+ ? null
429
+ : normalizeExecutorMode(rawProfile.id, `${label}.id`),
430
+ model: normalizeOptionalString(rawProfile.model, null),
431
+ fallbacks: normalizeExecutorModeArray(rawProfile.fallbacks, `${label}.fallbacks`),
432
+ tags: normalizeOptionalStringArray(rawProfile.tags, []),
433
+ budget: normalizeExecutorBudget(rawProfile.budget, `${label}.budget`),
434
+ codex: rawProfile.codex
435
+ ? {
436
+ command: normalizeOptionalString(rawProfile.codex.command, null),
437
+ profileName: normalizeOptionalString(rawProfile.codex.profileName, null),
438
+ config: normalizeOptionalStringOrStringArray(
439
+ rawProfile.codex.config,
440
+ `${label}.codex.config`,
441
+ ),
442
+ search:
443
+ rawProfile.codex.search === undefined
444
+ ? null
445
+ : normalizeOptionalBoolean(rawProfile.codex.search, false),
446
+ images: normalizeOptionalStringOrStringArray(
447
+ rawProfile.codex.images,
448
+ `${label}.codex.images`,
449
+ ),
450
+ addDirs: normalizeOptionalStringOrStringArray(
451
+ rawProfile.codex.addDirs,
452
+ `${label}.codex.addDirs`,
453
+ ),
454
+ json:
455
+ rawProfile.codex.json === undefined
456
+ ? null
457
+ : normalizeOptionalBoolean(rawProfile.codex.json, false),
458
+ ephemeral:
459
+ rawProfile.codex.ephemeral === undefined
460
+ ? null
461
+ : normalizeOptionalBoolean(rawProfile.codex.ephemeral, false),
462
+ sandbox:
463
+ rawProfile.codex.sandbox === undefined ||
464
+ rawProfile.codex.sandbox === null ||
465
+ rawProfile.codex.sandbox === ""
466
+ ? null
467
+ : normalizeCodexSandboxMode(
468
+ rawProfile.codex.sandbox,
469
+ `${label}.codex.sandbox`,
470
+ ),
471
+ }
472
+ : null,
473
+ claude: rawProfile.claude
474
+ ? {
475
+ command: normalizeOptionalString(rawProfile.claude.command, null),
476
+ agent: normalizeOptionalString(rawProfile.claude.agent, null),
477
+ permissionMode: normalizeOptionalString(
478
+ rawProfile.claude.permissionMode,
479
+ null,
480
+ ),
481
+ permissionPromptTool: normalizeOptionalString(
482
+ rawProfile.claude.permissionPromptTool,
483
+ null,
484
+ ),
485
+ maxTurns: normalizeOptionalPositiveInt(
486
+ rawProfile.claude.maxTurns,
487
+ `${label}.claude.maxTurns`,
488
+ null,
489
+ ),
490
+ mcpConfig: normalizeOptionalStringOrStringArray(
491
+ rawProfile.claude.mcpConfig,
492
+ `${label}.claude.mcpConfig`,
493
+ ),
494
+ strictMcpConfig:
495
+ rawProfile.claude.strictMcpConfig === undefined
496
+ ? null
497
+ : normalizeOptionalBoolean(
498
+ rawProfile.claude.strictMcpConfig,
499
+ false,
500
+ ),
501
+ settings: normalizeOptionalString(rawProfile.claude.settings, null),
502
+ settingsJson: normalizeOptionalJsonObject(
503
+ rawProfile.claude.settingsJson,
504
+ `${label}.claude.settingsJson`,
505
+ ),
506
+ hooksJson: normalizeOptionalJsonObject(
507
+ rawProfile.claude.hooksJson,
508
+ `${label}.claude.hooksJson`,
509
+ ),
510
+ allowedHttpHookUrls: normalizeOptionalStringOrStringArray(
511
+ rawProfile.claude.allowedHttpHookUrls,
512
+ `${label}.claude.allowedHttpHookUrls`,
513
+ ),
514
+ outputFormat:
515
+ rawProfile.claude.outputFormat === undefined ||
516
+ rawProfile.claude.outputFormat === null ||
517
+ rawProfile.claude.outputFormat === ""
518
+ ? null
519
+ : normalizeClaudeOutputFormat(
520
+ rawProfile.claude.outputFormat,
521
+ `${label}.claude.outputFormat`,
522
+ ),
523
+ allowedTools: normalizeOptionalStringArray(rawProfile.claude.allowedTools, []),
524
+ disallowedTools: normalizeOptionalStringArray(
525
+ rawProfile.claude.disallowedTools,
526
+ [],
527
+ ),
528
+ }
529
+ : null,
530
+ opencode: rawProfile.opencode
531
+ ? {
532
+ command: normalizeOptionalString(rawProfile.opencode.command, null),
533
+ agent: normalizeOptionalString(rawProfile.opencode.agent, null),
534
+ attach: normalizeOptionalString(rawProfile.opencode.attach, null),
535
+ files: normalizeOptionalStringOrStringArray(
536
+ rawProfile.opencode.files,
537
+ `${label}.opencode.files`,
538
+ ),
539
+ format:
540
+ rawProfile.opencode.format === undefined ||
541
+ rawProfile.opencode.format === null ||
542
+ rawProfile.opencode.format === ""
543
+ ? null
544
+ : normalizeOpenCodeFormat(
545
+ rawProfile.opencode.format,
546
+ `${label}.opencode.format`,
547
+ ),
548
+ steps: normalizeOptionalPositiveInt(
549
+ rawProfile.opencode.steps,
550
+ `${label}.opencode.steps`,
551
+ null,
552
+ ),
553
+ instructions: normalizeOptionalStringArray(rawProfile.opencode.instructions, []),
554
+ permission: normalizeOptionalJsonObject(
555
+ rawProfile.opencode.permission,
556
+ `${label}.opencode.permission`,
557
+ ),
558
+ configJson: normalizeOptionalJsonObject(
559
+ rawProfile.opencode.configJson,
560
+ `${label}.opencode.configJson`,
561
+ ),
562
+ }
563
+ : null,
564
+ };
565
+ }
566
+
567
+ function mergeExecutors(baseExecutors = {}, overrideExecutors = {}) {
568
+ return {
569
+ ...baseExecutors,
570
+ ...overrideExecutors,
571
+ profiles: {
572
+ ...(baseExecutors.profiles || {}),
573
+ ...(overrideExecutors.profiles || {}),
574
+ },
575
+ codex: {
576
+ ...(baseExecutors.codex || {}),
577
+ ...(overrideExecutors.codex || {}),
578
+ },
579
+ claude: {
580
+ ...(baseExecutors.claude || {}),
581
+ ...(overrideExecutors.claude || {}),
582
+ },
583
+ opencode: {
584
+ ...(baseExecutors.opencode || {}),
585
+ ...(overrideExecutors.opencode || {}),
586
+ },
587
+ };
588
+ }
589
+
590
+ function normalizeExecutors(rawExecutors = {}) {
591
+ const executors = rawExecutors && typeof rawExecutors === "object" ? rawExecutors : {};
592
+ return {
593
+ default: normalizeExecutorMode(executors.default || DEFAULT_EXECUTOR_MODE, "executors.default"),
594
+ profiles:
595
+ executors.profiles &&
596
+ typeof executors.profiles === "object" &&
597
+ !Array.isArray(executors.profiles)
598
+ ? Object.fromEntries(
599
+ Object.entries(executors.profiles).map(([profileName, profile]) => [
600
+ String(profileName || "")
601
+ .trim()
602
+ .toLowerCase(),
603
+ normalizeExecutorProfile(
604
+ profile,
605
+ `executors.profiles.${profileName}`,
606
+ ),
607
+ ]),
608
+ )
609
+ : {},
610
+ codex: {
611
+ command: normalizeOptionalString(
612
+ executors.codex?.command,
613
+ DEFAULT_CODEX_COMMAND,
614
+ ),
615
+ profileName: normalizeOptionalString(executors.codex?.profileName, null),
616
+ config: normalizeOptionalStringOrStringArray(
617
+ executors.codex?.config,
618
+ "executors.codex.config",
619
+ ),
620
+ search: normalizeOptionalBoolean(executors.codex?.search, false),
621
+ images: normalizeOptionalStringOrStringArray(
622
+ executors.codex?.images,
623
+ "executors.codex.images",
624
+ ),
625
+ addDirs: normalizeOptionalStringOrStringArray(
626
+ executors.codex?.addDirs,
627
+ "executors.codex.addDirs",
628
+ ),
629
+ json: normalizeOptionalBoolean(executors.codex?.json, false),
630
+ ephemeral: normalizeOptionalBoolean(executors.codex?.ephemeral, false),
631
+ sandbox: normalizeCodexSandboxMode(
632
+ executors.codex?.sandbox || DEFAULT_CODEX_SANDBOX_MODE,
633
+ "executors.codex.sandbox",
634
+ ),
635
+ },
636
+ claude: {
637
+ command: normalizeOptionalString(
638
+ executors.claude?.command,
639
+ DEFAULT_CLAUDE_COMMAND,
640
+ ),
641
+ model: normalizeOptionalString(executors.claude?.model, null),
642
+ agent: normalizeOptionalString(executors.claude?.agent, null),
643
+ appendSystemPromptMode: normalizeClaudePromptMode(
644
+ executors.claude?.appendSystemPromptMode,
645
+ ),
646
+ permissionMode: normalizeOptionalString(executors.claude?.permissionMode, null),
647
+ permissionPromptTool: normalizeOptionalString(
648
+ executors.claude?.permissionPromptTool,
649
+ null,
650
+ ),
651
+ maxTurns: normalizeOptionalPositiveInt(executors.claude?.maxTurns, "executors.claude.maxTurns"),
652
+ mcpConfig: normalizeOptionalStringOrStringArray(
653
+ executors.claude?.mcpConfig,
654
+ "executors.claude.mcpConfig",
655
+ ),
656
+ strictMcpConfig: normalizeOptionalBoolean(executors.claude?.strictMcpConfig, false),
657
+ settings: normalizeOptionalString(executors.claude?.settings, null),
658
+ settingsJson: normalizeOptionalJsonObject(
659
+ executors.claude?.settingsJson,
660
+ "executors.claude.settingsJson",
661
+ ),
662
+ hooksJson: normalizeOptionalJsonObject(
663
+ executors.claude?.hooksJson,
664
+ "executors.claude.hooksJson",
665
+ ),
666
+ allowedHttpHookUrls: normalizeOptionalStringOrStringArray(
667
+ executors.claude?.allowedHttpHookUrls,
668
+ "executors.claude.allowedHttpHookUrls",
669
+ ),
670
+ outputFormat: normalizeClaudeOutputFormat(executors.claude?.outputFormat),
671
+ allowedTools: normalizeOptionalStringArray(executors.claude?.allowedTools, []),
672
+ disallowedTools: normalizeOptionalStringArray(executors.claude?.disallowedTools, []),
673
+ },
674
+ opencode: {
675
+ command: normalizeOptionalString(
676
+ executors.opencode?.command,
677
+ DEFAULT_OPENCODE_COMMAND,
678
+ ),
679
+ model: normalizeOptionalString(executors.opencode?.model, null),
680
+ agent: normalizeOptionalString(executors.opencode?.agent, null),
681
+ attach: normalizeOptionalString(executors.opencode?.attach, null),
682
+ files: normalizeOptionalStringOrStringArray(
683
+ executors.opencode?.files,
684
+ "executors.opencode.files",
685
+ ),
686
+ format: normalizeOpenCodeFormat(executors.opencode?.format),
687
+ steps: normalizeOptionalPositiveInt(executors.opencode?.steps, "executors.opencode.steps"),
688
+ instructions: normalizeOptionalStringArray(executors.opencode?.instructions, []),
689
+ permission: normalizeOptionalJsonObject(executors.opencode?.permission, "executors.opencode.permission"),
690
+ configJson: normalizeOptionalJsonObject(
691
+ executors.opencode?.configJson,
692
+ "executors.opencode.configJson",
693
+ ),
694
+ },
695
+ };
696
+ }
697
+
698
+ export function loadWaveConfig(configPath = DEFAULT_WAVE_CONFIG_PATH) {
699
+ const rawConfig = readJsonOrNull(configPath) || {};
700
+ const repoMode =
701
+ String(rawConfig.repoMode || "single-repo")
702
+ .trim()
703
+ .toLowerCase() || "single-repo";
704
+ if (!["single-repo", "multi-lane"].includes(repoMode)) {
705
+ throw new Error(`Unsupported repoMode in ${path.relative(REPO_ROOT, configPath)}: ${repoMode}`);
706
+ }
707
+ const defaultLane = sanitizeLaneName(rawConfig.defaultLane || DEFAULT_WAVE_LANE);
708
+ const paths = {
709
+ docsDir: normalizeRepoRelativePath(rawConfig.paths?.docsDir || DEFAULT_DOCS_DIR, "paths.docsDir"),
710
+ stateRoot: normalizeRepoRelativePath(
711
+ rawConfig.paths?.stateRoot || DEFAULT_STATE_ROOT,
712
+ "paths.stateRoot",
713
+ ),
714
+ orchestratorStateDir: normalizeRepoRelativePath(
715
+ rawConfig.paths?.orchestratorStateDir || DEFAULT_ORCHESTRATOR_STATE_DIR,
716
+ "paths.orchestratorStateDir",
717
+ ),
718
+ terminalsPath: normalizeRepoRelativePath(
719
+ rawConfig.paths?.terminalsPath || DEFAULT_TERMINALS_PATH,
720
+ "paths.terminalsPath",
721
+ ),
722
+ context7BundleIndexPath: normalizeRepoRelativePath(
723
+ rawConfig.paths?.context7BundleIndexPath || DEFAULT_CONTEXT7_BUNDLE_INDEX_PATH,
724
+ "paths.context7BundleIndexPath",
725
+ ),
726
+ componentCutoverMatrixDocPath: normalizeRepoRelativePath(
727
+ rawConfig.paths?.componentCutoverMatrixDocPath ||
728
+ defaultComponentCutoverMatrixDocPath(defaultPlansDir(rawConfig.paths?.docsDir || DEFAULT_DOCS_DIR)),
729
+ "paths.componentCutoverMatrixDocPath",
730
+ ),
731
+ componentCutoverMatrixJsonPath: normalizeRepoRelativePath(
732
+ rawConfig.paths?.componentCutoverMatrixJsonPath ||
733
+ defaultComponentCutoverMatrixJsonPath(defaultPlansDir(rawConfig.paths?.docsDir || DEFAULT_DOCS_DIR)),
734
+ "paths.componentCutoverMatrixJsonPath",
735
+ ),
736
+ };
737
+ const sharedPlanDocs =
738
+ normalizeOptionalPathArray(rawConfig.sharedPlanDocs, "sharedPlanDocs") || null;
739
+ const lanes = Object.fromEntries(
740
+ Object.entries(rawConfig.lanes || {}).map(([laneName, laneConfig]) => [
741
+ sanitizeLaneName(laneName),
742
+ laneConfig || {},
743
+ ]),
744
+ );
745
+ return {
746
+ version: Number.parseInt(String(rawConfig.version ?? "1"), 10) || 1,
747
+ projectName: String(rawConfig.projectName || "Wave Orchestrator").trim(),
748
+ repoMode,
749
+ defaultLane,
750
+ paths,
751
+ roles: normalizeRoles(rawConfig.roles),
752
+ validation: normalizeValidation(rawConfig.validation),
753
+ executors: normalizeExecutors(rawConfig.executors),
754
+ capabilityRouting: normalizeCapabilityRouting(rawConfig.capabilityRouting),
755
+ runtimePolicy: normalizeRuntimePolicy(rawConfig.runtimePolicy),
756
+ sharedPlanDocs,
757
+ lanes,
758
+ configPath,
759
+ };
760
+ }
761
+
762
+ export function resolveLaneProfile(config, laneInput = config.defaultLane) {
763
+ const lane = sanitizeLaneName(laneInput || config.defaultLane);
764
+ const laneConfig = config.lanes[lane] || {};
765
+ const docsDir = normalizeRepoRelativePath(
766
+ laneConfig.docsDir || defaultDocsDirForLane(lane, config.defaultLane, config.repoMode),
767
+ `${lane}.docsDir`,
768
+ );
769
+ const plansDir = normalizeRepoRelativePath(
770
+ laneConfig.plansDir || defaultPlansDir(docsDir),
771
+ `${lane}.plansDir`,
772
+ );
773
+ const wavesDir = normalizeRepoRelativePath(
774
+ laneConfig.wavesDir || defaultWavesDir(plansDir),
775
+ `${lane}.wavesDir`,
776
+ );
777
+ const roles = normalizeRoles({
778
+ ...config.roles,
779
+ ...(laneConfig.roles || {}),
780
+ });
781
+ const validation = normalizeValidation({
782
+ ...config.validation,
783
+ ...(laneConfig.validation || {}),
784
+ });
785
+ const executors = normalizeExecutors(
786
+ mergeExecutors(config.executors, laneConfig.executors),
787
+ );
788
+ const capabilityRouting = normalizeCapabilityRouting({
789
+ ...config.capabilityRouting,
790
+ ...(laneConfig.capabilityRouting || {}),
791
+ });
792
+ const runtimePolicy = normalizeRuntimePolicy({
793
+ ...config.runtimePolicy,
794
+ ...(laneConfig.runtimePolicy || {}),
795
+ ...(laneConfig.runtimeMixTargets ? { runtimeMixTargets: laneConfig.runtimeMixTargets } : {}),
796
+ ...(laneConfig.defaultExecutorByRole
797
+ ? { defaultExecutorByRole: laneConfig.defaultExecutorByRole }
798
+ : {}),
799
+ ...(laneConfig.fallbackExecutorOrder
800
+ ? { fallbackExecutorOrder: laneConfig.fallbackExecutorOrder }
801
+ : {}),
802
+ });
803
+ return {
804
+ lane,
805
+ docsDir,
806
+ plansDir,
807
+ wavesDir,
808
+ sharedPlanDocs:
809
+ normalizeOptionalPathArray(laneConfig.sharedPlanDocs, `${lane}.sharedPlanDocs`) ||
810
+ config.sharedPlanDocs ||
811
+ defaultSharedPlanDocs(plansDir),
812
+ roles,
813
+ validation,
814
+ executors,
815
+ capabilityRouting,
816
+ runtimePolicy,
817
+ paths: {
818
+ terminalsPath: normalizeRepoRelativePath(
819
+ laneConfig.terminalsPath || config.paths.terminalsPath,
820
+ `${lane}.terminalsPath`,
821
+ ),
822
+ stateRoot: normalizeRepoRelativePath(
823
+ laneConfig.stateRoot || config.paths.stateRoot,
824
+ `${lane}.stateRoot`,
825
+ ),
826
+ orchestratorStateDir: normalizeRepoRelativePath(
827
+ laneConfig.orchestratorStateDir || config.paths.orchestratorStateDir,
828
+ `${lane}.orchestratorStateDir`,
829
+ ),
830
+ context7BundleIndexPath: normalizeRepoRelativePath(
831
+ laneConfig.context7BundleIndexPath || config.paths.context7BundleIndexPath,
832
+ `${lane}.context7BundleIndexPath`,
833
+ ),
834
+ componentCutoverMatrixDocPath: normalizeRepoRelativePath(
835
+ laneConfig.componentCutoverMatrixDocPath ||
836
+ config.paths.componentCutoverMatrixDocPath ||
837
+ defaultComponentCutoverMatrixDocPath(plansDir),
838
+ `${lane}.componentCutoverMatrixDocPath`,
839
+ ),
840
+ componentCutoverMatrixJsonPath: normalizeRepoRelativePath(
841
+ laneConfig.componentCutoverMatrixJsonPath ||
842
+ config.paths.componentCutoverMatrixJsonPath ||
843
+ defaultComponentCutoverMatrixJsonPath(plansDir),
844
+ `${lane}.componentCutoverMatrixJsonPath`,
845
+ ),
846
+ },
847
+ };
848
+ }