@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,1923 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ DEFAULT_CODEX_SANDBOX_MODE,
6
+ DEFAULT_DOCUMENTATION_AGENT_ID,
7
+ DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH,
8
+ DEFAULT_EVALUATOR_AGENT_ID,
9
+ DEFAULT_EVALUATOR_ROLE_PROMPT_PATH,
10
+ DEFAULT_INTEGRATION_AGENT_ID,
11
+ DEFAULT_INTEGRATION_ROLE_PROMPT_PATH,
12
+ DEFAULT_WAVE_LANE,
13
+ loadWaveConfig,
14
+ normalizeCodexSandboxMode,
15
+ normalizeExecutorMode,
16
+ resolveLaneProfile,
17
+ } from "./config.mjs";
18
+ import {
19
+ REPO_ROOT,
20
+ ensureDirectory,
21
+ hashText,
22
+ parseVerdictFromText,
23
+ readJsonOrNull,
24
+ readFileTail,
25
+ readStatusRecordIfPresent,
26
+ REPORT_VERDICT_REGEX,
27
+ WAVE_VERDICT_REGEX,
28
+ walkFiles,
29
+ writeJsonAtomic,
30
+ } from "./shared.mjs";
31
+ import { normalizeContext7Config, hashAgentPromptFingerprint } from "./context7.mjs";
32
+ import {
33
+ openClarificationLinkedRequests,
34
+ readMaterializedCoordinationState,
35
+ } from "./coordination-store.mjs";
36
+ import {
37
+ normalizeExitContract,
38
+ readAgentExecutionSummary,
39
+ validateDocumentationClosureSummary,
40
+ validateEvaluatorSummary,
41
+ validateExitContractShape,
42
+ validateIntegrationSummary,
43
+ validateImplementationSummary,
44
+ } from "./agent-state.mjs";
45
+
46
+ export const WAVE_EVALUATOR_ROLE_PROMPT_PATH = DEFAULT_EVALUATOR_ROLE_PROMPT_PATH;
47
+ export const WAVE_INTEGRATION_ROLE_PROMPT_PATH = DEFAULT_INTEGRATION_ROLE_PROMPT_PATH;
48
+ export const WAVE_DOCUMENTATION_ROLE_PROMPT_PATH = DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH;
49
+ export const SHARED_PLAN_DOC_PATHS = [
50
+ "docs/plans/current-state.md",
51
+ "docs/plans/master-plan.md",
52
+ "docs/plans/migration.md",
53
+ ];
54
+
55
+ const COMPONENT_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
56
+
57
+ function resolveLaneProfileForOptions(options = {}) {
58
+ if (options.laneProfile) {
59
+ return options.laneProfile;
60
+ }
61
+ const config = options.config || loadWaveConfig();
62
+ return resolveLaneProfile(config, options.lane || config.defaultLane || DEFAULT_WAVE_LANE);
63
+ }
64
+
65
+ export function waveNumberFromFileName(fileName) {
66
+ const match = fileName.match(/^wave-(\d+)\.md$/);
67
+ if (!match) {
68
+ throw new Error(`Invalid wave filename: ${fileName}`);
69
+ }
70
+ return Number.parseInt(match[1], 10);
71
+ }
72
+
73
+ function escapeRegExp(value) {
74
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+ }
76
+
77
+ function isAllowedRolePromptPath(relPath, rolePromptDir) {
78
+ const allowedDir = String(rolePromptDir || "docs/agents")
79
+ .replaceAll("\\", "/")
80
+ .replace(/\/+$/, "");
81
+ return (
82
+ isRepoContainedPath(relPath) &&
83
+ relPath.replaceAll("\\", "/").startsWith(`${allowedDir}/`) &&
84
+ /\.md$/i.test(relPath)
85
+ );
86
+ }
87
+
88
+ function extractSectionBody(sectionText, heading, filePath, agentId, options = {}) {
89
+ const headingMatch = sectionText.match(new RegExp(`### ${escapeRegExp(heading)}[\\r\\n]+`));
90
+ if (!headingMatch) {
91
+ if (options.required === false) {
92
+ return null;
93
+ }
94
+ throw new Error(`Missing "### ${heading}" section for agent ${agentId} in ${filePath}`);
95
+ }
96
+ const afterHeading = sectionText.slice(headingMatch.index + headingMatch[0].length);
97
+ const nextHeadingMatch = /^###\s+/m.exec(afterHeading);
98
+ return (nextHeadingMatch ? afterHeading.slice(0, nextHeadingMatch.index) : afterHeading).trim();
99
+ }
100
+
101
+ function extractTopLevelSectionBody(content, heading, filePath, options = {}) {
102
+ const headingMatch = content.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
103
+ if (!headingMatch) {
104
+ if (options.required === false) {
105
+ return null;
106
+ }
107
+ throw new Error(`Missing "## ${heading}" section in ${filePath}`);
108
+ }
109
+ const afterHeading = content.slice(headingMatch.index + headingMatch[0].length);
110
+ const nextHeadingMatch = /^##\s+/m.exec(afterHeading);
111
+ return (nextHeadingMatch ? afterHeading.slice(0, nextHeadingMatch.index) : afterHeading).trim();
112
+ }
113
+
114
+ function normalizeComponentId(value, label, filePath) {
115
+ const normalized = String(value || "").trim();
116
+ if (!COMPONENT_ID_REGEX.test(normalized)) {
117
+ throw new Error(`Invalid component id "${value}" in ${label} (${filePath})`);
118
+ }
119
+ return normalized;
120
+ }
121
+
122
+ function parseComponentPromotions(blockText, filePath, label) {
123
+ if (!blockText) {
124
+ return [];
125
+ }
126
+ const promotions = [];
127
+ const seen = new Set();
128
+ for (const line of String(blockText || "").split(/\r?\n/)) {
129
+ const trimmed = line.trim();
130
+ if (!trimmed) {
131
+ continue;
132
+ }
133
+ const bulletMatch = trimmed.match(/^-\s+([a-z0-9._-]+)\s*:\s*([a-z0-9._-]+)\s*$/i);
134
+ if (!bulletMatch) {
135
+ throw new Error(`Malformed component promotion "${trimmed}" in ${label} (${filePath})`);
136
+ }
137
+ const componentId = normalizeComponentId(bulletMatch[1], label, filePath);
138
+ const targetLevel = String(bulletMatch[2] || "").trim();
139
+ if (!targetLevel) {
140
+ throw new Error(`Missing component target level for ${componentId} in ${label} (${filePath})`);
141
+ }
142
+ if (seen.has(componentId)) {
143
+ throw new Error(`Duplicate component promotion "${componentId}" in ${label} (${filePath})`);
144
+ }
145
+ seen.add(componentId);
146
+ promotions.push({ componentId, targetLevel });
147
+ }
148
+ return promotions;
149
+ }
150
+
151
+ function parseComponentList(blockText, filePath, label) {
152
+ if (!blockText) {
153
+ return [];
154
+ }
155
+ const components = [];
156
+ const seen = new Set();
157
+ for (const line of String(blockText || "").split(/\r?\n/)) {
158
+ const trimmed = line.trim();
159
+ if (!trimmed) {
160
+ continue;
161
+ }
162
+ const bulletMatch = trimmed.match(/^-\s+([a-z0-9._-]+)\s*$/i);
163
+ if (!bulletMatch) {
164
+ throw new Error(`Malformed component entry "${trimmed}" in ${label} (${filePath})`);
165
+ }
166
+ const componentId = normalizeComponentId(bulletMatch[1], label, filePath);
167
+ if (seen.has(componentId)) {
168
+ throw new Error(`Duplicate component "${componentId}" in ${label} (${filePath})`);
169
+ }
170
+ seen.add(componentId);
171
+ components.push(componentId);
172
+ }
173
+ return components;
174
+ }
175
+
176
+ function extractFencedBlock(blockText, messagePrefix) {
177
+ const fencedBlockMatch = String(blockText || "").match(
178
+ /```(?:[a-zA-Z0-9_-]+)?\r?\n([\s\S]*?)\r?\n```/,
179
+ );
180
+ if (!fencedBlockMatch) {
181
+ throw new Error(`${messagePrefix}: missing fenced prompt block`);
182
+ }
183
+ return fencedBlockMatch[1].trim();
184
+ }
185
+
186
+ export function extractPromptFromSection(sectionText, filePath, agentId) {
187
+ const promptBlock = extractSectionBody(sectionText, "Prompt", filePath, agentId);
188
+ return extractFencedBlock(promptBlock, `Agent ${agentId} in ${filePath}`);
189
+ }
190
+
191
+ function parseContext7Settings(blockText, filePath, label) {
192
+ if (!blockText) {
193
+ return null;
194
+ }
195
+ const settings = {};
196
+ for (const line of String(blockText || "").split(/\r?\n/)) {
197
+ const trimmed = line.trim();
198
+ if (!trimmed) {
199
+ continue;
200
+ }
201
+ const bulletMatch = trimmed.match(/^-\s+([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*$/);
202
+ if (!bulletMatch) {
203
+ throw new Error(`Malformed Context7 setting "${trimmed}" in ${label} (${filePath})`);
204
+ }
205
+ settings[bulletMatch[1]] = bulletMatch[2];
206
+ }
207
+ return normalizeContext7Config(settings);
208
+ }
209
+
210
+ export function extractContext7ConfigFromSection(sectionText, filePath, agentId) {
211
+ const context7Block = extractSectionBody(sectionText, "Context7", filePath, agentId, {
212
+ required: false,
213
+ });
214
+ return parseContext7Settings(context7Block, filePath, `agent ${agentId}`);
215
+ }
216
+
217
+ function parseExitContractSettings(blockText, filePath, label) {
218
+ if (!blockText) {
219
+ return null;
220
+ }
221
+ const settings = {};
222
+ for (const line of String(blockText || "").split(/\r?\n/)) {
223
+ const trimmed = line.trim();
224
+ if (!trimmed) {
225
+ continue;
226
+ }
227
+ const bulletMatch = trimmed.match(/^-\s+([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*$/);
228
+ if (!bulletMatch) {
229
+ throw new Error(`Malformed Exit contract setting "${trimmed}" in ${label} (${filePath})`);
230
+ }
231
+ settings[bulletMatch[1]] = bulletMatch[2];
232
+ }
233
+ return normalizeExitContract(settings);
234
+ }
235
+
236
+ function parsePositiveExecutorInt(value, label, filePath) {
237
+ const parsed = Number.parseInt(String(value), 10);
238
+ if (!Number.isFinite(parsed) || parsed <= 0) {
239
+ throw new Error(`Invalid ${label} "${value}" in ${filePath}`);
240
+ }
241
+ return parsed;
242
+ }
243
+
244
+ function parseExecutorBoolean(value, label, filePath) {
245
+ const normalized = String(value || "")
246
+ .trim()
247
+ .toLowerCase();
248
+ if (["true", "1", "yes", "on"].includes(normalized)) {
249
+ return true;
250
+ }
251
+ if (["false", "0", "no", "off"].includes(normalized)) {
252
+ return false;
253
+ }
254
+ throw new Error(`Invalid ${label} "${value}" in ${filePath}`);
255
+ }
256
+
257
+ function parseExecutorStringList(value) {
258
+ if (Array.isArray(value)) {
259
+ return value.map((entry) => cleanExecutorValue(entry)).filter(Boolean);
260
+ }
261
+ return String(value || "")
262
+ .split(",")
263
+ .map((entry) => cleanExecutorValue(entry))
264
+ .filter(Boolean);
265
+ }
266
+
267
+ function parseExecutorJson(value, label, filePath) {
268
+ try {
269
+ return JSON.parse(String(value || ""));
270
+ } catch (error) {
271
+ throw new Error(`Invalid JSON for ${label} in ${filePath}: ${error.message}`);
272
+ }
273
+ }
274
+
275
+ function parseExecutorJsonObject(value, label, filePath) {
276
+ const parsed = parseExecutorJson(value, label, filePath);
277
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
278
+ throw new Error(`Invalid JSON object for ${label} in ${filePath}`);
279
+ }
280
+ return parsed;
281
+ }
282
+
283
+ function cleanExecutorValue(value) {
284
+ return String(value || "")
285
+ .trim()
286
+ .replace(/^["'`]|["'`]$/g, "");
287
+ }
288
+
289
+ function parseExecutorSettings(blockText, filePath, label) {
290
+ if (!blockText) {
291
+ return null;
292
+ }
293
+ const settings = {};
294
+ for (const line of String(blockText || "").split(/\r?\n/)) {
295
+ const trimmed = line.trim();
296
+ if (!trimmed) {
297
+ continue;
298
+ }
299
+ const bulletMatch = trimmed.match(/^-\s+([a-zA-Z0-9._-]+)\s*:\s*(.+?)\s*$/);
300
+ if (!bulletMatch) {
301
+ throw new Error(`Malformed Executor setting "${trimmed}" in ${label} (${filePath})`);
302
+ }
303
+ settings[bulletMatch[1]] = cleanExecutorValue(bulletMatch[2]);
304
+ }
305
+ return settings;
306
+ }
307
+
308
+ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
309
+ if (!rawSettings || typeof rawSettings !== "object") {
310
+ return null;
311
+ }
312
+ const executorConfig = {
313
+ id: null,
314
+ profile: null,
315
+ model: null,
316
+ fallbacks: [],
317
+ tags: [],
318
+ budget: null,
319
+ codex: null,
320
+ claude: null,
321
+ opencode: null,
322
+ };
323
+ const allowedKeys = new Set([
324
+ "id",
325
+ "profile",
326
+ "model",
327
+ "fallbacks",
328
+ "tags",
329
+ "budget.turns",
330
+ "budget.minutes",
331
+ "codex.command",
332
+ "codex.sandbox",
333
+ "codex.profile_name",
334
+ "codex.config",
335
+ "codex.search",
336
+ "codex.images",
337
+ "codex.add_dirs",
338
+ "codex.json",
339
+ "codex.ephemeral",
340
+ "claude.command",
341
+ "claude.agent",
342
+ "claude.permission_mode",
343
+ "claude.permission_prompt_tool",
344
+ "claude.max_turns",
345
+ "claude.mcp_config",
346
+ "claude.settings",
347
+ "claude.settings_json",
348
+ "claude.hooks_json",
349
+ "claude.allowed_http_hook_urls",
350
+ "claude.output_format",
351
+ "claude.allowed_tools",
352
+ "claude.disallowed_tools",
353
+ "opencode.command",
354
+ "opencode.agent",
355
+ "opencode.attach",
356
+ "opencode.files",
357
+ "opencode.format",
358
+ "opencode.steps",
359
+ "opencode.instructions",
360
+ "opencode.permission",
361
+ "opencode.config_json",
362
+ ]);
363
+ for (const [key, rawValue] of Object.entries(rawSettings)) {
364
+ if (!allowedKeys.has(key)) {
365
+ throw new Error(`Unsupported Executor setting "${key}" in ${label} (${filePath})`);
366
+ }
367
+ const value = cleanExecutorValue(rawValue);
368
+ if (!value) {
369
+ throw new Error(`Empty Executor setting "${key}" in ${label} (${filePath})`);
370
+ }
371
+ if (key === "id") {
372
+ executorConfig.id = normalizeExecutorMode(value, `${label}.id`);
373
+ } else if (key === "profile") {
374
+ executorConfig.profile = value.toLowerCase();
375
+ } else if (key === "model") {
376
+ executorConfig.model = value;
377
+ } else if (key === "fallbacks") {
378
+ executorConfig.fallbacks = parseExecutorStringList(value).map((entry, index) =>
379
+ normalizeExecutorMode(entry, `${label}.fallbacks[${index}]`),
380
+ );
381
+ } else if (key === "tags") {
382
+ executorConfig.tags = parseExecutorStringList(value);
383
+ } else if (key === "budget.turns" || key === "budget.minutes") {
384
+ executorConfig.budget = {
385
+ ...(executorConfig.budget || { turns: null, minutes: null }),
386
+ [key.endsWith(".turns") ? "turns" : "minutes"]: parsePositiveExecutorInt(
387
+ value,
388
+ `${label}.${key}`,
389
+ filePath,
390
+ ),
391
+ };
392
+ } else if (key === "codex.command") {
393
+ executorConfig.codex = {
394
+ ...(executorConfig.codex || {}),
395
+ command: value,
396
+ };
397
+ } else if (key === "codex.sandbox") {
398
+ executorConfig.codex = {
399
+ ...(executorConfig.codex || {}),
400
+ sandbox: normalizeCodexSandboxMode(value, `${label}.codex.sandbox`),
401
+ };
402
+ } else if (key === "codex.profile_name") {
403
+ executorConfig.codex = {
404
+ ...(executorConfig.codex || {}),
405
+ profileName: value,
406
+ };
407
+ } else if (key === "codex.config") {
408
+ executorConfig.codex = {
409
+ ...(executorConfig.codex || {}),
410
+ config: parseExecutorStringList(value),
411
+ };
412
+ } else if (key === "codex.search") {
413
+ executorConfig.codex = {
414
+ ...(executorConfig.codex || {}),
415
+ search: parseExecutorBoolean(value, `${label}.codex.search`, filePath),
416
+ };
417
+ } else if (key === "codex.images") {
418
+ executorConfig.codex = {
419
+ ...(executorConfig.codex || {}),
420
+ images: parseExecutorStringList(value),
421
+ };
422
+ } else if (key === "codex.add_dirs") {
423
+ executorConfig.codex = {
424
+ ...(executorConfig.codex || {}),
425
+ addDirs: parseExecutorStringList(value),
426
+ };
427
+ } else if (key === "codex.json") {
428
+ executorConfig.codex = {
429
+ ...(executorConfig.codex || {}),
430
+ json: parseExecutorBoolean(value, `${label}.codex.json`, filePath),
431
+ };
432
+ } else if (key === "codex.ephemeral") {
433
+ executorConfig.codex = {
434
+ ...(executorConfig.codex || {}),
435
+ ephemeral: parseExecutorBoolean(value, `${label}.codex.ephemeral`, filePath),
436
+ };
437
+ } else if (key === "claude.command") {
438
+ executorConfig.claude = {
439
+ ...(executorConfig.claude || {}),
440
+ command: value,
441
+ };
442
+ } else if (key === "claude.agent") {
443
+ executorConfig.claude = {
444
+ ...(executorConfig.claude || {}),
445
+ agent: value,
446
+ };
447
+ } else if (key === "claude.permission_mode") {
448
+ executorConfig.claude = {
449
+ ...(executorConfig.claude || {}),
450
+ permissionMode: value,
451
+ };
452
+ } else if (key === "claude.permission_prompt_tool") {
453
+ executorConfig.claude = {
454
+ ...(executorConfig.claude || {}),
455
+ permissionPromptTool: value,
456
+ };
457
+ } else if (key === "claude.max_turns") {
458
+ executorConfig.claude = {
459
+ ...(executorConfig.claude || {}),
460
+ maxTurns: parsePositiveExecutorInt(value, `${label}.claude.max_turns`, filePath),
461
+ };
462
+ } else if (key === "claude.mcp_config") {
463
+ executorConfig.claude = {
464
+ ...(executorConfig.claude || {}),
465
+ mcpConfig: parseExecutorStringList(value),
466
+ };
467
+ } else if (key === "claude.settings") {
468
+ executorConfig.claude = {
469
+ ...(executorConfig.claude || {}),
470
+ settings: value,
471
+ };
472
+ } else if (key === "claude.settings_json") {
473
+ executorConfig.claude = {
474
+ ...(executorConfig.claude || {}),
475
+ settingsJson: parseExecutorJsonObject(value, `${label}.claude.settings_json`, filePath),
476
+ };
477
+ } else if (key === "claude.hooks_json") {
478
+ executorConfig.claude = {
479
+ ...(executorConfig.claude || {}),
480
+ hooksJson: parseExecutorJsonObject(value, `${label}.claude.hooks_json`, filePath),
481
+ };
482
+ } else if (key === "claude.allowed_http_hook_urls") {
483
+ executorConfig.claude = {
484
+ ...(executorConfig.claude || {}),
485
+ allowedHttpHookUrls: parseExecutorStringList(value),
486
+ };
487
+ } else if (key === "claude.output_format") {
488
+ const normalizedOutputFormat = value.toLowerCase();
489
+ if (!["text", "json", "stream-json"].includes(normalizedOutputFormat)) {
490
+ throw new Error(
491
+ `Invalid ${label}.claude.output_format "${value}" in ${filePath}; expected text, json, or stream-json`,
492
+ );
493
+ }
494
+ executorConfig.claude = {
495
+ ...(executorConfig.claude || {}),
496
+ outputFormat: normalizedOutputFormat,
497
+ };
498
+ } else if (key === "claude.allowed_tools") {
499
+ executorConfig.claude = {
500
+ ...(executorConfig.claude || {}),
501
+ allowedTools: parseExecutorStringList(value),
502
+ };
503
+ } else if (key === "claude.disallowed_tools") {
504
+ executorConfig.claude = {
505
+ ...(executorConfig.claude || {}),
506
+ disallowedTools: parseExecutorStringList(value),
507
+ };
508
+ } else if (key === "opencode.command") {
509
+ executorConfig.opencode = {
510
+ ...(executorConfig.opencode || {}),
511
+ command: value,
512
+ };
513
+ } else if (key === "opencode.agent") {
514
+ executorConfig.opencode = {
515
+ ...(executorConfig.opencode || {}),
516
+ agent: value,
517
+ };
518
+ } else if (key === "opencode.attach") {
519
+ executorConfig.opencode = {
520
+ ...(executorConfig.opencode || {}),
521
+ attach: value,
522
+ };
523
+ } else if (key === "opencode.files") {
524
+ executorConfig.opencode = {
525
+ ...(executorConfig.opencode || {}),
526
+ files: parseExecutorStringList(value),
527
+ };
528
+ } else if (key === "opencode.format") {
529
+ const normalizedFormat = value.toLowerCase();
530
+ if (!["default", "json"].includes(normalizedFormat)) {
531
+ throw new Error(
532
+ `Invalid ${label}.opencode.format "${value}" in ${filePath}; expected default or json`,
533
+ );
534
+ }
535
+ executorConfig.opencode = {
536
+ ...(executorConfig.opencode || {}),
537
+ format: normalizedFormat,
538
+ };
539
+ } else if (key === "opencode.steps") {
540
+ executorConfig.opencode = {
541
+ ...(executorConfig.opencode || {}),
542
+ steps: parsePositiveExecutorInt(value, `${label}.opencode.steps`, filePath),
543
+ };
544
+ } else if (key === "opencode.instructions") {
545
+ executorConfig.opencode = {
546
+ ...(executorConfig.opencode || {}),
547
+ instructions: parseExecutorStringList(value),
548
+ };
549
+ } else if (key === "opencode.permission") {
550
+ executorConfig.opencode = {
551
+ ...(executorConfig.opencode || {}),
552
+ permission: parseExecutorJson(value, `${label}.opencode.permission`, filePath),
553
+ };
554
+ } else if (key === "opencode.config_json") {
555
+ executorConfig.opencode = {
556
+ ...(executorConfig.opencode || {}),
557
+ configJson: parseExecutorJsonObject(value, `${label}.opencode.config_json`, filePath),
558
+ };
559
+ }
560
+ }
561
+ return executorConfig;
562
+ }
563
+
564
+ export function extractExitContractFromSection(sectionText, filePath, agentId) {
565
+ const exitContractBlock = extractSectionBody(sectionText, "Exit contract", filePath, agentId, {
566
+ required: false,
567
+ });
568
+ return parseExitContractSettings(exitContractBlock, filePath, `agent ${agentId}`);
569
+ }
570
+
571
+ export function extractExecutorConfigFromSection(sectionText, filePath, agentId) {
572
+ const executorBlock = extractSectionBody(sectionText, "Executor", filePath, agentId, {
573
+ required: false,
574
+ });
575
+ return normalizeAgentExecutorConfig(
576
+ parseExecutorSettings(executorBlock, filePath, `agent ${agentId}`),
577
+ filePath,
578
+ `agent ${agentId}`,
579
+ );
580
+ }
581
+
582
+ export function extractWaveContext7Defaults(content, filePath) {
583
+ const topLevelContext7 = extractTopLevelSectionBody(content, "Context7 defaults", filePath, {
584
+ required: false,
585
+ });
586
+ return parseContext7Settings(topLevelContext7, filePath, "wave defaults");
587
+ }
588
+
589
+ export function extractWaveComponentPromotions(content, filePath) {
590
+ const block = extractTopLevelSectionBody(content, "Component promotions", filePath, {
591
+ required: false,
592
+ });
593
+ return parseComponentPromotions(block, filePath, "wave component promotions");
594
+ }
595
+
596
+ export function extractRolePromptPaths(sectionText, filePath, agentId) {
597
+ const rolePromptsBlock = extractSectionBody(sectionText, "Role prompts", filePath, agentId, {
598
+ required: false,
599
+ });
600
+ if (!rolePromptsBlock) {
601
+ return [];
602
+ }
603
+ const rolePromptPaths = [];
604
+ for (const line of rolePromptsBlock.split(/\r?\n/)) {
605
+ const trimmed = line.trim();
606
+ if (!trimmed) {
607
+ continue;
608
+ }
609
+ const bulletMatch = trimmed.match(/^-\s+(.+?)\s*$/);
610
+ if (!bulletMatch) {
611
+ throw new Error(`Malformed role prompt entry "${trimmed}" for agent ${agentId} in ${filePath}`);
612
+ }
613
+ const rolePromptPath = bulletMatch[1].replace(/[`"']/g, "").trim();
614
+ if (!rolePromptPath) {
615
+ throw new Error(`Empty role prompt entry for agent ${agentId} in ${filePath}`);
616
+ }
617
+ rolePromptPaths.push(rolePromptPath);
618
+ }
619
+ if (rolePromptPaths.length === 0) {
620
+ throw new Error(`Missing role prompt paths for agent ${agentId} in ${filePath}`);
621
+ }
622
+ return Array.from(new Set(rolePromptPaths));
623
+ }
624
+
625
+ export function extractAgentComponentsFromSection(sectionText, filePath, agentId) {
626
+ const block = extractSectionBody(sectionText, "Components", filePath, agentId, {
627
+ required: false,
628
+ });
629
+ return parseComponentList(block, filePath, `agent ${agentId} components`);
630
+ }
631
+
632
+ export function extractAgentCapabilitiesFromSection(sectionText, filePath, agentId) {
633
+ const block = extractSectionBody(sectionText, "Capabilities", filePath, agentId, {
634
+ required: false,
635
+ });
636
+ return parseComponentList(block, filePath, `agent ${agentId} capabilities`);
637
+ }
638
+
639
+ export function slugify(value) {
640
+ return value
641
+ .toLowerCase()
642
+ .replace(/[^a-z0-9]+/g, "-")
643
+ .replace(/^-+|-+$/g, "")
644
+ .slice(0, 48);
645
+ }
646
+
647
+ export function extractOwnedPaths(promptText) {
648
+ const ownedPaths = [];
649
+ let inFileOwnership = false;
650
+ for (const line of String(promptText || "").split(/\r?\n/)) {
651
+ if (/^\s*File ownership\b/i.test(line)) {
652
+ inFileOwnership = true;
653
+ continue;
654
+ }
655
+ if (inFileOwnership && /^\s*[A-Za-z][A-Za-z0-9 _/-]*:\s*$/.test(line)) {
656
+ inFileOwnership = false;
657
+ }
658
+ if (!inFileOwnership) {
659
+ continue;
660
+ }
661
+ const bulletMatch = line.match(/^\s*-\s+(.+?)\s*$/);
662
+ if (!bulletMatch) {
663
+ continue;
664
+ }
665
+ const cleaned = bulletMatch[1].replace(/[`"']/g, "").trim();
666
+ if (!cleaned) {
667
+ continue;
668
+ }
669
+ ownedPaths.push(cleaned);
670
+ }
671
+ return Array.from(new Set(ownedPaths));
672
+ }
673
+
674
+ function isRepoContainedPath(relPath) {
675
+ if (!relPath || path.isAbsolute(relPath)) {
676
+ return false;
677
+ }
678
+ const resolved = path.resolve(REPO_ROOT, relPath);
679
+ const relative = path.relative(REPO_ROOT, resolved);
680
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
681
+ }
682
+
683
+ function resolveRolePromptAbsolutePath(rolePromptPath, filePath, agentId, rolePromptDir) {
684
+ if (!isAllowedRolePromptPath(rolePromptPath, rolePromptDir)) {
685
+ throw new Error(
686
+ `Role prompt path ${rolePromptPath} for agent ${agentId} in ${filePath} must stay within ${rolePromptDir}/*.md`,
687
+ );
688
+ }
689
+ return path.resolve(REPO_ROOT, rolePromptPath);
690
+ }
691
+
692
+ export function extractStandingPromptFromRoleDoc(
693
+ rolePromptPath,
694
+ filePath,
695
+ agentId,
696
+ options = {},
697
+ ) {
698
+ const absoluteRolePromptPath = resolveRolePromptAbsolutePath(
699
+ rolePromptPath,
700
+ filePath,
701
+ agentId,
702
+ options.rolePromptDir || "docs/agents",
703
+ );
704
+ if (!fs.existsSync(absoluteRolePromptPath)) {
705
+ throw new Error(
706
+ `Missing role prompt ${rolePromptPath} for agent ${agentId} in ${filePath}`,
707
+ );
708
+ }
709
+ const rolePromptContent = fs.readFileSync(absoluteRolePromptPath, "utf8");
710
+ const standingPromptBlock = rolePromptContent.match(/## Standing prompt[\r\n]+/);
711
+ if (!standingPromptBlock) {
712
+ throw new Error(
713
+ `Missing "## Standing prompt" section in ${rolePromptPath} required by agent ${agentId} in ${filePath}`,
714
+ );
715
+ }
716
+ const afterStandingPrompt = rolePromptContent.slice(
717
+ standingPromptBlock.index + standingPromptBlock[0].length,
718
+ );
719
+ return extractFencedBlock(
720
+ afterStandingPrompt,
721
+ `Role prompt ${rolePromptPath} required by agent ${agentId} in ${filePath}`,
722
+ );
723
+ }
724
+
725
+ export function composeResolvedPrompt(rolePromptPaths, localPrompt, filePath, agentId, options = {}) {
726
+ return [
727
+ ...rolePromptPaths.map((rolePath) =>
728
+ extractStandingPromptFromRoleDoc(rolePath, filePath, agentId, options),
729
+ ),
730
+ localPrompt,
731
+ ]
732
+ .filter(Boolean)
733
+ .join("\n\n");
734
+ }
735
+
736
+ export function resolveEvaluatorReportPath(wave, options = {}) {
737
+ const evaluatorAgentId = options.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
738
+ const evaluator = wave?.agents?.find((agent) => agent.agentId === evaluatorAgentId);
739
+ if (!evaluator) {
740
+ return null;
741
+ }
742
+ return (
743
+ evaluator.ownedPaths.find((ownedPath) =>
744
+ /(?:^|\/)(?:reviews?|.*evaluator).*\.(?:md|txt)$/i.test(ownedPath),
745
+ ) ??
746
+ evaluator.ownedPaths[0] ??
747
+ null
748
+ );
749
+ }
750
+
751
+ function normalizeMatrixStringArray(values, label, filePath) {
752
+ if (!Array.isArray(values)) {
753
+ return [];
754
+ }
755
+ return values
756
+ .map((value, index) => String(value || "").trim())
757
+ .filter(Boolean)
758
+ .map((value, index) => {
759
+ if (!value) {
760
+ throw new Error(`Empty ${label}[${index}] in ${filePath}`);
761
+ }
762
+ return value;
763
+ });
764
+ }
765
+
766
+ export function loadComponentCutoverMatrix(options = {}) {
767
+ const laneProfile =
768
+ options.componentMatrixPayload !== undefined ? null : resolveLaneProfileForOptions(options);
769
+ const matrixJsonPath =
770
+ options.componentMatrixJsonPath ||
771
+ (laneProfile
772
+ ? path.resolve(REPO_ROOT, laneProfile.paths.componentCutoverMatrixJsonPath)
773
+ : "trace-bundle/component-cutover-matrix.json");
774
+ const payload =
775
+ options.componentMatrixPayload !== undefined
776
+ ? options.componentMatrixPayload
777
+ : readJsonOrNull(matrixJsonPath);
778
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
779
+ throw new Error(
780
+ `Component cutover matrix is missing or invalid: ${path.relative(REPO_ROOT, matrixJsonPath)}`,
781
+ );
782
+ }
783
+ const levels = Array.isArray(payload.levels)
784
+ ? payload.levels.map((value) => String(value || "").trim()).filter(Boolean)
785
+ : [];
786
+ if (levels.length === 0) {
787
+ throw new Error(
788
+ `Component cutover matrix must define a non-empty "levels" array in ${path.relative(REPO_ROOT, matrixJsonPath)}`,
789
+ );
790
+ }
791
+ const levelSet = new Set(levels);
792
+ const levelOrder = Object.fromEntries(levels.map((level, index) => [level, index]));
793
+ const rawComponents =
794
+ payload.components && typeof payload.components === "object" && !Array.isArray(payload.components)
795
+ ? payload.components
796
+ : null;
797
+ if (!rawComponents) {
798
+ throw new Error(
799
+ `Component cutover matrix must define a "components" object in ${path.relative(REPO_ROOT, matrixJsonPath)}`,
800
+ );
801
+ }
802
+ const components = Object.fromEntries(
803
+ Object.entries(rawComponents).map(([rawComponentId, rawComponent]) => {
804
+ const componentId = normalizeComponentId(
805
+ rawComponentId,
806
+ "component cutover matrix",
807
+ path.relative(REPO_ROOT, matrixJsonPath),
808
+ );
809
+ if (!rawComponent || typeof rawComponent !== "object" || Array.isArray(rawComponent)) {
810
+ throw new Error(
811
+ `Component "${componentId}" must be an object in ${path.relative(REPO_ROOT, matrixJsonPath)}`,
812
+ );
813
+ }
814
+ const currentLevel = String(rawComponent.currentLevel || "").trim();
815
+ if (!levelSet.has(currentLevel)) {
816
+ throw new Error(
817
+ `Component "${componentId}" has invalid currentLevel "${rawComponent.currentLevel}" in ${path.relative(REPO_ROOT, matrixJsonPath)}`,
818
+ );
819
+ }
820
+ const promotions = Array.isArray(rawComponent.promotions)
821
+ ? rawComponent.promotions.map((promotion, index) => {
822
+ if (!promotion || typeof promotion !== "object" || Array.isArray(promotion)) {
823
+ throw new Error(
824
+ `Component "${componentId}" has malformed promotion[${index}] in ${path.relative(REPO_ROOT, matrixJsonPath)}`,
825
+ );
826
+ }
827
+ const wave = Number.parseInt(String(promotion.wave), 10);
828
+ const target = String(promotion.target || "").trim();
829
+ if (!Number.isFinite(wave) || wave < 0) {
830
+ throw new Error(
831
+ `Component "${componentId}" has invalid promotion wave "${promotion.wave}" in ${path.relative(REPO_ROOT, matrixJsonPath)}`,
832
+ );
833
+ }
834
+ if (!levelSet.has(target)) {
835
+ throw new Error(
836
+ `Component "${componentId}" has invalid promotion target "${promotion.target}" in ${path.relative(REPO_ROOT, matrixJsonPath)}`,
837
+ );
838
+ }
839
+ return { wave, target };
840
+ })
841
+ : [];
842
+ return [
843
+ componentId,
844
+ {
845
+ title: String(rawComponent.title || componentId).trim() || componentId,
846
+ canonicalDocs: normalizeMatrixStringArray(
847
+ rawComponent.canonicalDocs,
848
+ `${componentId}.canonicalDocs`,
849
+ path.relative(REPO_ROOT, matrixJsonPath),
850
+ ),
851
+ currentLevel,
852
+ promotions,
853
+ proofSurfaces: normalizeMatrixStringArray(
854
+ rawComponent.proofSurfaces,
855
+ `${componentId}.proofSurfaces`,
856
+ path.relative(REPO_ROOT, matrixJsonPath),
857
+ ),
858
+ },
859
+ ];
860
+ }),
861
+ );
862
+ return {
863
+ version: Number.parseInt(String(payload.version ?? "1"), 10) || 1,
864
+ levels,
865
+ levelSet,
866
+ levelOrder,
867
+ components,
868
+ docPath:
869
+ laneProfile?.paths?.componentCutoverMatrixDocPath ??
870
+ options.componentMatrixDocPath ??
871
+ null,
872
+ jsonPath:
873
+ laneProfile?.paths?.componentCutoverMatrixJsonPath ??
874
+ options.componentMatrixJsonPath ??
875
+ "trace-bundle/component-cutover-matrix.json",
876
+ };
877
+ }
878
+
879
+ export function requiredDocumentationStewardPathsForWave(waveNumber, options = {}) {
880
+ const laneProfile = resolveLaneProfileForOptions(options);
881
+ const out = [...laneProfile.sharedPlanDocs];
882
+ const componentThreshold = laneProfile.validation.requireComponentPromotionsFromWave;
883
+ if (componentThreshold !== null && waveNumber >= componentThreshold) {
884
+ out.push(
885
+ laneProfile.paths.componentCutoverMatrixDocPath,
886
+ laneProfile.paths.componentCutoverMatrixJsonPath,
887
+ );
888
+ }
889
+ return Array.from(new Set(out));
890
+ }
891
+
892
+ export function validateWaveDefinition(wave, options = {}) {
893
+ const laneProfile = resolveLaneProfileForOptions(options);
894
+ const lane = laneProfile.lane;
895
+ const evaluatorAgentId = laneProfile.roles.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
896
+ const integrationAgentId =
897
+ laneProfile.roles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
898
+ const documentationAgentId =
899
+ laneProfile.roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
900
+ const documentationThreshold = laneProfile.validation.requireDocumentationStewardFromWave;
901
+ const context7Threshold = laneProfile.validation.requireContext7DeclarationsFromWave;
902
+ const exitContractThreshold = laneProfile.validation.requireExitContractsFromWave;
903
+ const integrationThreshold = laneProfile.validation.requireIntegrationStewardFromWave;
904
+ const componentPromotionThreshold =
905
+ laneProfile.validation.requireComponentPromotionsFromWave;
906
+ const agentComponentsThreshold = laneProfile.validation.requireAgentComponentsFromWave;
907
+ const componentPromotionRuleActive =
908
+ componentPromotionThreshold !== null && wave.wave >= componentPromotionThreshold;
909
+ const agentComponentsRuleActive =
910
+ agentComponentsThreshold !== null && wave.wave >= agentComponentsThreshold;
911
+ const integrationRuleActive =
912
+ integrationThreshold !== null && wave.wave >= integrationThreshold;
913
+ const errors = [];
914
+ const promotedComponents = new Map(
915
+ Array.isArray(wave.componentPromotions)
916
+ ? wave.componentPromotions.map((promotion) => [promotion.componentId, promotion.targetLevel])
917
+ : [],
918
+ );
919
+ const componentOwners = new Map(
920
+ Array.from(promotedComponents.keys()).map((componentId) => [componentId, new Set()]),
921
+ );
922
+ let componentMatrix = null;
923
+ if (componentPromotionRuleActive || promotedComponents.size > 0 || agentComponentsRuleActive) {
924
+ try {
925
+ componentMatrix = loadComponentCutoverMatrix({ laneProfile });
926
+ } catch (error) {
927
+ errors.push(error.message);
928
+ }
929
+ }
930
+ const agentIds = wave.agents.map((agent) => agent.agentId);
931
+ const duplicateAgentIds = agentIds.filter(
932
+ (agentId, index) => agentIds.indexOf(agentId) !== index,
933
+ );
934
+ if (wave.agents.length === 0) {
935
+ errors.push("must declare at least one agent");
936
+ }
937
+ if (duplicateAgentIds.length > 0) {
938
+ errors.push(`must not repeat agent ids (${Array.from(new Set(duplicateAgentIds)).join(", ")})`);
939
+ }
940
+ if (!wave.agents.some((agent) => agent.agentId === evaluatorAgentId)) {
941
+ errors.push(`must include Agent ${evaluatorAgentId} as the running evaluator`);
942
+ }
943
+ if (componentPromotionRuleActive && promotedComponents.size === 0) {
944
+ errors.push(
945
+ `Wave ${wave.wave} must declare a ## Component promotions section in waves ${componentPromotionThreshold} and later`,
946
+ );
947
+ }
948
+ if (componentMatrix) {
949
+ for (const [componentId, targetLevel] of promotedComponents.entries()) {
950
+ const component = componentMatrix.components[componentId];
951
+ if (!component) {
952
+ errors.push(
953
+ `Wave ${wave.wave} references unknown component "${componentId}" from ${componentMatrix.jsonPath}`,
954
+ );
955
+ continue;
956
+ }
957
+ if (!componentMatrix.levelSet.has(targetLevel)) {
958
+ errors.push(
959
+ `Wave ${wave.wave} uses invalid component level "${targetLevel}" for ${componentId}`,
960
+ );
961
+ continue;
962
+ }
963
+ const matrixPromotion = component.promotions.find((promotion) => promotion.wave === wave.wave);
964
+ if (!matrixPromotion) {
965
+ errors.push(
966
+ `Component "${componentId}" is missing a wave ${wave.wave} promotion entry in ${componentMatrix.jsonPath}`,
967
+ );
968
+ } else if (matrixPromotion.target !== targetLevel) {
969
+ errors.push(
970
+ `Wave ${wave.wave} promotes ${componentId} to ${targetLevel}, but ${componentMatrix.jsonPath} declares ${matrixPromotion.target}`,
971
+ );
972
+ } else if (componentMatrix.levelOrder[targetLevel] < componentMatrix.levelOrder[component.currentLevel]) {
973
+ errors.push(
974
+ `Wave ${wave.wave} promotes ${componentId} to ${targetLevel}, but ${componentMatrix.jsonPath} already records currentLevel ${component.currentLevel}`,
975
+ );
976
+ }
977
+ }
978
+ const matrixWavePromotions = Object.entries(componentMatrix.components)
979
+ .flatMap(([componentId, component]) =>
980
+ component.promotions
981
+ .filter((promotion) => promotion.wave === wave.wave)
982
+ .map((promotion) => ({ componentId, targetLevel: promotion.target })),
983
+ );
984
+ for (const promotion of matrixWavePromotions) {
985
+ if (promotedComponents.get(promotion.componentId) !== promotion.targetLevel) {
986
+ errors.push(
987
+ `Wave ${wave.wave} must declare component promotion ${promotion.componentId}: ${promotion.targetLevel} to match ${componentMatrix.jsonPath}`,
988
+ );
989
+ }
990
+ }
991
+ }
992
+ for (const agent of wave.agents) {
993
+ if (!Array.isArray(agent.ownedPaths) || agent.ownedPaths.length === 0) {
994
+ errors.push(`Agent ${agent.agentId} must declare at least one owned path`);
995
+ continue;
996
+ }
997
+ const unsafeOwnedPaths = agent.ownedPaths.filter(
998
+ (ownedPath) => !isRepoContainedPath(ownedPath),
999
+ );
1000
+ if (unsafeOwnedPaths.length > 0) {
1001
+ errors.push(
1002
+ `Agent ${agent.agentId} has non-repo-owned paths (${unsafeOwnedPaths.join(", ")})`,
1003
+ );
1004
+ }
1005
+ const invalidRolePromptPaths = Array.isArray(agent.rolePromptPaths)
1006
+ ? agent.rolePromptPaths.filter(
1007
+ (rolePromptPath) =>
1008
+ !isAllowedRolePromptPath(rolePromptPath, laneProfile.roles.rolePromptDir),
1009
+ )
1010
+ : [];
1011
+ if (invalidRolePromptPaths.length > 0) {
1012
+ errors.push(
1013
+ `Agent ${agent.agentId} has invalid role prompt paths (${invalidRolePromptPaths.join(", ")})`,
1014
+ );
1015
+ }
1016
+ if (
1017
+ agent.executorConfig?.id === "claude" &&
1018
+ (agent.executorConfig?.codex || agent.executorConfig?.opencode)
1019
+ ) {
1020
+ errors.push(`Agent ${agent.agentId} declares executor=claude but includes non-Claude overrides`);
1021
+ }
1022
+ if (
1023
+ agent.executorConfig?.id === "opencode" &&
1024
+ (agent.executorConfig?.codex || agent.executorConfig?.claude)
1025
+ ) {
1026
+ errors.push(
1027
+ `Agent ${agent.agentId} declares executor=opencode but includes non-OpenCode overrides`,
1028
+ );
1029
+ }
1030
+ if (
1031
+ agent.executorConfig?.id === "codex" &&
1032
+ (agent.executorConfig?.claude || agent.executorConfig?.opencode)
1033
+ ) {
1034
+ errors.push(`Agent ${agent.agentId} declares executor=codex but includes non-Codex overrides`);
1035
+ }
1036
+ if (
1037
+ agent.executorConfig?.id === "local" &&
1038
+ (agent.executorConfig?.codex || agent.executorConfig?.claude || agent.executorConfig?.opencode)
1039
+ ) {
1040
+ errors.push(`Agent ${agent.agentId} declares executor=local but includes vendor overrides`);
1041
+ }
1042
+ if (context7Threshold !== null && wave.wave >= context7Threshold) {
1043
+ if (!agent.context7Config) {
1044
+ errors.push(
1045
+ `Agent ${agent.agentId} must declare a ### Context7 section in waves ${context7Threshold} and later`,
1046
+ );
1047
+ }
1048
+ }
1049
+ if ([evaluatorAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId)) {
1050
+ if (Array.isArray(agent.components) && agent.components.length > 0) {
1051
+ errors.push(`Agent ${agent.agentId} must not declare a ### Components section`);
1052
+ }
1053
+ } else {
1054
+ if (agentComponentsRuleActive && (!Array.isArray(agent.components) || agent.components.length === 0)) {
1055
+ errors.push(
1056
+ `Agent ${agent.agentId} must declare a ### Components section in waves ${agentComponentsThreshold} and later`,
1057
+ );
1058
+ }
1059
+ for (const componentId of agent.components || []) {
1060
+ if (componentMatrix && !componentMatrix.components[componentId]) {
1061
+ errors.push(
1062
+ `Agent ${agent.agentId} references unknown component "${componentId}" from ${componentMatrix.jsonPath}`,
1063
+ );
1064
+ continue;
1065
+ }
1066
+ if (!promotedComponents.has(componentId)) {
1067
+ errors.push(
1068
+ `Agent ${agent.agentId} declares component "${componentId}" that is not promoted in wave ${wave.wave}`,
1069
+ );
1070
+ continue;
1071
+ }
1072
+ componentOwners.get(componentId)?.add(agent.agentId);
1073
+ }
1074
+ }
1075
+ if (exitContractThreshold !== null && wave.wave >= exitContractThreshold) {
1076
+ if (![evaluatorAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId)) {
1077
+ if (!agent.exitContract) {
1078
+ errors.push(
1079
+ `Agent ${agent.agentId} must declare a ### Exit contract section in waves ${exitContractThreshold} and later`,
1080
+ );
1081
+ } else {
1082
+ const exitContractErrors = validateExitContractShape(agent.exitContract);
1083
+ if (exitContractErrors.length > 0) {
1084
+ errors.push(
1085
+ `Agent ${agent.agentId} has invalid exit contract (${exitContractErrors.join(", ")})`,
1086
+ );
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+ }
1092
+ for (const agent of wave.agents) {
1093
+ for (const requiredRef of laneProfile.validation.requiredPromptReferences) {
1094
+ if (!agent.prompt.includes(requiredRef)) {
1095
+ errors.push(`Agent ${agent.agentId} must reference ${requiredRef}`);
1096
+ }
1097
+ }
1098
+ }
1099
+ const evaluator = wave.agents.find((agent) => agent.agentId === evaluatorAgentId);
1100
+ if (!evaluator?.rolePromptPaths?.includes(laneProfile.roles.evaluatorRolePromptPath)) {
1101
+ errors.push(
1102
+ `Agent ${evaluatorAgentId} must import ${laneProfile.roles.evaluatorRolePromptPath}`,
1103
+ );
1104
+ }
1105
+ if (!resolveEvaluatorReportPath(wave, { evaluatorAgentId })) {
1106
+ errors.push(`Agent ${evaluatorAgentId} must own an evaluator report path`);
1107
+ }
1108
+ if (integrationRuleActive) {
1109
+ const integrationStewards = wave.agents.filter((agent) =>
1110
+ agent.rolePromptPaths?.includes(laneProfile.roles.integrationRolePromptPath),
1111
+ );
1112
+ if (integrationStewards.length !== 1) {
1113
+ errors.push(
1114
+ `Wave ${wave.wave} must include exactly one integration steward importing ${laneProfile.roles.integrationRolePromptPath}`,
1115
+ );
1116
+ }
1117
+ }
1118
+ const documentationRuleActive =
1119
+ (documentationThreshold !== null && wave.wave >= documentationThreshold) ||
1120
+ componentPromotionRuleActive;
1121
+ if (documentationRuleActive) {
1122
+ const documentationStewards = wave.agents.filter((agent) =>
1123
+ agent.rolePromptPaths?.includes(laneProfile.roles.documentationRolePromptPath),
1124
+ );
1125
+ if (documentationStewards.length !== 1) {
1126
+ errors.push(
1127
+ `Wave ${wave.wave} must include exactly one documentation steward importing ${laneProfile.roles.documentationRolePromptPath}`,
1128
+ );
1129
+ } else {
1130
+ const documentationSteward = documentationStewards[0];
1131
+ const requiredDocPaths = requiredDocumentationStewardPathsForWave(wave.wave, { laneProfile });
1132
+ const missingSharedPlanDocs = requiredDocPaths.filter(
1133
+ (docPath) => !documentationSteward.ownedPaths.includes(docPath),
1134
+ );
1135
+ if (missingSharedPlanDocs.length > 0) {
1136
+ errors.push(
1137
+ `Documentation steward ${documentationSteward.agentId} must own ${missingSharedPlanDocs.join(", ")}`,
1138
+ );
1139
+ }
1140
+ const sharedPlanDocOwners = wave.agents.filter(
1141
+ (agent) =>
1142
+ agent.agentId !== documentationSteward.agentId &&
1143
+ agent.ownedPaths.some((ownedPath) => requiredDocPaths.includes(ownedPath)),
1144
+ );
1145
+ if (sharedPlanDocOwners.length > 0) {
1146
+ errors.push(
1147
+ `Shared plan docs must be owned only by ${documentationSteward.agentId} (also owned by ${sharedPlanDocOwners.map((agent) => agent.agentId).join(", ")})`,
1148
+ );
1149
+ }
1150
+ }
1151
+ }
1152
+ if (context7Threshold !== null && wave.wave >= context7Threshold && !wave.context7Defaults) {
1153
+ errors.push(`Waves ${context7Threshold} and later must declare a ## Context7 defaults section`);
1154
+ }
1155
+ const runtimeMixValidation = validateWaveRuntimeMixAssignments(wave, { laneProfile });
1156
+ if (!runtimeMixValidation.ok) {
1157
+ errors.push(
1158
+ `Wave ${wave.wave} exceeds lane runtime mix targets (${runtimeMixValidation.detail})`,
1159
+ );
1160
+ }
1161
+ for (const [componentId, owners] of componentOwners.entries()) {
1162
+ if (owners.size === 0) {
1163
+ errors.push(
1164
+ `Wave ${wave.wave} must assign promoted component "${componentId}" to at least one non-${evaluatorAgentId}/${documentationAgentId} agent`,
1165
+ );
1166
+ }
1167
+ }
1168
+ if (errors.length > 0) {
1169
+ throw new Error(`Invalid wave ${wave.wave} (${wave.file}): ${errors.join("; ")}`);
1170
+ }
1171
+ return wave;
1172
+ }
1173
+
1174
+ export function parseWaveContent(content, filePath, options = {}) {
1175
+ const laneProfile = resolveLaneProfileForOptions(options);
1176
+ const fileName = path.basename(filePath);
1177
+ const waveNumber = waveNumberFromFileName(fileName);
1178
+ const commitMessageMatch = content.match(/\*\*Commit message\*\*:\s*`([^`]+)`/);
1179
+ const agentHeaders = [];
1180
+ const headerRegex = /^## Agent ([^:]+):\s*(.+)$/gm;
1181
+ let match = headerRegex.exec(content);
1182
+ while (match !== null) {
1183
+ agentHeaders.push({
1184
+ agentId: match[1].trim(),
1185
+ title: match[2].trim(),
1186
+ startIndex: match.index,
1187
+ headerLength: match[0].length,
1188
+ });
1189
+ match = headerRegex.exec(content);
1190
+ }
1191
+
1192
+ const agents = [];
1193
+ for (let i = 0; i < agentHeaders.length; i += 1) {
1194
+ const current = agentHeaders[i];
1195
+ const next = agentHeaders[i + 1];
1196
+ const sectionStart = current.startIndex + current.headerLength;
1197
+ const sectionEnd = next ? next.startIndex : content.length;
1198
+ const sectionText = content.slice(sectionStart, sectionEnd);
1199
+ const rolePromptPaths = extractRolePromptPaths(sectionText, filePath, current.agentId);
1200
+ const context7Config = extractContext7ConfigFromSection(sectionText, filePath, current.agentId);
1201
+ const exitContract = extractExitContractFromSection(sectionText, filePath, current.agentId);
1202
+ const executorConfig = extractExecutorConfigFromSection(sectionText, filePath, current.agentId);
1203
+ const components = extractAgentComponentsFromSection(sectionText, filePath, current.agentId);
1204
+ const capabilities = extractAgentCapabilitiesFromSection(sectionText, filePath, current.agentId);
1205
+ const promptOverlay = extractPromptFromSection(sectionText, filePath, current.agentId);
1206
+ const prompt = composeResolvedPrompt(
1207
+ rolePromptPaths,
1208
+ promptOverlay,
1209
+ filePath,
1210
+ current.agentId,
1211
+ {
1212
+ rolePromptDir: laneProfile.roles.rolePromptDir,
1213
+ },
1214
+ );
1215
+ const ownedPaths = extractOwnedPaths(promptOverlay);
1216
+ agents.push({
1217
+ agentId: current.agentId,
1218
+ title: current.title,
1219
+ slug: slugify(`${waveNumber}-${current.agentId}`),
1220
+ prompt,
1221
+ promptOverlay,
1222
+ rolePromptPaths,
1223
+ context7Config,
1224
+ exitContract,
1225
+ executorConfig,
1226
+ components,
1227
+ capabilities,
1228
+ ownedPaths,
1229
+ });
1230
+ }
1231
+
1232
+ const componentPromotions = extractWaveComponentPromotions(content, filePath);
1233
+ const componentTargetById = Object.fromEntries(
1234
+ componentPromotions.map((promotion) => [promotion.componentId, promotion.targetLevel]),
1235
+ );
1236
+ const agentsWithComponentTargets = agents.map((agent) => ({
1237
+ ...agent,
1238
+ componentTargets: Object.fromEntries(
1239
+ (agent.components || []).map((componentId) => [componentId, componentTargetById[componentId] || null]),
1240
+ ),
1241
+ }));
1242
+
1243
+ return {
1244
+ wave: waveNumber,
1245
+ file: path.relative(REPO_ROOT, filePath),
1246
+ commitMessage: commitMessageMatch ? commitMessageMatch[1] : null,
1247
+ context7Defaults: extractWaveContext7Defaults(content, filePath),
1248
+ componentPromotions,
1249
+ agents: agentsWithComponentTargets,
1250
+ evaluatorReportPath: resolveEvaluatorReportPath(
1251
+ { agents: agentsWithComponentTargets },
1252
+ { evaluatorAgentId: laneProfile.roles.evaluatorAgentId },
1253
+ ),
1254
+ };
1255
+ }
1256
+
1257
+ function cloneExecutorValue(value) {
1258
+ return value ? JSON.parse(JSON.stringify(value)) : value;
1259
+ }
1260
+
1261
+ function mergeUniqueStringArrays(...lists) {
1262
+ return Array.from(
1263
+ new Set(
1264
+ lists.flatMap((list) => (Array.isArray(list) ? list.map((entry) => String(entry || "").trim()) : []))
1265
+ .filter(Boolean),
1266
+ ),
1267
+ );
1268
+ }
1269
+
1270
+ function mergeExecutorSections(baseSection, profileSection, inlineSection, arrayKeys = []) {
1271
+ const merged = {
1272
+ ...(cloneExecutorValue(baseSection) || {}),
1273
+ ...(cloneExecutorValue(profileSection) || {}),
1274
+ ...(cloneExecutorValue(inlineSection) || {}),
1275
+ };
1276
+ for (const key of arrayKeys) {
1277
+ const mergedArray = mergeUniqueStringArrays(
1278
+ baseSection?.[key],
1279
+ profileSection?.[key],
1280
+ inlineSection?.[key],
1281
+ );
1282
+ if (mergedArray.length > 0) {
1283
+ merged[key] = mergedArray;
1284
+ }
1285
+ }
1286
+ return merged;
1287
+ }
1288
+
1289
+ function inferAgentRuntimeRole(agent, laneProfile) {
1290
+ if (agent?.agentId === laneProfile.roles.evaluatorAgentId) {
1291
+ return "evaluator";
1292
+ }
1293
+ if (agent?.agentId === laneProfile.roles.integrationAgentId) {
1294
+ return "integration";
1295
+ }
1296
+ if (agent?.agentId === laneProfile.roles.documentationAgentId) {
1297
+ return "documentation";
1298
+ }
1299
+ const capabilities = Array.isArray(agent?.capabilities)
1300
+ ? agent.capabilities.map((entry) => String(entry || "").trim().toLowerCase())
1301
+ : [];
1302
+ const title = String(agent?.title || "").trim().toLowerCase();
1303
+ if (capabilities.some((capability) => capability.startsWith("infra")) || /\binfra\b/.test(title)) {
1304
+ return "infra";
1305
+ }
1306
+ if (capabilities.some((capability) => capability.startsWith("deploy")) || /\bdeploy\b/.test(title)) {
1307
+ return "deploy";
1308
+ }
1309
+ if (capabilities.some((capability) => capability.startsWith("research")) || /\bresearch\b/.test(title)) {
1310
+ return "research";
1311
+ }
1312
+ return "implementation";
1313
+ }
1314
+
1315
+ export function validateWaveRuntimeMixAssignments(wave, options = {}) {
1316
+ const laneProfile = resolveLaneProfileForOptions(options);
1317
+ const targets = laneProfile.runtimePolicy?.runtimeMixTargets || {};
1318
+ if (Object.keys(targets).length === 0) {
1319
+ return {
1320
+ ok: true,
1321
+ statusCode: "pass",
1322
+ detail: "No runtime mix targets configured.",
1323
+ counts: {},
1324
+ targets,
1325
+ };
1326
+ }
1327
+ const counts = {};
1328
+ for (const agent of wave.agents || []) {
1329
+ const executorId =
1330
+ agent?.executorResolved?.id ||
1331
+ options.executorMode ||
1332
+ laneProfile.executors.default;
1333
+ counts[executorId] = (counts[executorId] || 0) + 1;
1334
+ }
1335
+ const violations = Object.entries(counts).filter(
1336
+ ([executorId, count]) =>
1337
+ Number.isFinite(targets[executorId]) && count > Number(targets[executorId]),
1338
+ );
1339
+ if (violations.length > 0) {
1340
+ return {
1341
+ ok: false,
1342
+ statusCode: "runtime-mix-exceeded",
1343
+ detail: violations
1344
+ .map(
1345
+ ([executorId, count]) =>
1346
+ `${executorId}=${count} exceeds target ${targets[executorId]}`,
1347
+ )
1348
+ .join("; "),
1349
+ counts,
1350
+ targets,
1351
+ };
1352
+ }
1353
+ return {
1354
+ ok: true,
1355
+ statusCode: "pass",
1356
+ detail: "Runtime mix assignments are within configured targets.",
1357
+ counts,
1358
+ targets,
1359
+ };
1360
+ }
1361
+
1362
+ export function resolveAgentExecutor(agent, options = {}) {
1363
+ const laneProfile = resolveLaneProfileForOptions(options);
1364
+ const executorConfig = agent?.executorConfig || null;
1365
+ const role = inferAgentRuntimeRole(agent, laneProfile);
1366
+ const profileName = executorConfig?.profile || null;
1367
+ if (profileName && !laneProfile.executors.profiles?.[profileName]) {
1368
+ throw new Error(
1369
+ `Agent ${agent?.agentId || "unknown"} references unknown executor profile "${profileName}"`,
1370
+ );
1371
+ }
1372
+ const profile =
1373
+ profileName && laneProfile.executors.profiles?.[profileName]
1374
+ ? laneProfile.executors.profiles[profileName]
1375
+ : null;
1376
+ const selectedBy = executorConfig?.id
1377
+ ? "agent-id"
1378
+ : profile?.id
1379
+ ? "agent-profile"
1380
+ : laneProfile.runtimePolicy?.defaultExecutorByRole?.[role]
1381
+ ? "lane-role-default"
1382
+ : options.executorMode
1383
+ ? "cli-default"
1384
+ : "lane-default";
1385
+ const executorId = normalizeExecutorMode(
1386
+ executorConfig?.id ||
1387
+ profile?.id ||
1388
+ laneProfile.runtimePolicy?.defaultExecutorByRole?.[role] ||
1389
+ options.executorMode ||
1390
+ laneProfile.executors.default,
1391
+ `agent ${agent?.agentId || "unknown"} executor`,
1392
+ );
1393
+ const resolvedModel =
1394
+ executorConfig?.model ||
1395
+ profile?.model ||
1396
+ (executorId === "claude"
1397
+ ? laneProfile.executors.claude.model
1398
+ : executorId === "opencode"
1399
+ ? laneProfile.executors.opencode.model
1400
+ : null);
1401
+ const fallbacks = mergeUniqueStringArrays(
1402
+ profile?.fallbacks,
1403
+ executorConfig?.fallbacks,
1404
+ );
1405
+ const runtimeFallbacks =
1406
+ fallbacks.length > 0
1407
+ ? fallbacks
1408
+ : (laneProfile.runtimePolicy?.fallbackExecutorOrder || []).filter(
1409
+ (candidate) => candidate !== executorId,
1410
+ );
1411
+ const runtimeTags = mergeUniqueStringArrays(profile?.tags, executorConfig?.tags);
1412
+ const runtimeBudget = {
1413
+ turns:
1414
+ executorConfig?.budget?.turns ??
1415
+ profile?.budget?.turns ??
1416
+ null,
1417
+ minutes:
1418
+ executorConfig?.budget?.minutes ??
1419
+ profile?.budget?.minutes ??
1420
+ null,
1421
+ };
1422
+ return {
1423
+ id: executorId,
1424
+ initialExecutorId: executorId,
1425
+ model: resolvedModel || null,
1426
+ role,
1427
+ profile: profileName,
1428
+ selectedBy,
1429
+ fallbacks: runtimeFallbacks,
1430
+ tags: runtimeTags,
1431
+ budget:
1432
+ runtimeBudget.turns !== null || runtimeBudget.minutes !== null ? runtimeBudget : null,
1433
+ fallbackUsed: false,
1434
+ fallbackReason: null,
1435
+ executorHistory: [{ attempt: 0, executorId, reason: "initial" }],
1436
+ codex: {
1437
+ ...mergeExecutorSections(
1438
+ laneProfile.executors.codex,
1439
+ profile?.codex,
1440
+ executorConfig?.codex,
1441
+ ["config", "images", "addDirs"],
1442
+ ),
1443
+ command:
1444
+ executorConfig?.codex?.command ||
1445
+ profile?.codex?.command ||
1446
+ laneProfile.executors.codex.command,
1447
+ sandbox:
1448
+ executorConfig?.codex?.sandbox ||
1449
+ profile?.codex?.sandbox ||
1450
+ (executorId === "codex"
1451
+ ? normalizeCodexSandboxMode(
1452
+ options.codexSandboxMode || laneProfile.executors.codex.sandbox,
1453
+ "executor.codex.sandbox",
1454
+ )
1455
+ : laneProfile.executors.codex.sandbox || DEFAULT_CODEX_SANDBOX_MODE),
1456
+ profileName:
1457
+ executorConfig?.codex?.profileName ||
1458
+ profile?.codex?.profileName ||
1459
+ laneProfile.executors.codex.profileName,
1460
+ config: mergeUniqueStringArrays(
1461
+ laneProfile.executors.codex.config,
1462
+ profile?.codex?.config,
1463
+ executorConfig?.codex?.config,
1464
+ ),
1465
+ search:
1466
+ executorConfig?.codex?.search ??
1467
+ profile?.codex?.search ??
1468
+ laneProfile.executors.codex.search,
1469
+ images: mergeUniqueStringArrays(
1470
+ laneProfile.executors.codex.images,
1471
+ profile?.codex?.images,
1472
+ executorConfig?.codex?.images,
1473
+ ),
1474
+ addDirs: mergeUniqueStringArrays(
1475
+ laneProfile.executors.codex.addDirs,
1476
+ profile?.codex?.addDirs,
1477
+ executorConfig?.codex?.addDirs,
1478
+ ),
1479
+ json:
1480
+ executorConfig?.codex?.json ??
1481
+ profile?.codex?.json ??
1482
+ laneProfile.executors.codex.json,
1483
+ ephemeral:
1484
+ executorConfig?.codex?.ephemeral ??
1485
+ profile?.codex?.ephemeral ??
1486
+ laneProfile.executors.codex.ephemeral,
1487
+ },
1488
+ claude: {
1489
+ ...mergeExecutorSections(
1490
+ laneProfile.executors.claude,
1491
+ profile?.claude,
1492
+ executorConfig?.claude,
1493
+ ["mcpConfig", "allowedTools", "disallowedTools", "allowedHttpHookUrls"],
1494
+ ),
1495
+ model:
1496
+ executorId === "claude"
1497
+ ? resolvedModel || laneProfile.executors.claude.model
1498
+ : laneProfile.executors.claude.model,
1499
+ maxTurns:
1500
+ executorConfig?.claude?.maxTurns ??
1501
+ profile?.claude?.maxTurns ??
1502
+ runtimeBudget.turns ??
1503
+ laneProfile.executors.claude.maxTurns,
1504
+ },
1505
+ opencode: {
1506
+ ...mergeExecutorSections(
1507
+ laneProfile.executors.opencode,
1508
+ profile?.opencode,
1509
+ executorConfig?.opencode,
1510
+ ["instructions", "files"],
1511
+ ),
1512
+ model:
1513
+ executorId === "opencode"
1514
+ ? resolvedModel || laneProfile.executors.opencode.model
1515
+ : laneProfile.executors.opencode.model,
1516
+ steps:
1517
+ executorConfig?.opencode?.steps ??
1518
+ profile?.opencode?.steps ??
1519
+ runtimeBudget.turns ??
1520
+ laneProfile.executors.opencode.steps,
1521
+ },
1522
+ };
1523
+ }
1524
+
1525
+ export function applyExecutorSelectionsToWave(wave, options = {}) {
1526
+ return {
1527
+ ...wave,
1528
+ agents: wave.agents.map((agent) => ({
1529
+ ...agent,
1530
+ executorResolved: resolveAgentExecutor(agent, options),
1531
+ })),
1532
+ };
1533
+ }
1534
+
1535
+ export function parseWaveFile(filePath, options = {}) {
1536
+ return parseWaveContent(fs.readFileSync(filePath, "utf8"), filePath, options);
1537
+ }
1538
+
1539
+ export function parseWaveFiles(wavesDir, options = {}) {
1540
+ if (!fs.existsSync(wavesDir)) {
1541
+ throw new Error(`Waves directory not found: ${path.relative(REPO_ROOT, wavesDir)}`);
1542
+ }
1543
+ const files = fs
1544
+ .readdirSync(wavesDir)
1545
+ .filter((fileName) => /^wave-\d+\.md$/.test(fileName))
1546
+ .toSorted((a, b) => waveNumberFromFileName(a) - waveNumberFromFileName(b));
1547
+ if (files.length === 0) {
1548
+ throw new Error(`No wave files found in ${path.relative(REPO_ROOT, wavesDir)}`);
1549
+ }
1550
+ return files.map((fileName) => parseWaveFile(path.join(wavesDir, fileName), options));
1551
+ }
1552
+
1553
+ export function buildManifest(lanePaths, waves) {
1554
+ const docs = walkFiles(lanePaths.docsDir)
1555
+ .map((fullPath) => {
1556
+ const data = fs.readFileSync(fullPath, "utf8");
1557
+ return {
1558
+ path: path.relative(REPO_ROOT, fullPath),
1559
+ bytes: Buffer.byteLength(data, "utf8"),
1560
+ sha256: crypto.createHash("sha256").update(data).digest("hex"),
1561
+ };
1562
+ })
1563
+ .toSorted((a, b) => a.path.localeCompare(b.path));
1564
+
1565
+ return {
1566
+ generatedAt: new Date().toISOString(),
1567
+ source: `${path.relative(REPO_ROOT, lanePaths.docsDir).replaceAll(path.sep, "/")}/**/*`,
1568
+ waves,
1569
+ docs,
1570
+ };
1571
+ }
1572
+
1573
+ export function validateWaveComponentPromotions(wave, summariesByAgentId = {}, options = {}) {
1574
+ const laneProfile = resolveLaneProfileForOptions(options);
1575
+ const componentThreshold = laneProfile.validation.requireComponentPromotionsFromWave;
1576
+ if (componentThreshold === null || wave.wave < componentThreshold) {
1577
+ return {
1578
+ ok: true,
1579
+ statusCode: "pass",
1580
+ detail: "Component promotion gate is not active for this wave.",
1581
+ componentId: null,
1582
+ };
1583
+ }
1584
+ const promotions = Array.isArray(wave.componentPromotions) ? wave.componentPromotions : [];
1585
+ if (promotions.length === 0) {
1586
+ return {
1587
+ ok: false,
1588
+ statusCode: "missing-component-promotions",
1589
+ detail: `Wave ${wave.wave} is missing component promotions.`,
1590
+ componentId: null,
1591
+ };
1592
+ }
1593
+ const roles = laneProfile.roles || {};
1594
+ const evaluatorAgentId = roles.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
1595
+ const documentationAgentId =
1596
+ roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
1597
+ const satisfied = new Set();
1598
+ for (const agent of wave.agents) {
1599
+ if ([evaluatorAgentId, documentationAgentId].includes(agent.agentId)) {
1600
+ continue;
1601
+ }
1602
+ const summary = summariesByAgentId[agent.agentId] || null;
1603
+ const markers = new Map(
1604
+ Array.isArray(summary?.components)
1605
+ ? summary.components.map((component) => [component.componentId, component])
1606
+ : [],
1607
+ );
1608
+ for (const componentId of agent.components || []) {
1609
+ const expectedLevel = agent.componentTargets?.[componentId] || null;
1610
+ const marker = markers.get(componentId);
1611
+ if (marker && marker.state === "met" && (!expectedLevel || marker.level === expectedLevel)) {
1612
+ satisfied.add(componentId);
1613
+ }
1614
+ }
1615
+ }
1616
+ for (const promotion of promotions) {
1617
+ if (!satisfied.has(promotion.componentId)) {
1618
+ return {
1619
+ ok: false,
1620
+ statusCode: "component-promotion-gap",
1621
+ detail: `Wave ${wave.wave} does not yet prove component ${promotion.componentId} at ${promotion.targetLevel}.`,
1622
+ componentId: promotion.componentId,
1623
+ };
1624
+ }
1625
+ }
1626
+ return {
1627
+ ok: true,
1628
+ statusCode: "pass",
1629
+ detail: "All promoted components are proven at the declared level.",
1630
+ componentId: null,
1631
+ };
1632
+ }
1633
+
1634
+ export function validateWaveComponentMatrixCurrentLevels(wave, options = {}) {
1635
+ const laneProfile = resolveLaneProfileForOptions(options);
1636
+ const componentThreshold = laneProfile.validation.requireComponentPromotionsFromWave;
1637
+ const promotions = Array.isArray(wave.componentPromotions) ? wave.componentPromotions : [];
1638
+ if (
1639
+ promotions.length === 0 &&
1640
+ (componentThreshold === null || wave.wave < componentThreshold)
1641
+ ) {
1642
+ return {
1643
+ ok: true,
1644
+ statusCode: "pass",
1645
+ detail: "Component current-level gate is not active for this wave.",
1646
+ componentId: null,
1647
+ };
1648
+ }
1649
+
1650
+ const componentMatrix = loadComponentCutoverMatrix({
1651
+ laneProfile,
1652
+ componentMatrixPayload: options.componentMatrixPayload,
1653
+ componentMatrixJsonPath: options.componentMatrixJsonPath,
1654
+ });
1655
+ for (const promotion of promotions) {
1656
+ const component = componentMatrix.components[promotion.componentId];
1657
+ if (!component) {
1658
+ return {
1659
+ ok: false,
1660
+ statusCode: "unknown-component",
1661
+ detail: `Wave ${wave.wave} references unknown component ${promotion.componentId}.`,
1662
+ componentId: promotion.componentId,
1663
+ };
1664
+ }
1665
+ if (component.currentLevel !== promotion.targetLevel) {
1666
+ return {
1667
+ ok: false,
1668
+ statusCode: "component-current-level-stale",
1669
+ detail: `Component ${promotion.componentId} is still recorded at ${component.currentLevel} in ${componentMatrix.jsonPath}; expected ${promotion.targetLevel}.`,
1670
+ componentId: promotion.componentId,
1671
+ };
1672
+ }
1673
+ }
1674
+
1675
+ return {
1676
+ ok: true,
1677
+ statusCode: "pass",
1678
+ detail: "Component matrix current levels match the promoted targets.",
1679
+ componentId: null,
1680
+ };
1681
+ }
1682
+
1683
+ export function writeManifest(manifestPath, manifest) {
1684
+ writeJsonAtomic(manifestPath, manifest);
1685
+ }
1686
+
1687
+ export function normalizeCompletedWaves(values) {
1688
+ if (!Array.isArray(values)) {
1689
+ return [];
1690
+ }
1691
+ return Array.from(
1692
+ new Set(
1693
+ values
1694
+ .map((value) => Number.parseInt(String(value), 10))
1695
+ .filter((value) => Number.isFinite(value) && value >= 0),
1696
+ ),
1697
+ ).toSorted((a, b) => a - b);
1698
+ }
1699
+
1700
+ export function readRunState(runStatePath) {
1701
+ const payload = readJsonOrNull(runStatePath);
1702
+ return {
1703
+ completedWaves: normalizeCompletedWaves(payload?.completedWaves),
1704
+ lastUpdatedAt: typeof payload?.lastUpdatedAt === "string" ? payload.lastUpdatedAt : undefined,
1705
+ };
1706
+ }
1707
+
1708
+ export function writeRunState(runStatePath, state) {
1709
+ ensureDirectory(path.dirname(runStatePath));
1710
+ const payload = {
1711
+ completedWaves: normalizeCompletedWaves(state.completedWaves),
1712
+ lastUpdatedAt: new Date().toISOString(),
1713
+ };
1714
+ writeJsonAtomic(runStatePath, payload);
1715
+ return payload;
1716
+ }
1717
+
1718
+ export function markWaveCompleted(runStatePath, waveNumber) {
1719
+ const state = readRunState(runStatePath);
1720
+ state.completedWaves = normalizeCompletedWaves([...state.completedWaves, waveNumber]);
1721
+ return writeRunState(runStatePath, state);
1722
+ }
1723
+
1724
+ export function resolveAutoNextWaveStart(allWaves, runStatePath) {
1725
+ const state = readRunState(runStatePath);
1726
+ const completed = new Set(state.completedWaves);
1727
+ for (const waveNumber of allWaves.map((wave) => wave.wave).toSorted((a, b) => a - b)) {
1728
+ if (!completed.has(waveNumber)) {
1729
+ return { nextWave: waveNumber, state };
1730
+ }
1731
+ }
1732
+ return { nextWave: null, state };
1733
+ }
1734
+
1735
+ export function arraysEqual(a, b) {
1736
+ if (a.length !== b.length) {
1737
+ return false;
1738
+ }
1739
+ for (let i = 0; i < a.length; i += 1) {
1740
+ if (a[i] !== b[i]) {
1741
+ return false;
1742
+ }
1743
+ }
1744
+ return true;
1745
+ }
1746
+
1747
+ export function readWaveEvaluatorArtifacts(wave, { logsDir, evaluatorAgentId } = {}) {
1748
+ const resolvedEvaluatorAgentId = evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
1749
+ const evaluator =
1750
+ wave.agents.find((agent) => agent.agentId === resolvedEvaluatorAgentId) ?? null;
1751
+ if (!evaluator) {
1752
+ return {
1753
+ ok: false,
1754
+ statusCode: "missing-evaluator",
1755
+ detail: `Agent ${resolvedEvaluatorAgentId} is missing.`,
1756
+ };
1757
+ }
1758
+ const evaluatorReportPath = wave.evaluatorReportPath
1759
+ ? path.resolve(REPO_ROOT, wave.evaluatorReportPath)
1760
+ : null;
1761
+ const reportText =
1762
+ evaluatorReportPath && fs.existsSync(evaluatorReportPath)
1763
+ ? fs.readFileSync(evaluatorReportPath, "utf8")
1764
+ : "";
1765
+ const reportVerdict = parseVerdictFromText(reportText, REPORT_VERDICT_REGEX);
1766
+ if (reportVerdict.verdict) {
1767
+ return {
1768
+ ok: reportVerdict.verdict === "pass",
1769
+ statusCode: reportVerdict.verdict === "pass" ? "pass" : `evaluator-${reportVerdict.verdict}`,
1770
+ detail: reportVerdict.detail || "Verdict read from evaluator report.",
1771
+ };
1772
+ }
1773
+ const evaluatorLogPath = logsDir
1774
+ ? path.join(logsDir, `wave-${wave.wave}-${evaluator.slug}.log`)
1775
+ : null;
1776
+ const logVerdict = parseVerdictFromText(
1777
+ evaluatorLogPath ? readFileTail(evaluatorLogPath, 30000) : "",
1778
+ WAVE_VERDICT_REGEX,
1779
+ );
1780
+ if (logVerdict.verdict) {
1781
+ return {
1782
+ ok: logVerdict.verdict === "pass",
1783
+ statusCode: logVerdict.verdict === "pass" ? "pass" : `evaluator-${logVerdict.verdict}`,
1784
+ detail: logVerdict.detail || "Verdict read from evaluator log marker.",
1785
+ };
1786
+ }
1787
+ return {
1788
+ ok: false,
1789
+ statusCode: "missing-evaluator-verdict",
1790
+ detail: evaluatorReportPath
1791
+ ? `Missing evaluator verdict in ${path.relative(REPO_ROOT, evaluatorReportPath)}.`
1792
+ : "Missing evaluator report path and evaluator log verdict.",
1793
+ };
1794
+ }
1795
+
1796
+ export function completedWavesFromStatusFiles(allWaves, statusDir, options = {}) {
1797
+ const logsDir = options.logsDir || path.join(path.resolve(statusDir, ".."), "logs");
1798
+ const coordinationDir =
1799
+ options.coordinationDir || path.join(path.resolve(statusDir, ".."), "coordination");
1800
+ const evaluatorAgentId = options.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
1801
+ const integrationAgentId = options.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
1802
+ const documentationAgentId =
1803
+ options.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
1804
+ const laneProfile = resolveLaneProfileForOptions(options);
1805
+ const integrationThreshold =
1806
+ options.requireIntegrationStewardFromWave ??
1807
+ laneProfile.validation.requireIntegrationStewardFromWave;
1808
+ const componentThreshold =
1809
+ options.requireComponentPromotionsFromWave ??
1810
+ laneProfile.validation.requireComponentPromotionsFromWave;
1811
+ const completed = [];
1812
+ for (const wave of allWaves) {
1813
+ let waveIsComplete = wave.agents.length > 0;
1814
+ const summariesByAgentId = {};
1815
+ for (const agent of wave.agents) {
1816
+ const statusPath = path.join(statusDir, `wave-${wave.wave}-${agent.slug}.status`);
1817
+ const statusRecord = readStatusRecordIfPresent(statusPath);
1818
+ if (!statusRecord) {
1819
+ waveIsComplete = false;
1820
+ break;
1821
+ }
1822
+ const expectedPromptHash = hashAgentPromptFingerprint(agent);
1823
+ if (statusRecord.code !== 0 || statusRecord.promptHash !== expectedPromptHash) {
1824
+ waveIsComplete = false;
1825
+ break;
1826
+ }
1827
+ const summary = readAgentExecutionSummary(statusPath);
1828
+ summariesByAgentId[agent.agentId] = summary;
1829
+ if (agent.agentId === evaluatorAgentId && summary) {
1830
+ if (!validateEvaluatorSummary(agent, summary).ok) {
1831
+ waveIsComplete = false;
1832
+ break;
1833
+ }
1834
+ continue;
1835
+ }
1836
+ if (
1837
+ agent.agentId === integrationAgentId &&
1838
+ integrationThreshold !== null &&
1839
+ wave.wave >= integrationThreshold
1840
+ ) {
1841
+ if (!validateIntegrationSummary(agent, summary).ok) {
1842
+ waveIsComplete = false;
1843
+ break;
1844
+ }
1845
+ continue;
1846
+ }
1847
+ if (agent.agentId === documentationAgentId) {
1848
+ if (!validateDocumentationClosureSummary(agent, summary).ok) {
1849
+ waveIsComplete = false;
1850
+ break;
1851
+ }
1852
+ continue;
1853
+ }
1854
+ if (!validateImplementationSummary(agent, summary).ok) {
1855
+ waveIsComplete = false;
1856
+ break;
1857
+ }
1858
+ }
1859
+ if (
1860
+ waveIsComplete &&
1861
+ componentThreshold !== null &&
1862
+ wave.wave >= componentThreshold &&
1863
+ !validateWaveComponentPromotions(wave, summariesByAgentId, options).ok
1864
+ ) {
1865
+ waveIsComplete = false;
1866
+ }
1867
+ if (
1868
+ waveIsComplete &&
1869
+ componentThreshold !== null &&
1870
+ wave.wave >= componentThreshold &&
1871
+ !validateWaveComponentMatrixCurrentLevels(wave, { ...options, laneProfile }).ok
1872
+ ) {
1873
+ waveIsComplete = false;
1874
+ }
1875
+ if (
1876
+ waveIsComplete &&
1877
+ !readWaveEvaluatorArtifacts(wave, { logsDir, evaluatorAgentId }).ok
1878
+ ) {
1879
+ waveIsComplete = false;
1880
+ }
1881
+ if (waveIsComplete) {
1882
+ const coordinationState = readMaterializedCoordinationState(
1883
+ path.join(coordinationDir, `wave-${wave.wave}.jsonl`),
1884
+ );
1885
+ if (
1886
+ coordinationState.clarifications.some((record) =>
1887
+ ["open", "acknowledged", "in_progress"].includes(record.status),
1888
+ ) ||
1889
+ openClarificationLinkedRequests(coordinationState).length > 0 ||
1890
+ coordinationState.humanEscalations.some((record) =>
1891
+ ["open", "acknowledged", "in_progress"].includes(record.status),
1892
+ ) ||
1893
+ coordinationState.humanFeedback.some((record) =>
1894
+ ["open", "acknowledged", "in_progress"].includes(record.status),
1895
+ )
1896
+ ) {
1897
+ waveIsComplete = false;
1898
+ }
1899
+ }
1900
+ if (waveIsComplete) {
1901
+ completed.push(wave.wave);
1902
+ }
1903
+ }
1904
+ return normalizeCompletedWaves(completed);
1905
+ }
1906
+
1907
+ export function reconcileRunStateFromStatusFiles(allWaves, runStatePath, statusDir, options = {}) {
1908
+ const completedFromStatus = completedWavesFromStatusFiles(allWaves, statusDir, options);
1909
+ const before = readRunState(runStatePath);
1910
+ const firstMerge = normalizeCompletedWaves([...before.completedWaves, ...completedFromStatus]);
1911
+ const latest = readRunState(runStatePath);
1912
+ const merged = normalizeCompletedWaves([...latest.completedWaves, ...completedFromStatus]);
1913
+ let state = latest;
1914
+ if (!arraysEqual(merged, latest.completedWaves)) {
1915
+ state = writeRunState(runStatePath, { completedWaves: merged });
1916
+ }
1917
+ return {
1918
+ completedFromStatus,
1919
+ addedFromBefore: firstMerge.filter((waveNumber) => !before.completedWaves.includes(waveNumber)),
1920
+ addedFromLatest: merged.filter((waveNumber) => !latest.completedWaves.includes(waveNumber)),
1921
+ state,
1922
+ };
1923
+ }