@chllming/wave-orchestration 0.5.3 → 0.5.4

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 (79) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +56 -509
  3. package/docs/README.md +39 -0
  4. package/docs/concepts/context7-vs-skills.md +94 -0
  5. package/docs/concepts/operating-modes.md +91 -0
  6. package/docs/concepts/runtime-agnostic-orchestration.md +95 -0
  7. package/docs/concepts/what-is-a-wave.md +133 -0
  8. package/docs/guides/planner.md +113 -0
  9. package/docs/guides/terminal-surfaces.md +80 -0
  10. package/docs/image.png +0 -0
  11. package/docs/plans/context7-wave-orchestrator.md +2 -0
  12. package/docs/plans/current-state.md +10 -0
  13. package/docs/plans/master-plan.md +3 -3
  14. package/docs/plans/migration.md +4 -3
  15. package/docs/plans/wave-orchestrator.md +27 -3
  16. package/docs/reference/runtime-config/README.md +19 -0
  17. package/docs/reference/skills.md +156 -0
  18. package/docs/roadmap.md +160 -564
  19. package/package.json +2 -1
  20. package/releases/manifest.json +17 -0
  21. package/scripts/wave-orchestrator/config.mjs +17 -0
  22. package/scripts/wave-orchestrator/context7.mjs +9 -0
  23. package/scripts/wave-orchestrator/coordination.mjs +16 -0
  24. package/scripts/wave-orchestrator/executors.mjs +24 -11
  25. package/scripts/wave-orchestrator/install.mjs +41 -2
  26. package/scripts/wave-orchestrator/launcher.mjs +113 -20
  27. package/scripts/wave-orchestrator/planner.mjs +1328 -0
  28. package/scripts/wave-orchestrator/project-profile.mjs +190 -0
  29. package/scripts/wave-orchestrator/shared.mjs +2 -0
  30. package/scripts/wave-orchestrator/skills.mjs +448 -0
  31. package/scripts/wave-orchestrator/terminals.mjs +16 -0
  32. package/scripts/wave-orchestrator/traces.mjs +23 -0
  33. package/scripts/wave-orchestrator/wave-files.mjs +299 -84
  34. package/scripts/wave.mjs +11 -0
  35. package/skills/provider-aws/SKILL.md +6 -0
  36. package/skills/provider-aws/skill.json +5 -0
  37. package/skills/provider-custom-deploy/SKILL.md +5 -0
  38. package/skills/provider-custom-deploy/skill.json +5 -0
  39. package/skills/provider-docker-compose/SKILL.md +6 -0
  40. package/skills/provider-docker-compose/skill.json +5 -0
  41. package/skills/provider-github-release/SKILL.md +6 -0
  42. package/skills/provider-github-release/skill.json +5 -0
  43. package/skills/provider-kubernetes/SKILL.md +6 -0
  44. package/skills/provider-kubernetes/skill.json +5 -0
  45. package/skills/provider-railway/SKILL.md +6 -0
  46. package/skills/provider-railway/adapters/claude.md +1 -0
  47. package/skills/provider-railway/adapters/codex.md +1 -0
  48. package/skills/provider-railway/adapters/local.md +1 -0
  49. package/skills/provider-railway/adapters/opencode.md +1 -0
  50. package/skills/provider-railway/skill.json +5 -0
  51. package/skills/provider-ssh-manual/SKILL.md +6 -0
  52. package/skills/provider-ssh-manual/skill.json +5 -0
  53. package/skills/repo-coding-rules/SKILL.md +7 -0
  54. package/skills/repo-coding-rules/skill.json +5 -0
  55. package/skills/role-deploy/SKILL.md +6 -0
  56. package/skills/role-deploy/skill.json +5 -0
  57. package/skills/role-documentation/SKILL.md +6 -0
  58. package/skills/role-documentation/skill.json +5 -0
  59. package/skills/role-evaluator/SKILL.md +6 -0
  60. package/skills/role-evaluator/skill.json +5 -0
  61. package/skills/role-implementation/SKILL.md +6 -0
  62. package/skills/role-implementation/skill.json +5 -0
  63. package/skills/role-infra/SKILL.md +6 -0
  64. package/skills/role-infra/skill.json +5 -0
  65. package/skills/role-integration/SKILL.md +6 -0
  66. package/skills/role-integration/skill.json +5 -0
  67. package/skills/role-research/SKILL.md +6 -0
  68. package/skills/role-research/skill.json +5 -0
  69. package/skills/runtime-claude/SKILL.md +6 -0
  70. package/skills/runtime-claude/skill.json +5 -0
  71. package/skills/runtime-codex/SKILL.md +6 -0
  72. package/skills/runtime-codex/skill.json +5 -0
  73. package/skills/runtime-local/SKILL.md +5 -0
  74. package/skills/runtime-local/skill.json +5 -0
  75. package/skills/runtime-opencode/SKILL.md +6 -0
  76. package/skills/runtime-opencode/skill.json +5 -0
  77. package/skills/wave-core/SKILL.md +7 -0
  78. package/skills/wave-core/skill.json +5 -0
  79. package/wave.config.json +27 -0
@@ -0,0 +1,1328 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import readline from "node:readline/promises";
4
+ import { stdin, stderr } from "node:process";
5
+ import { EXIT_CONTRACT_COMPLETION_VALUES, EXIT_CONTRACT_DOC_IMPACT_VALUES, EXIT_CONTRACT_DURABILITY_VALUES, EXIT_CONTRACT_PROOF_VALUES } from "./agent-state.mjs";
6
+ import { loadWaveConfig } from "./config.mjs";
7
+ import { loadComponentCutoverMatrix, parseWaveFile, requiredDocumentationStewardPathsForWave, SHARED_PLAN_DOC_PATHS, validateWaveDefinition, applyExecutorSelectionsToWave } from "./wave-files.mjs";
8
+ import { buildLanePaths, ensureDirectory, REPO_ROOT, writeJsonAtomic, writeTextAtomic } from "./shared.mjs";
9
+ import {
10
+ DEPLOY_ENVIRONMENT_KINDS,
11
+ DRAFT_TEMPLATES,
12
+ buildDefaultProjectProfile,
13
+ normalizeDraftTemplate,
14
+ normalizeOversightMode,
15
+ PROJECT_OVERSIGHT_MODES,
16
+ PROJECT_PROFILE_PATH,
17
+ PROJECT_PROFILE_TERMINAL_SURFACES,
18
+ readProjectProfile,
19
+ updateProjectProfile,
20
+ writeProjectProfile,
21
+ } from "./project-profile.mjs";
22
+ import { normalizeTerminalSurface } from "./terminals.mjs";
23
+
24
+ const COMPONENT_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
25
+ const WAVE_SPEC_SCHEMA_VERSION = 1;
26
+ let bufferedNonTtyAnswers = null;
27
+ let bufferedNonTtyAnswerIndex = 0;
28
+
29
+ function printJson(payload) {
30
+ console.log(JSON.stringify(payload, null, 2));
31
+ }
32
+
33
+ function compactSingleLine(value) {
34
+ return String(value ?? "")
35
+ .replace(/\s+/g, " ")
36
+ .trim();
37
+ }
38
+
39
+ function cleanText(value) {
40
+ return String(value ?? "").trim();
41
+ }
42
+
43
+ function normalizeListText(value) {
44
+ return String(value ?? "")
45
+ .split(/[|,]/)
46
+ .map((entry) => entry.trim())
47
+ .filter(Boolean);
48
+ }
49
+
50
+ function normalizePipeList(value) {
51
+ return String(value ?? "")
52
+ .split("|")
53
+ .map((entry) => entry.trim())
54
+ .filter(Boolean);
55
+ }
56
+
57
+ function normalizeComponentId(value, label = "component id") {
58
+ const normalized = cleanText(value).toLowerCase();
59
+ if (!COMPONENT_ID_REGEX.test(normalized)) {
60
+ throw new Error(`${label} must match ${COMPONENT_ID_REGEX}`);
61
+ }
62
+ return normalized;
63
+ }
64
+
65
+ function normalizeRepoPathList(values, label) {
66
+ return values.map((entry, index) => {
67
+ const normalized = cleanText(entry).replaceAll("\\", "/").replace(/^\.\/+/, "");
68
+ if (!normalized) {
69
+ throw new Error(`${label}[${index}] is required`);
70
+ }
71
+ if (normalized.startsWith("/") || normalized.startsWith("../") || normalized.includes("/../")) {
72
+ throw new Error(`${label}[${index}] must stay inside the repository`);
73
+ }
74
+ return normalized;
75
+ });
76
+ }
77
+
78
+ function defaultWorkerRoleKindForTemplate(template) {
79
+ if (template === "infra") {
80
+ return "infra";
81
+ }
82
+ if (template === "release") {
83
+ return "deploy";
84
+ }
85
+ if (template === "qa") {
86
+ return "qa";
87
+ }
88
+ return "implementation";
89
+ }
90
+
91
+ function defaultWorkerTitle(template, index) {
92
+ if (template === "qa") {
93
+ return `QA Track ${index + 1}`;
94
+ }
95
+ if (template === "infra") {
96
+ return `Infra Track ${index + 1}`;
97
+ }
98
+ if (template === "release") {
99
+ return `Release Track ${index + 1}`;
100
+ }
101
+ return `Implementation Track ${index + 1}`;
102
+ }
103
+
104
+ function defaultTargetLevel(template) {
105
+ if (template === "qa") {
106
+ return "qa-proved";
107
+ }
108
+ if (template === "release") {
109
+ return "fleet-ready";
110
+ }
111
+ if (template === "infra") {
112
+ return "baseline-proved";
113
+ }
114
+ return "repo-landed";
115
+ }
116
+
117
+ function defaultExecutorProfile(roleKind) {
118
+ if (roleKind === "infra" || roleKind === "deploy" || roleKind === "research") {
119
+ return "ops-triage";
120
+ }
121
+ return "implement-fast";
122
+ }
123
+
124
+ function defaultExitContract(roleKind) {
125
+ if (roleKind === "infra" || roleKind === "deploy") {
126
+ return {
127
+ completion: "live",
128
+ durability: "durable",
129
+ proof: "live",
130
+ docImpact: "owned",
131
+ };
132
+ }
133
+ if (roleKind === "qa") {
134
+ return {
135
+ completion: "integrated",
136
+ durability: "none",
137
+ proof: "integration",
138
+ docImpact: "owned",
139
+ };
140
+ }
141
+ return {
142
+ completion: "contract",
143
+ durability: "none",
144
+ proof: "unit",
145
+ docImpact: "owned",
146
+ };
147
+ }
148
+
149
+ function buildDefaultValidationCommand(template, roleKind) {
150
+ if (template === "qa" || roleKind === "qa") {
151
+ return "pnpm test";
152
+ }
153
+ if (roleKind === "infra" || roleKind === "deploy") {
154
+ return "pnpm exec wave launch --dry-run --no-dashboard";
155
+ }
156
+ return "pnpm test";
157
+ }
158
+
159
+ function buildDefaultOutputSummary(template, roleKind) {
160
+ if (template === "qa" || roleKind === "qa") {
161
+ return "Summarize the proved QA coverage, the remaining gaps, and whether the wave is closure-ready.";
162
+ }
163
+ if (roleKind === "infra" || roleKind === "deploy") {
164
+ return "Summarize the environment proof, operator-visible risks, and rollback posture.";
165
+ }
166
+ return "Summarize the landed implementation, proof status, and exact follow-up owners.";
167
+ }
168
+
169
+ function buildDefaultPrimaryGoal(template, roleKind, title) {
170
+ if (template === "qa" || roleKind === "qa") {
171
+ return `Build and validate the ${title.toLowerCase()} QA slice.`;
172
+ }
173
+ if (roleKind === "infra" || roleKind === "deploy") {
174
+ return `Own the ${title.toLowerCase()} environment and deployment proof.`;
175
+ }
176
+ return `Implement and prove the ${title.toLowerCase()} slice.`;
177
+ }
178
+
179
+ class PromptSession {
180
+ constructor() {
181
+ this.interface = stdin.isTTY
182
+ ? readline.createInterface({
183
+ input: stdin,
184
+ output: stderr,
185
+ terminal: true,
186
+ })
187
+ : null;
188
+ }
189
+
190
+ static consumeBufferedNonTtyAnswer() {
191
+ if (bufferedNonTtyAnswers === null) {
192
+ bufferedNonTtyAnswers = fs.readFileSync(0, "utf8").split(/\r?\n/);
193
+ bufferedNonTtyAnswerIndex = 0;
194
+ }
195
+ const answer =
196
+ bufferedNonTtyAnswerIndex < bufferedNonTtyAnswers.length
197
+ ? bufferedNonTtyAnswers[bufferedNonTtyAnswerIndex]
198
+ : "";
199
+ bufferedNonTtyAnswerIndex += 1;
200
+ return answer;
201
+ }
202
+
203
+ async ask(question, defaultValue = null) {
204
+ const suffix =
205
+ defaultValue !== null && defaultValue !== undefined && String(defaultValue).length > 0
206
+ ? ` [${defaultValue}]`
207
+ : "";
208
+ let answer = "";
209
+ if (this.interface) {
210
+ answer = await this.interface.question(`${question}${suffix}: `);
211
+ } else {
212
+ stderr.write(`${question}${suffix}: `);
213
+ answer = PromptSession.consumeBufferedNonTtyAnswer();
214
+ stderr.write("\n");
215
+ }
216
+ const trimmed = String(answer ?? "").trim();
217
+ if (!trimmed && defaultValue !== null && defaultValue !== undefined) {
218
+ return String(defaultValue);
219
+ }
220
+ return trimmed;
221
+ }
222
+
223
+ async askInteger(question, defaultValue = 0, options = {}) {
224
+ const min = Number.isFinite(options.min) ? options.min : 0;
225
+ while (true) {
226
+ const raw = await this.ask(question, String(defaultValue));
227
+ const parsed = Number.parseInt(raw, 10);
228
+ if (Number.isFinite(parsed) && parsed >= min) {
229
+ return parsed;
230
+ }
231
+ stderr.write(`Expected an integer >= ${min}.\n`);
232
+ }
233
+ }
234
+
235
+ async askBoolean(question, defaultValue = true) {
236
+ const defaultToken = defaultValue ? "y" : "n";
237
+ while (true) {
238
+ const raw = (await this.ask(`${question} (y/n)`, defaultToken)).toLowerCase();
239
+ if (["y", "yes"].includes(raw)) {
240
+ return true;
241
+ }
242
+ if (["n", "no"].includes(raw)) {
243
+ return false;
244
+ }
245
+ stderr.write("Expected y or n.\n");
246
+ }
247
+ }
248
+
249
+ async askChoice(question, choices, defaultValue) {
250
+ const normalizedChoices = choices.map((choice) => String(choice).trim()).filter(Boolean);
251
+ while (true) {
252
+ const answer = cleanText(await this.ask(`${question} (${normalizedChoices.join("/")})`, defaultValue));
253
+ if (normalizedChoices.includes(answer)) {
254
+ return answer;
255
+ }
256
+ stderr.write(`Expected one of: ${normalizedChoices.join(", ")}.\n`);
257
+ }
258
+ }
259
+
260
+ async close() {
261
+ this.interface?.close();
262
+ }
263
+ }
264
+
265
+ function ensureWavePaths(lanePaths, waveNumber) {
266
+ const wavePath = path.join(lanePaths.wavesDir, `wave-${waveNumber}.md`);
267
+ const specPath = path.join(lanePaths.wavesDir, "specs", `wave-${waveNumber}.json`);
268
+ return { wavePath, specPath };
269
+ }
270
+
271
+ function renderBulletLines(items) {
272
+ return items.map((item) => `- ${item}`);
273
+ }
274
+
275
+ function renderPromptBlock({
276
+ primaryGoal,
277
+ collaborationNotes = [],
278
+ requiredContext = [],
279
+ earlierWaveOutputs = [],
280
+ ownedPaths = [],
281
+ requirements = [],
282
+ validationCommand = "",
283
+ outputSummary = "",
284
+ deployEnvironment = null,
285
+ }) {
286
+ const lines = [];
287
+ lines.push("Primary goal:");
288
+ lines.push(`- ${primaryGoal}`);
289
+ if (collaborationNotes.length > 0 || deployEnvironment) {
290
+ lines.push("");
291
+ lines.push("Collaboration notes:");
292
+ for (const note of collaborationNotes) {
293
+ lines.push(`- ${note}`);
294
+ }
295
+ if (deployEnvironment) {
296
+ const suffix = deployEnvironment.notes ? ` (${deployEnvironment.notes})` : "";
297
+ lines.push(
298
+ `- The primary deploy environment for this role is \`${deployEnvironment.id}\` via \`${deployEnvironment.kind}\`${suffix}.`,
299
+ );
300
+ }
301
+ }
302
+ lines.push("");
303
+ lines.push("Required context before coding:");
304
+ lines.push(...renderBulletLines(requiredContext));
305
+ if (earlierWaveOutputs.length > 0) {
306
+ lines.push("");
307
+ lines.push("Earlier wave outputs to read:");
308
+ lines.push(...renderBulletLines(earlierWaveOutputs.map((item) => `\`${item}\``)));
309
+ }
310
+ lines.push("");
311
+ lines.push("File ownership (only touch these paths):");
312
+ lines.push(...renderBulletLines(ownedPaths));
313
+ if (requirements.length > 0) {
314
+ lines.push("");
315
+ lines.push("Requirements:");
316
+ requirements.forEach((requirement, index) => {
317
+ lines.push(`${index + 1}) ${requirement}`);
318
+ });
319
+ }
320
+ lines.push("");
321
+ lines.push("Validation:");
322
+ lines.push(`- ${validationCommand || "Manual review of the changed files and generated proof artifacts."}`);
323
+ lines.push("");
324
+ lines.push("Output:");
325
+ lines.push(`- ${outputSummary || "Summarize the landed work, proof state, and remaining blockers."}`);
326
+ return lines.join("\n");
327
+ }
328
+
329
+ function renderExecutorSection(agent) {
330
+ const lines = [];
331
+ if (agent.executor?.profile) {
332
+ lines.push(`- profile: ${agent.executor.profile}`);
333
+ }
334
+ if (agent.executor?.id) {
335
+ lines.push(`- id: ${agent.executor.id}`);
336
+ }
337
+ if (agent.executor?.model) {
338
+ lines.push(`- model: ${agent.executor.model}`);
339
+ }
340
+ if (Array.isArray(agent.executor?.fallbacks) && agent.executor.fallbacks.length > 0) {
341
+ lines.push(`- fallbacks: ${agent.executor.fallbacks.join(", ")}`);
342
+ }
343
+ return lines;
344
+ }
345
+
346
+ function renderContext7Section(context7) {
347
+ const lines = [`- bundle: ${context7?.bundle || "none"}`];
348
+ if (context7?.query) {
349
+ lines.push(`- query: "${context7.query.replace(/"/g, '\\"')}"`);
350
+ }
351
+ return lines;
352
+ }
353
+
354
+ function renderSkillsSection(skills) {
355
+ return Array.isArray(skills) && skills.length > 0 ? renderBulletLines(skills) : [];
356
+ }
357
+
358
+ function renderWaveMarkdown(spec, lanePaths) {
359
+ const sections = [];
360
+ sections.push(`# Wave ${spec.wave} - ${spec.title}`);
361
+ sections.push("");
362
+ sections.push(`**Commit message**: \`${spec.commitMessage}\``);
363
+ if (spec.projectProfile?.newProject !== undefined || spec.oversightMode) {
364
+ sections.push("");
365
+ sections.push("## Project profile");
366
+ sections.push("");
367
+ sections.push(`- project: ${spec.projectProfile?.projectName || lanePaths.config.projectName}`);
368
+ sections.push(`- new-project: ${spec.projectProfile?.newProject ? "yes" : "no"}`);
369
+ sections.push(`- oversight-mode: ${spec.oversightMode}`);
370
+ sections.push(`- lane: ${spec.lane}`);
371
+ }
372
+ if (spec.sequencingNote) {
373
+ sections.push("");
374
+ sections.push("## Sequencing note");
375
+ sections.push("");
376
+ sections.push(...renderBulletLines([spec.sequencingNote]));
377
+ }
378
+ if (spec.referenceRule) {
379
+ sections.push("");
380
+ sections.push("## Reference rule");
381
+ sections.push("");
382
+ sections.push(...renderBulletLines([spec.referenceRule]));
383
+ }
384
+ if (spec.deployEnvironments.length > 0) {
385
+ sections.push("");
386
+ sections.push("## Deploy environments");
387
+ sections.push("");
388
+ sections.push(
389
+ ...spec.deployEnvironments.map((environment) => {
390
+ const suffix = environment.notes ? ` (${environment.notes})` : "";
391
+ return `- \`${environment.id}\`: \`${environment.kind}\`${environment.isDefault ? " default" : ""}${suffix}`;
392
+ }),
393
+ );
394
+ }
395
+ sections.push("");
396
+ sections.push("## Component promotions");
397
+ sections.push("");
398
+ sections.push(
399
+ ...spec.componentPromotions.map(
400
+ (promotion) => `- ${promotion.componentId}: ${promotion.targetLevel}`,
401
+ ),
402
+ );
403
+ sections.push("");
404
+ sections.push("## Context7 defaults");
405
+ sections.push("");
406
+ sections.push(...renderContext7Section(spec.context7Defaults));
407
+ for (const agent of spec.agents) {
408
+ sections.push("");
409
+ sections.push(`## Agent ${agent.agentId}: ${agent.title}`);
410
+ if (Array.isArray(agent.rolePromptPaths) && agent.rolePromptPaths.length > 0) {
411
+ sections.push("");
412
+ sections.push("### Role prompts");
413
+ sections.push("");
414
+ sections.push(...renderBulletLines(agent.rolePromptPaths));
415
+ }
416
+ sections.push("");
417
+ sections.push("### Executor");
418
+ sections.push("");
419
+ sections.push(...renderExecutorSection(agent));
420
+ sections.push("");
421
+ sections.push("### Context7");
422
+ sections.push("");
423
+ sections.push(...renderContext7Section(agent.context7));
424
+ if (Array.isArray(agent.skills) && agent.skills.length > 0) {
425
+ sections.push("");
426
+ sections.push("### Skills");
427
+ sections.push("");
428
+ sections.push(...renderSkillsSection(agent.skills));
429
+ }
430
+ if (Array.isArray(agent.components) && agent.components.length > 0) {
431
+ sections.push("");
432
+ sections.push("### Components");
433
+ sections.push("");
434
+ sections.push(...renderBulletLines(agent.components));
435
+ }
436
+ if (Array.isArray(agent.capabilities) && agent.capabilities.length > 0) {
437
+ sections.push("");
438
+ sections.push("### Capabilities");
439
+ sections.push("");
440
+ sections.push(...renderBulletLines(agent.capabilities));
441
+ }
442
+ if (agent.exitContract) {
443
+ sections.push("");
444
+ sections.push("### Exit contract");
445
+ sections.push("");
446
+ sections.push(`- completion: ${agent.exitContract.completion}`);
447
+ sections.push(`- durability: ${agent.exitContract.durability}`);
448
+ sections.push(`- proof: ${agent.exitContract.proof}`);
449
+ sections.push(`- doc-impact: ${agent.exitContract.docImpact}`);
450
+ }
451
+ sections.push("");
452
+ sections.push("### Prompt");
453
+ sections.push("");
454
+ sections.push("```text");
455
+ sections.push(
456
+ renderPromptBlock({
457
+ primaryGoal: agent.primaryGoal,
458
+ collaborationNotes: agent.collaborationNotes,
459
+ requiredContext: agent.requiredContext,
460
+ earlierWaveOutputs: agent.earlierWaveOutputs,
461
+ ownedPaths: agent.ownedPaths,
462
+ requirements: agent.requirements,
463
+ validationCommand: agent.validationCommand,
464
+ outputSummary: agent.outputSummary,
465
+ deployEnvironment:
466
+ agent.deployEnvironmentId &&
467
+ spec.deployEnvironments.find((environment) => environment.id === agent.deployEnvironmentId),
468
+ }),
469
+ );
470
+ sections.push("```");
471
+ }
472
+ sections.push("");
473
+ return sections.join("\n");
474
+ }
475
+
476
+ function renderComponentMatrixMarkdown(matrixPayload) {
477
+ const componentEntries = Object.entries(matrixPayload.components).sort((left, right) =>
478
+ left[0].localeCompare(right[0]),
479
+ );
480
+ const wavePromotions = new Map();
481
+ for (const [componentId, component] of componentEntries) {
482
+ for (const promotion of component.promotions || []) {
483
+ const list = wavePromotions.get(promotion.wave) || [];
484
+ list.push({ componentId, target: promotion.target });
485
+ wavePromotions.set(promotion.wave, list);
486
+ }
487
+ }
488
+ const lines = [
489
+ "# Component Cutover Matrix",
490
+ "",
491
+ "This matrix is the canonical place to answer which harness components are expected to be working at which maturity level.",
492
+ "",
493
+ "## Levels",
494
+ "",
495
+ ...renderBulletLines(matrixPayload.levels.map((level) => `\`${level}\``)),
496
+ "",
497
+ "## Components",
498
+ "",
499
+ ...componentEntries.map(
500
+ ([componentId, component]) => `- \`${componentId}\`: ${component.title || componentId}`,
501
+ ),
502
+ "",
503
+ "## Current Levels",
504
+ "",
505
+ "| Component | Current level | Proof surfaces |",
506
+ "| --- | --- | --- |",
507
+ ...componentEntries.map(
508
+ ([componentId, component]) =>
509
+ `| \`${componentId}\` | \`${component.currentLevel}\` | ${(component.proofSurfaces || []).join(", ")} |`,
510
+ ),
511
+ "",
512
+ "## Promotions By Wave",
513
+ "",
514
+ ];
515
+ for (const waveNumber of Array.from(wavePromotions.keys()).sort((a, b) => a - b)) {
516
+ lines.push(`### Wave ${waveNumber}`);
517
+ lines.push("");
518
+ lines.push(
519
+ ...wavePromotions
520
+ .get(waveNumber)
521
+ .toSorted((left, right) => left.componentId.localeCompare(right.componentId))
522
+ .map(
523
+ (promotion) =>
524
+ `- Wave ${waveNumber} promotes \`${promotion.componentId}\` to \`${promotion.target}\`.`,
525
+ ),
526
+ );
527
+ lines.push("");
528
+ }
529
+ lines.push("## Usage");
530
+ lines.push("");
531
+ lines.push("- Keep architecture and repository guidance docs descriptive.");
532
+ lines.push("- Keep wave-by-wave component maturity and promotion targets here.");
533
+ lines.push("- `currentLevel` is the canonical post-wave state of the repo, not a future plan.");
534
+ lines.push("- When component promotion gating is active, wave files must match this matrix exactly.");
535
+ lines.push("");
536
+ return lines.join("\n");
537
+ }
538
+
539
+ function buildSpecialAgents({ spec, lanePaths, standardRoles }) {
540
+ const sharedDocs = requiredDocumentationStewardPathsForWave(spec.wave, {
541
+ laneProfile: lanePaths.laneProfile,
542
+ });
543
+ const commonRequiredContext = Array.from(
544
+ new Set([
545
+ "docs/reference/repository-guidance.md",
546
+ "docs/research/agent-context-sources.md",
547
+ ...SHARED_PLAN_DOC_PATHS,
548
+ ]),
549
+ );
550
+ const evaluatorTitle = standardRoles.evaluator ? "Running Evaluator" : "Custom Evaluator";
551
+ const integrationTitle = standardRoles.integration ? "Integration Steward" : "Custom Integration Steward";
552
+ const documentationTitle = standardRoles.documentation
553
+ ? "Documentation Steward"
554
+ : "Custom Documentation Steward";
555
+ return [
556
+ {
557
+ agentId: lanePaths.evaluatorAgentId,
558
+ title: evaluatorTitle,
559
+ rolePromptPaths: [lanePaths.evaluatorRolePromptPath],
560
+ skills: [],
561
+ executor: { profile: "deep-review" },
562
+ context7: { bundle: "none", query: "Architecture evaluation only; repository docs remain canonical" },
563
+ components: [],
564
+ capabilities: [],
565
+ exitContract: null,
566
+ primaryGoal: `Evaluate Wave ${spec.wave} and publish the final verdict.`,
567
+ collaborationNotes: [
568
+ "Collect explicit verdicts from the implementation-facing agents plus A8 and A9 before closing the wave.",
569
+ "Do not publish PASS unless the evidence, documentation closure, and integration summary are all coherent.",
570
+ ],
571
+ requiredContext: commonRequiredContext,
572
+ earlierWaveOutputs: [],
573
+ ownedPaths: [`docs/plans/waves/reviews/wave-${spec.wave}-evaluator.md`],
574
+ requirements: [
575
+ "Verify the wave requirements are covered by landed evidence, not only by intent.",
576
+ "Record any blocker that later waves must not silently assume away.",
577
+ ],
578
+ validationCommand:
579
+ "Re-read the changed reports and end the evaluator report with `Verdict: PASS`, `Verdict: CONCERNS`, or `Verdict: BLOCKED`.",
580
+ outputSummary: "Summarize the gate verdict and the top unresolved cross-cutting risks.",
581
+ deployEnvironmentId: null,
582
+ },
583
+ {
584
+ agentId: lanePaths.integrationAgentId,
585
+ title: integrationTitle,
586
+ rolePromptPaths: [lanePaths.integrationRolePromptPath],
587
+ skills: [],
588
+ executor: { profile: "deep-review" },
589
+ context7: { bundle: "none", query: "Integration synthesis only; repository docs remain canonical" },
590
+ components: [],
591
+ capabilities: ["integration", "docs-shared-plan"],
592
+ exitContract: null,
593
+ primaryGoal: `Synthesize the final Wave ${spec.wave} state before documentation and evaluator closure.`,
594
+ collaborationNotes: [
595
+ "Re-read the message board, compiled inboxes, and latest artifacts before final output.",
596
+ "Treat contradictions, missing proof, or stale shared-plan assumptions as integration failures.",
597
+ ],
598
+ requiredContext: commonRequiredContext,
599
+ earlierWaveOutputs: [],
600
+ ownedPaths: [
601
+ path.join(path.relative(REPO_ROOT, lanePaths.integrationDir), `wave-${spec.wave}.md`).replaceAll("\\", "/"),
602
+ path.join(path.relative(REPO_ROOT, lanePaths.integrationDir), `wave-${spec.wave}.json`).replaceAll("\\", "/"),
603
+ ],
604
+ requirements: [
605
+ "Produce a closure-ready summary of claims, conflicts, blockers, and remaining follow-up owners.",
606
+ "Decide whether the wave is `ready-for-doc-closure` or `needs-more-work`.",
607
+ ],
608
+ validationCommand: "Re-read the generated integration artifact and the latest changed proof docs before final output.",
609
+ outputSummary: "Summarize the integration verdict, blockers, and exact closure recommendation.",
610
+ deployEnvironmentId: null,
611
+ },
612
+ {
613
+ agentId: lanePaths.documentationAgentId,
614
+ title: documentationTitle,
615
+ rolePromptPaths: [lanePaths.documentationRolePromptPath],
616
+ skills: [],
617
+ executor: { profile: "docs-pass" },
618
+ context7: { bundle: "none", query: "Shared plan documentation only; repository docs remain canonical" },
619
+ components: [],
620
+ capabilities: [],
621
+ exitContract: null,
622
+ primaryGoal: `Keep shared plan docs aligned with Wave ${spec.wave} end-to-end.`,
623
+ collaborationNotes: [
624
+ "Coordinate with the implementation-facing agents and A8 before changing shared plan docs.",
625
+ "Treat implementation-owned proof docs as owned deliverables; keep shared-plan deltas in your files.",
626
+ ],
627
+ requiredContext: commonRequiredContext,
628
+ earlierWaveOutputs: [],
629
+ ownedPaths: sharedDocs,
630
+ requirements: [
631
+ "Track which wave outcomes change shared status, sequencing, ownership, or proof expectations.",
632
+ "Leave an exact-scope closure note when no shared-plan update is required.",
633
+ ],
634
+ validationCommand: "Manual review of shared plan docs against the landed wave deliverables.",
635
+ outputSummary: "Summarize the shared doc updates, deliberate no-change decisions, and remaining follow-ups.",
636
+ deployEnvironmentId: null,
637
+ },
638
+ ];
639
+ }
640
+
641
+ function buildWorkerAgentSpec({
642
+ template,
643
+ lanePaths,
644
+ spec,
645
+ index,
646
+ values,
647
+ }) {
648
+ const roleKind = values.roleKind;
649
+ const agentId = values.agentId;
650
+ const title = values.title;
651
+ const requiredContext = Array.from(
652
+ new Set([
653
+ "docs/reference/repository-guidance.md",
654
+ "docs/research/agent-context-sources.md",
655
+ ...values.additionalContext,
656
+ ]),
657
+ );
658
+ const capabilities = values.capabilities.slice();
659
+ if (roleKind === "infra" && !capabilities.includes("infra")) {
660
+ capabilities.push("infra");
661
+ }
662
+ if (roleKind === "deploy" && !capabilities.includes("deploy")) {
663
+ capabilities.push("deploy");
664
+ }
665
+ if (roleKind === "research" && !capabilities.includes("research")) {
666
+ capabilities.push("research");
667
+ }
668
+ return {
669
+ agentId,
670
+ title,
671
+ rolePromptPaths: [],
672
+ skills: values.skills || [],
673
+ executor: {
674
+ profile: values.executorProfile,
675
+ },
676
+ context7: {
677
+ bundle: values.context7Bundle,
678
+ query: values.context7Query || null,
679
+ },
680
+ components: values.components,
681
+ capabilities,
682
+ exitContract: values.exitContract,
683
+ primaryGoal:
684
+ values.primaryGoal || buildDefaultPrimaryGoal(template, roleKind, title),
685
+ collaborationNotes: [
686
+ "Re-read the wave message board before major decisions, before validation, and before final output.",
687
+ `Notify Agent ${lanePaths.evaluatorAgentId} when your evidence changes the closure picture.`,
688
+ ],
689
+ requiredContext,
690
+ earlierWaveOutputs: values.earlierWaveOutputs,
691
+ ownedPaths: values.ownedPaths,
692
+ requirements: values.requirements,
693
+ validationCommand: values.validationCommand,
694
+ outputSummary: values.outputSummary,
695
+ deployEnvironmentId: values.deployEnvironmentId || null,
696
+ };
697
+ }
698
+
699
+ function buildSpecPayload({ config, lanePaths, profile, draftValues }) {
700
+ const projectDeployEnvironments = profile.deployEnvironments || [];
701
+ const selectedDeployEnvironments = projectDeployEnvironments.filter((environment) =>
702
+ draftValues.workerAgents.some((agent) => agent.deployEnvironmentId === environment.id),
703
+ );
704
+ return {
705
+ schemaVersion: WAVE_SPEC_SCHEMA_VERSION,
706
+ generatedAt: new Date().toISOString(),
707
+ projectProfile: {
708
+ projectName: profile.source?.projectName || config.projectName,
709
+ newProject: profile.newProject === true,
710
+ defaultTerminalSurface: profile.defaultTerminalSurface,
711
+ },
712
+ template: draftValues.template,
713
+ lane: lanePaths.lane,
714
+ wave: draftValues.wave,
715
+ title: draftValues.title,
716
+ commitMessage: draftValues.commitMessage,
717
+ oversightMode: draftValues.oversightMode,
718
+ sequencingNote: draftValues.sequencingNote || null,
719
+ referenceRule: draftValues.referenceRule || null,
720
+ deployEnvironments: selectedDeployEnvironments,
721
+ context7Defaults: {
722
+ bundle: draftValues.context7Bundle,
723
+ query: draftValues.context7Query || null,
724
+ },
725
+ componentPromotions: draftValues.componentPromotions,
726
+ componentsCatalog: draftValues.componentCatalog,
727
+ agents: [
728
+ ...buildSpecialAgents({
729
+ spec: draftValues,
730
+ lanePaths,
731
+ standardRoles: draftValues.standardRoles,
732
+ }),
733
+ ...draftValues.workerAgents.map((agent, index) =>
734
+ buildWorkerAgentSpec({
735
+ template: draftValues.template,
736
+ lanePaths,
737
+ spec: draftValues,
738
+ index,
739
+ values: agent,
740
+ }),
741
+ ),
742
+ ],
743
+ };
744
+ }
745
+
746
+ function upsertComponentMatrix(matrix, spec) {
747
+ const next = {
748
+ version: matrix.version,
749
+ levels: matrix.levels.slice(),
750
+ components: JSON.parse(JSON.stringify(matrix.components)),
751
+ };
752
+ for (const componentSpec of spec.componentsCatalog) {
753
+ const existing = next.components[componentSpec.componentId];
754
+ next.components[componentSpec.componentId] = {
755
+ title: componentSpec.title,
756
+ canonicalDocs: componentSpec.canonicalDocs,
757
+ currentLevel: componentSpec.currentLevel,
758
+ promotions: (
759
+ existing?.promotions?.filter((promotion) => promotion.wave !== spec.wave) || []
760
+ )
761
+ .concat([{ wave: spec.wave, target: componentSpec.targetLevel }])
762
+ .toSorted((left, right) => left.wave - right.wave),
763
+ proofSurfaces: componentSpec.proofSurfaces,
764
+ };
765
+ }
766
+ return next;
767
+ }
768
+
769
+ async function ensureProjectProfile(options = {}) {
770
+ const config = options.config || loadWaveConfig();
771
+ const existing = readProjectProfile({ config });
772
+ if (existing) {
773
+ return existing;
774
+ }
775
+ return runProjectSetupFlow({
776
+ config,
777
+ json: false,
778
+ fromDraft: true,
779
+ });
780
+ }
781
+
782
+ async function runProjectSetupFlow(options = {}) {
783
+ const config = options.config || loadWaveConfig();
784
+ const existing = readProjectProfile({ config });
785
+ const base = existing || buildDefaultProjectProfile(config);
786
+ const prompt = new PromptSession();
787
+ try {
788
+ const laneChoices = Array.from(
789
+ new Set([config.defaultLane, ...Object.keys(config.lanes || {})].filter(Boolean)),
790
+ );
791
+ const newProject = await prompt.askBoolean("Treat this repository as a new project?", base.newProject);
792
+ const defaultOversightMode = normalizeOversightMode(
793
+ await prompt.askChoice(
794
+ "Default execution posture",
795
+ PROJECT_OVERSIGHT_MODES,
796
+ base.defaultOversightMode,
797
+ ),
798
+ );
799
+ const defaultTerminalSurface = normalizeTerminalSurface(
800
+ await prompt.askChoice(
801
+ "Default terminal surface",
802
+ PROJECT_PROFILE_TERMINAL_SURFACES,
803
+ base.defaultTerminalSurface,
804
+ ),
805
+ );
806
+ const template = normalizeDraftTemplate(
807
+ await prompt.askChoice(
808
+ "Default draft template",
809
+ DRAFT_TEMPLATES,
810
+ base.plannerDefaults.template,
811
+ ),
812
+ );
813
+ const lane = await prompt.askChoice(
814
+ "Default draft lane",
815
+ laneChoices,
816
+ base.plannerDefaults.lane,
817
+ );
818
+ const deployEnvironmentCount = await prompt.askInteger(
819
+ "How many deploy environments should the planner remember?",
820
+ base.deployEnvironments.length,
821
+ { min: 0 },
822
+ );
823
+ const deployEnvironments = [];
824
+ for (let index = 0; index < deployEnvironmentCount; index += 1) {
825
+ const existingEnvironment = base.deployEnvironments[index] || null;
826
+ const id = normalizeComponentId(
827
+ await prompt.ask(
828
+ `Deploy environment ${index + 1} id`,
829
+ existingEnvironment?.id || (index === 0 ? "default" : `env-${index + 1}`),
830
+ ),
831
+ `deploy environment ${index + 1} id`,
832
+ );
833
+ const name = cleanText(
834
+ await prompt.ask(`Deploy environment ${index + 1} name`, existingEnvironment?.name || id),
835
+ );
836
+ const kind = await prompt.askChoice(
837
+ `Deploy environment ${index + 1} provider`,
838
+ DEPLOY_ENVIRONMENT_KINDS,
839
+ existingEnvironment?.kind || "custom",
840
+ );
841
+ const isDefault = await prompt.askBoolean(
842
+ `Mark deploy environment ${id} as the default?`,
843
+ existingEnvironment?.isDefault === true || (index === 0 && base.deployEnvironments.length === 0),
844
+ );
845
+ const notes = cleanText(
846
+ await prompt.ask(
847
+ `Deploy environment ${id} notes`,
848
+ existingEnvironment?.notes || "",
849
+ ),
850
+ );
851
+ deployEnvironments.push({
852
+ id,
853
+ name,
854
+ kind,
855
+ isDefault,
856
+ notes: notes || null,
857
+ });
858
+ }
859
+ const profile = writeProjectProfile(
860
+ {
861
+ ...base,
862
+ newProject,
863
+ defaultOversightMode,
864
+ defaultTerminalSurface,
865
+ deployEnvironments,
866
+ plannerDefaults: {
867
+ template,
868
+ lane,
869
+ },
870
+ },
871
+ { config },
872
+ );
873
+ return profile;
874
+ } finally {
875
+ await prompt.close();
876
+ }
877
+ }
878
+
879
+ async function collectComponentPromotions({ prompt, matrix, template, waveNumber }) {
880
+ const targetLevel = defaultTargetLevel(template);
881
+ const promotionCount = await prompt.askInteger("How many component promotions belong in this wave?", 1, {
882
+ min: 1,
883
+ });
884
+ const componentPromotions = [];
885
+ const componentCatalog = [];
886
+ for (let index = 0; index < promotionCount; index += 1) {
887
+ const componentId = normalizeComponentId(
888
+ await prompt.ask(
889
+ `Promotion ${index + 1} component id`,
890
+ index === 0 ? "new-component" : `component-${index + 1}`,
891
+ ),
892
+ `promotion ${index + 1} component id`,
893
+ );
894
+ const existing = matrix.components[componentId] || null;
895
+ const title = cleanText(
896
+ await prompt.ask(`Component ${componentId} title`, existing?.title || componentId),
897
+ );
898
+ const currentLevel = existing?.currentLevel
899
+ ? existing.currentLevel
900
+ : await prompt.askChoice(
901
+ `Component ${componentId} current level`,
902
+ matrix.levels,
903
+ "inventoried",
904
+ );
905
+ const target = await prompt.askChoice(
906
+ `Wave ${waveNumber} target level for ${componentId}`,
907
+ matrix.levels,
908
+ existing?.promotions?.find((promotion) => promotion.wave === waveNumber)?.target || targetLevel,
909
+ );
910
+ const canonicalDocs = normalizeRepoPathList(
911
+ normalizeListText(
912
+ await prompt.ask(
913
+ `Canonical docs for ${componentId} (comma or | separated)`,
914
+ (existing?.canonicalDocs || ["README.md"]).join(", "),
915
+ ),
916
+ ),
917
+ `${componentId}.canonicalDocs`,
918
+ );
919
+ const proofSurfaces = normalizeListText(
920
+ await prompt.ask(
921
+ `Proof surfaces for ${componentId} (comma or | separated)`,
922
+ (existing?.proofSurfaces || ["tests"]).join(", "),
923
+ ),
924
+ );
925
+ componentPromotions.push({ componentId, targetLevel: target });
926
+ componentCatalog.push({
927
+ componentId,
928
+ title,
929
+ currentLevel,
930
+ targetLevel: target,
931
+ canonicalDocs,
932
+ proofSurfaces,
933
+ });
934
+ }
935
+ return { componentPromotions, componentCatalog };
936
+ }
937
+
938
+ async function collectWorkerAgents({ prompt, template, profile, componentPromotions, waveNumber }) {
939
+ const defaultRoleKind = defaultWorkerRoleKindForTemplate(template);
940
+ const workerCount = await prompt.askInteger("How many worker agents should this wave include?", 1, {
941
+ min: 1,
942
+ });
943
+ const agentDefaultsByIndex = Array.from({ length: workerCount }, (_, index) => ({
944
+ agentId: `A${index + 1}`,
945
+ title: defaultWorkerTitle(template, index),
946
+ }));
947
+ const workerAgents = [];
948
+ for (let index = 0; index < workerCount; index += 1) {
949
+ const defaults = agentDefaultsByIndex[index];
950
+ const agentId = cleanText(await prompt.ask(`Worker ${index + 1} agent id`, defaults.agentId));
951
+ const title = cleanText(await prompt.ask(`Worker ${agentId} title`, defaults.title));
952
+ const roleKind = await prompt.askChoice(
953
+ `Worker ${agentId} role kind`,
954
+ ["implementation", "qa", "infra", "deploy", "research"],
955
+ defaultRoleKind,
956
+ );
957
+ const executorProfile = await prompt.askChoice(
958
+ `Worker ${agentId} executor profile`,
959
+ ["implement-fast", "deep-review", "docs-pass", "ops-triage"],
960
+ defaultExecutorProfile(roleKind),
961
+ );
962
+ const ownedPaths = normalizeRepoPathList(
963
+ normalizeListText(
964
+ await prompt.ask(
965
+ `Worker ${agentId} owned paths (comma or | separated)`,
966
+ template === "infra"
967
+ ? "scripts/,docs/plans/"
968
+ : template === "release"
969
+ ? "CHANGELOG.md,README.md"
970
+ : "README.md,scripts/",
971
+ ),
972
+ ),
973
+ `${agentId}.ownedPaths`,
974
+ );
975
+ const components = normalizeListText(
976
+ await prompt.ask(
977
+ `Worker ${agentId} component ids (comma or | separated)`,
978
+ componentPromotions.map((promotion) => promotion.componentId).join(", "),
979
+ ),
980
+ ).map((componentId) => normalizeComponentId(componentId, `${agentId}.components`));
981
+ const capabilities = normalizeListText(
982
+ await prompt.ask(`Worker ${agentId} capabilities (comma or | separated)`, roleKind === "implementation" ? "" : roleKind),
983
+ );
984
+ const additionalContext = normalizeRepoPathList(
985
+ normalizeListText(
986
+ await prompt.ask(
987
+ `Worker ${agentId} additional required context docs (comma or | separated)`,
988
+ template === "qa"
989
+ ? "docs/plans/current-state.md,docs/plans/master-plan.md"
990
+ : "docs/plans/current-state.md",
991
+ ),
992
+ ),
993
+ `${agentId}.requiredContext`,
994
+ );
995
+ const earlierWaveOutputs = normalizeRepoPathList(
996
+ normalizeListText(
997
+ await prompt.ask(`Worker ${agentId} earlier wave outputs to read (comma or | separated)`, ""),
998
+ ),
999
+ `${agentId}.earlierWaveOutputs`,
1000
+ );
1001
+ const requirements = normalizePipeList(
1002
+ await prompt.ask(
1003
+ `Worker ${agentId} requirements (use | between items)`,
1004
+ "Keep ownership explicit | Leave exact proof and doc deltas in the final output",
1005
+ ),
1006
+ );
1007
+ const validationCommand = cleanText(
1008
+ await prompt.ask(
1009
+ `Worker ${agentId} validation command`,
1010
+ buildDefaultValidationCommand(template, roleKind),
1011
+ ),
1012
+ );
1013
+ const outputSummary = cleanText(
1014
+ await prompt.ask(
1015
+ `Worker ${agentId} output summary`,
1016
+ buildDefaultOutputSummary(template, roleKind),
1017
+ ),
1018
+ );
1019
+ const primaryGoal = cleanText(
1020
+ await prompt.ask(
1021
+ `Worker ${agentId} primary goal`,
1022
+ buildDefaultPrimaryGoal(template, roleKind, title),
1023
+ ),
1024
+ );
1025
+ let deployEnvironmentId = null;
1026
+ if ((roleKind === "infra" || roleKind === "deploy") && profile.deployEnvironments.length > 0) {
1027
+ const deployChoices = ["none", ...profile.deployEnvironments.map((environment) => environment.id)];
1028
+ const defaultEnvironment =
1029
+ profile.deployEnvironments.find((environment) => environment.isDefault)?.id || "none";
1030
+ const selectedEnvironment = await prompt.askChoice(
1031
+ `Worker ${agentId} deploy environment`,
1032
+ deployChoices,
1033
+ defaultEnvironment,
1034
+ );
1035
+ deployEnvironmentId = selectedEnvironment === "none" ? null : selectedEnvironment;
1036
+ }
1037
+ const context7Bundle = await prompt.askChoice(
1038
+ `Worker ${agentId} Context7 bundle`,
1039
+ ["none"],
1040
+ "none",
1041
+ );
1042
+ const context7Query = cleanText(await prompt.ask(`Worker ${agentId} Context7 query`, ""));
1043
+ const exitDefaults = defaultExitContract(roleKind);
1044
+ const exitContract = {
1045
+ completion: await prompt.askChoice(
1046
+ `Worker ${agentId} exit completion`,
1047
+ EXIT_CONTRACT_COMPLETION_VALUES,
1048
+ exitDefaults.completion,
1049
+ ),
1050
+ durability: await prompt.askChoice(
1051
+ `Worker ${agentId} exit durability`,
1052
+ EXIT_CONTRACT_DURABILITY_VALUES,
1053
+ exitDefaults.durability,
1054
+ ),
1055
+ proof: await prompt.askChoice(
1056
+ `Worker ${agentId} exit proof`,
1057
+ EXIT_CONTRACT_PROOF_VALUES,
1058
+ exitDefaults.proof,
1059
+ ),
1060
+ docImpact: await prompt.askChoice(
1061
+ `Worker ${agentId} exit doc impact`,
1062
+ EXIT_CONTRACT_DOC_IMPACT_VALUES,
1063
+ exitDefaults.docImpact,
1064
+ ),
1065
+ };
1066
+ workerAgents.push({
1067
+ agentId,
1068
+ title,
1069
+ roleKind,
1070
+ executorProfile,
1071
+ ownedPaths,
1072
+ components,
1073
+ capabilities,
1074
+ additionalContext,
1075
+ earlierWaveOutputs,
1076
+ requirements,
1077
+ validationCommand,
1078
+ outputSummary,
1079
+ primaryGoal,
1080
+ deployEnvironmentId,
1081
+ context7Bundle,
1082
+ context7Query,
1083
+ exitContract,
1084
+ });
1085
+ }
1086
+ return workerAgents;
1087
+ }
1088
+
1089
+ async function runDraftFlow(options = {}) {
1090
+ const config = options.config || loadWaveConfig();
1091
+ const profile = await ensureProjectProfile({ config });
1092
+ const waveNumber = options.wave;
1093
+ const lane = options.lane || profile.plannerDefaults.lane || config.defaultLane;
1094
+ const lanePaths = buildLanePaths(lane, { config });
1095
+ const matrix = loadComponentCutoverMatrix({ laneProfile: lanePaths.laneProfile });
1096
+ const template = normalizeDraftTemplate(options.template || profile.plannerDefaults.template);
1097
+ const prompt = new PromptSession();
1098
+ try {
1099
+ const { wavePath, specPath } = ensureWavePaths(lanePaths, waveNumber);
1100
+ if (!options.force && (fs.existsSync(wavePath) || fs.existsSync(specPath))) {
1101
+ throw new Error(
1102
+ `Wave ${waveNumber} already exists. Re-run with --force to overwrite ${path.relative(REPO_ROOT, wavePath)} and ${path.relative(REPO_ROOT, specPath)}.`,
1103
+ );
1104
+ }
1105
+ const title = cleanText(
1106
+ await prompt.ask(
1107
+ "Wave title",
1108
+ template === "qa"
1109
+ ? "QA Closure"
1110
+ : template === "infra"
1111
+ ? "Infra Planning Slice"
1112
+ : template === "release"
1113
+ ? "Release Readiness"
1114
+ : "Implementation Slice",
1115
+ ),
1116
+ );
1117
+ const commitMessage = cleanText(
1118
+ await prompt.ask(
1119
+ "Commit message",
1120
+ template === "release"
1121
+ ? "Release: prepare next cut"
1122
+ : template === "qa"
1123
+ ? "Test: validate planned slice"
1124
+ : "Feat: land planned slice",
1125
+ ),
1126
+ );
1127
+ const sequencingNote = cleanText(await prompt.ask("Sequencing note", ""));
1128
+ const referenceRule = cleanText(await prompt.ask("Reference rule", ""));
1129
+ const oversightMode = normalizeOversightMode(
1130
+ await prompt.askChoice(
1131
+ "Wave execution posture",
1132
+ PROJECT_OVERSIGHT_MODES,
1133
+ profile.defaultOversightMode,
1134
+ ),
1135
+ );
1136
+ const context7Bundle = await prompt.askChoice("Wave Context7 bundle", ["none"], "none");
1137
+ const context7Query = cleanText(await prompt.ask("Wave Context7 query", ""));
1138
+ const standardRoles = {
1139
+ evaluator: await prompt.askBoolean("Use the standard evaluator role?", true),
1140
+ integration: await prompt.askBoolean("Use the standard integration role?", true),
1141
+ documentation: await prompt.askBoolean("Use the standard documentation role?", true),
1142
+ };
1143
+ const { componentPromotions, componentCatalog } = await collectComponentPromotions({
1144
+ prompt,
1145
+ matrix,
1146
+ template,
1147
+ waveNumber,
1148
+ });
1149
+ const workerAgents = await collectWorkerAgents({
1150
+ prompt,
1151
+ template,
1152
+ profile,
1153
+ componentPromotions,
1154
+ waveNumber,
1155
+ });
1156
+ const draftValues = {
1157
+ wave: waveNumber,
1158
+ lane: lanePaths.lane,
1159
+ template,
1160
+ title,
1161
+ commitMessage,
1162
+ sequencingNote,
1163
+ referenceRule,
1164
+ oversightMode,
1165
+ context7Bundle,
1166
+ context7Query,
1167
+ standardRoles,
1168
+ componentPromotions,
1169
+ componentCatalog,
1170
+ workerAgents,
1171
+ };
1172
+ const spec = buildSpecPayload({
1173
+ config,
1174
+ lanePaths,
1175
+ profile,
1176
+ draftValues,
1177
+ });
1178
+ const markdown = renderWaveMarkdown(spec, lanePaths);
1179
+ const nextMatrixPayload = upsertComponentMatrix(matrix, spec);
1180
+ ensureDirectory(path.dirname(specPath));
1181
+ writeJsonAtomic(specPath, spec);
1182
+ writeTextAtomic(wavePath, markdown);
1183
+ writeJsonAtomic(path.resolve(REPO_ROOT, lanePaths.componentCutoverMatrixJsonPath), nextMatrixPayload);
1184
+ writeTextAtomic(
1185
+ path.resolve(REPO_ROOT, lanePaths.componentCutoverMatrixDocPath),
1186
+ `${renderComponentMatrixMarkdown(nextMatrixPayload)}\n`,
1187
+ );
1188
+ const parsedWave = parseWaveFile(wavePath, { laneProfile: lanePaths.laneProfile });
1189
+ validateWaveDefinition(
1190
+ applyExecutorSelectionsToWave(parsedWave, { laneProfile: lanePaths.laneProfile }),
1191
+ { laneProfile: lanePaths.laneProfile },
1192
+ );
1193
+ updateProjectProfile(
1194
+ (current) => ({
1195
+ ...current,
1196
+ plannerDefaults: {
1197
+ template,
1198
+ lane: lanePaths.lane,
1199
+ },
1200
+ }),
1201
+ { config },
1202
+ );
1203
+ return {
1204
+ wave: waveNumber,
1205
+ lane: lanePaths.lane,
1206
+ template,
1207
+ wavePath: path.relative(REPO_ROOT, wavePath),
1208
+ specPath: path.relative(REPO_ROOT, specPath),
1209
+ matrixJsonPath: path.relative(REPO_ROOT, path.resolve(REPO_ROOT, lanePaths.componentCutoverMatrixJsonPath)),
1210
+ matrixDocPath: path.relative(REPO_ROOT, path.resolve(REPO_ROOT, lanePaths.componentCutoverMatrixDocPath)),
1211
+ profilePath: path.relative(REPO_ROOT, PROJECT_PROFILE_PATH),
1212
+ };
1213
+ } finally {
1214
+ await prompt.close();
1215
+ }
1216
+ }
1217
+
1218
+ function printPlannerHelp() {
1219
+ console.log(`Usage:
1220
+ wave project setup [--json]
1221
+ wave project show [--json]
1222
+ wave draft --wave <n> [--lane <lane>] [--template implementation|qa|infra|release] [--force] [--json]
1223
+ `);
1224
+ }
1225
+
1226
+ export async function runPlannerCli(argv) {
1227
+ const args = Array.isArray(argv) ? argv.slice() : [];
1228
+ const subcommand = cleanText(args.shift()).toLowerCase();
1229
+ const options = {
1230
+ json: false,
1231
+ force: false,
1232
+ wave: null,
1233
+ lane: null,
1234
+ template: null,
1235
+ };
1236
+ if (!subcommand) {
1237
+ printPlannerHelp();
1238
+ return;
1239
+ }
1240
+ if (subcommand === "project") {
1241
+ const action = cleanText(args.shift()).toLowerCase();
1242
+ for (const arg of args) {
1243
+ if (arg === "--json") {
1244
+ options.json = true;
1245
+ } else if (arg === "--help" || arg === "-h") {
1246
+ printPlannerHelp();
1247
+ return;
1248
+ } else {
1249
+ throw new Error(`Unknown argument: ${arg}`);
1250
+ }
1251
+ }
1252
+ if (action === "setup") {
1253
+ const profile = await runProjectSetupFlow({ json: options.json });
1254
+ if (options.json) {
1255
+ printJson({
1256
+ profilePath: path.relative(REPO_ROOT, PROJECT_PROFILE_PATH),
1257
+ profile,
1258
+ });
1259
+ return;
1260
+ }
1261
+ console.log(`[wave:project] profile: ${path.relative(REPO_ROOT, PROJECT_PROFILE_PATH)}`);
1262
+ console.log(`[wave:project] lane=${profile.plannerDefaults.lane}`);
1263
+ console.log(`[wave:project] template=${profile.plannerDefaults.template}`);
1264
+ console.log(`[wave:project] oversight=${profile.defaultOversightMode}`);
1265
+ console.log(`[wave:project] terminal_surface=${profile.defaultTerminalSurface}`);
1266
+ return;
1267
+ }
1268
+ if (action === "show") {
1269
+ const profile = readProjectProfile();
1270
+ if (options.json) {
1271
+ printJson({
1272
+ profilePath: path.relative(REPO_ROOT, PROJECT_PROFILE_PATH),
1273
+ profile,
1274
+ });
1275
+ return;
1276
+ }
1277
+ if (!profile) {
1278
+ console.log(`[wave:project] no saved profile at ${path.relative(REPO_ROOT, PROJECT_PROFILE_PATH)}`);
1279
+ return;
1280
+ }
1281
+ console.log(`[wave:project] profile: ${path.relative(REPO_ROOT, PROJECT_PROFILE_PATH)}`);
1282
+ console.log(`[wave:project] project=${profile.source.projectName}`);
1283
+ console.log(`[wave:project] lane=${profile.plannerDefaults.lane}`);
1284
+ console.log(`[wave:project] template=${profile.plannerDefaults.template}`);
1285
+ console.log(`[wave:project] new_project=${profile.newProject ? "yes" : "no"}`);
1286
+ console.log(`[wave:project] deploy_envs=${profile.deployEnvironments.length}`);
1287
+ return;
1288
+ }
1289
+ throw new Error(`Unknown project action: ${action || "(empty)"}`);
1290
+ }
1291
+ if (subcommand !== "draft") {
1292
+ throw new Error(`Unknown planner subcommand: ${subcommand}`);
1293
+ }
1294
+ for (let index = 0; index < args.length; index += 1) {
1295
+ const arg = args[index];
1296
+ if (arg === "--json") {
1297
+ options.json = true;
1298
+ } else if (arg === "--force") {
1299
+ options.force = true;
1300
+ } else if (arg === "--wave") {
1301
+ options.wave = Number.parseInt(String(args[++index] || ""), 10);
1302
+ } else if (arg === "--lane") {
1303
+ options.lane = cleanText(args[++index]);
1304
+ } else if (arg === "--template") {
1305
+ options.template = cleanText(args[++index]);
1306
+ } else if (arg === "--help" || arg === "-h") {
1307
+ printPlannerHelp();
1308
+ return;
1309
+ } else {
1310
+ throw new Error(`Unknown argument: ${arg}`);
1311
+ }
1312
+ }
1313
+ if (!Number.isFinite(options.wave) || options.wave < 0) {
1314
+ throw new Error("--wave <n> is required for `wave draft`.");
1315
+ }
1316
+ const result = await runDraftFlow(options);
1317
+ if (options.json) {
1318
+ printJson(result);
1319
+ return;
1320
+ }
1321
+ console.log(`[wave:draft] wave=${result.wave}`);
1322
+ console.log(`[wave:draft] lane=${result.lane}`);
1323
+ console.log(`[wave:draft] template=${result.template}`);
1324
+ console.log(`[wave:draft] markdown=${result.wavePath}`);
1325
+ console.log(`[wave:draft] spec=${result.specPath}`);
1326
+ console.log(`[wave:draft] matrix_json=${result.matrixJsonPath}`);
1327
+ console.log(`[wave:draft] matrix_md=${result.matrixDocPath}`);
1328
+ }