@chllming/wave-orchestration 0.5.4 → 0.6.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 (126) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/README.md +33 -5
  3. package/docs/README.md +18 -4
  4. package/docs/agents/wave-cont-eval-role.md +36 -0
  5. package/docs/agents/{wave-evaluator-role.md → wave-cont-qa-role.md} +14 -11
  6. package/docs/agents/wave-documentation-role.md +1 -1
  7. package/docs/agents/wave-infra-role.md +1 -1
  8. package/docs/agents/wave-integration-role.md +3 -3
  9. package/docs/agents/wave-launcher-role.md +4 -3
  10. package/docs/agents/wave-security-role.md +40 -0
  11. package/docs/concepts/context7-vs-skills.md +1 -1
  12. package/docs/concepts/what-is-a-wave.md +56 -6
  13. package/docs/evals/README.md +166 -0
  14. package/docs/evals/benchmark-catalog.json +663 -0
  15. package/docs/guides/author-and-run-waves.md +135 -0
  16. package/docs/guides/planner.md +5 -0
  17. package/docs/guides/terminal-surfaces.md +2 -0
  18. package/docs/plans/component-cutover-matrix.json +1 -1
  19. package/docs/plans/component-cutover-matrix.md +1 -1
  20. package/docs/plans/current-state.md +19 -1
  21. package/docs/plans/examples/wave-example-live-proof.md +435 -0
  22. package/docs/plans/migration.md +42 -0
  23. package/docs/plans/wave-orchestrator.md +46 -7
  24. package/docs/plans/waves/wave-0.md +4 -4
  25. package/docs/reference/live-proof-waves.md +177 -0
  26. package/docs/reference/migration-0.2-to-0.5.md +26 -19
  27. package/docs/reference/npmjs-trusted-publishing.md +6 -5
  28. package/docs/reference/runtime-config/README.md +14 -4
  29. package/docs/reference/sample-waves.md +87 -0
  30. package/docs/reference/skills.md +110 -42
  31. package/docs/research/agent-context-sources.md +130 -11
  32. package/docs/research/coordination-failure-review.md +266 -0
  33. package/docs/roadmap.md +6 -2
  34. package/package.json +2 -2
  35. package/releases/manifest.json +35 -2
  36. package/scripts/research/agent-context-archive.mjs +83 -1
  37. package/scripts/research/manifests/agent-context-expanded-2026-03-22.mjs +811 -0
  38. package/scripts/wave-orchestrator/adhoc.mjs +1331 -0
  39. package/scripts/wave-orchestrator/agent-state.mjs +358 -6
  40. package/scripts/wave-orchestrator/artifact-schemas.mjs +173 -0
  41. package/scripts/wave-orchestrator/clarification-triage.mjs +10 -3
  42. package/scripts/wave-orchestrator/config.mjs +48 -12
  43. package/scripts/wave-orchestrator/context7.mjs +2 -0
  44. package/scripts/wave-orchestrator/coord-cli.mjs +51 -19
  45. package/scripts/wave-orchestrator/coordination-store.mjs +26 -4
  46. package/scripts/wave-orchestrator/coordination.mjs +83 -9
  47. package/scripts/wave-orchestrator/dashboard-state.mjs +20 -8
  48. package/scripts/wave-orchestrator/dep-cli.mjs +5 -2
  49. package/scripts/wave-orchestrator/docs-queue.mjs +8 -2
  50. package/scripts/wave-orchestrator/evals.mjs +451 -0
  51. package/scripts/wave-orchestrator/feedback.mjs +15 -1
  52. package/scripts/wave-orchestrator/install.mjs +32 -9
  53. package/scripts/wave-orchestrator/launcher-closure.mjs +281 -0
  54. package/scripts/wave-orchestrator/launcher-runtime.mjs +334 -0
  55. package/scripts/wave-orchestrator/launcher.mjs +709 -601
  56. package/scripts/wave-orchestrator/ledger.mjs +123 -20
  57. package/scripts/wave-orchestrator/local-executor.mjs +99 -12
  58. package/scripts/wave-orchestrator/planner.mjs +177 -42
  59. package/scripts/wave-orchestrator/replay.mjs +6 -3
  60. package/scripts/wave-orchestrator/role-helpers.mjs +84 -0
  61. package/scripts/wave-orchestrator/shared.mjs +75 -11
  62. package/scripts/wave-orchestrator/skills.mjs +637 -106
  63. package/scripts/wave-orchestrator/traces.mjs +71 -48
  64. package/scripts/wave-orchestrator/wave-files.mjs +947 -101
  65. package/scripts/wave.mjs +9 -0
  66. package/skills/README.md +202 -0
  67. package/skills/provider-aws/SKILL.md +111 -0
  68. package/skills/provider-aws/adapters/claude.md +1 -0
  69. package/skills/provider-aws/adapters/codex.md +1 -0
  70. package/skills/provider-aws/references/service-verification.md +39 -0
  71. package/skills/provider-aws/skill.json +50 -1
  72. package/skills/provider-custom-deploy/SKILL.md +59 -0
  73. package/skills/provider-custom-deploy/skill.json +46 -1
  74. package/skills/provider-docker-compose/SKILL.md +90 -0
  75. package/skills/provider-docker-compose/adapters/local.md +1 -0
  76. package/skills/provider-docker-compose/skill.json +49 -1
  77. package/skills/provider-github-release/SKILL.md +116 -1
  78. package/skills/provider-github-release/adapters/claude.md +1 -0
  79. package/skills/provider-github-release/adapters/codex.md +1 -0
  80. package/skills/provider-github-release/skill.json +51 -1
  81. package/skills/provider-kubernetes/SKILL.md +137 -0
  82. package/skills/provider-kubernetes/adapters/claude.md +1 -0
  83. package/skills/provider-kubernetes/adapters/codex.md +1 -0
  84. package/skills/provider-kubernetes/references/kubectl-patterns.md +58 -0
  85. package/skills/provider-kubernetes/skill.json +48 -1
  86. package/skills/provider-railway/SKILL.md +118 -1
  87. package/skills/provider-railway/references/verification-commands.md +39 -0
  88. package/skills/provider-railway/skill.json +67 -1
  89. package/skills/provider-ssh-manual/SKILL.md +91 -0
  90. package/skills/provider-ssh-manual/skill.json +50 -1
  91. package/skills/repo-coding-rules/SKILL.md +84 -0
  92. package/skills/repo-coding-rules/skill.json +30 -1
  93. package/skills/role-cont-eval/SKILL.md +90 -0
  94. package/skills/role-cont-eval/adapters/codex.md +1 -0
  95. package/skills/role-cont-eval/skill.json +36 -0
  96. package/skills/role-cont-qa/SKILL.md +93 -0
  97. package/skills/role-cont-qa/adapters/claude.md +1 -0
  98. package/skills/role-cont-qa/skill.json +36 -0
  99. package/skills/role-deploy/SKILL.md +90 -0
  100. package/skills/role-deploy/skill.json +32 -1
  101. package/skills/role-documentation/SKILL.md +66 -0
  102. package/skills/role-documentation/skill.json +32 -1
  103. package/skills/role-implementation/SKILL.md +62 -0
  104. package/skills/role-implementation/skill.json +32 -1
  105. package/skills/role-infra/SKILL.md +74 -0
  106. package/skills/role-infra/skill.json +32 -1
  107. package/skills/role-integration/SKILL.md +79 -1
  108. package/skills/role-integration/skill.json +32 -1
  109. package/skills/role-research/SKILL.md +58 -0
  110. package/skills/role-research/skill.json +32 -1
  111. package/skills/role-security/SKILL.md +60 -0
  112. package/skills/role-security/skill.json +36 -0
  113. package/skills/runtime-claude/SKILL.md +60 -1
  114. package/skills/runtime-claude/skill.json +32 -1
  115. package/skills/runtime-codex/SKILL.md +52 -1
  116. package/skills/runtime-codex/skill.json +32 -1
  117. package/skills/runtime-local/SKILL.md +39 -0
  118. package/skills/runtime-local/skill.json +32 -1
  119. package/skills/runtime-opencode/SKILL.md +51 -0
  120. package/skills/runtime-opencode/skill.json +32 -1
  121. package/skills/wave-core/SKILL.md +107 -0
  122. package/skills/wave-core/references/marker-syntax.md +62 -0
  123. package/skills/wave-core/skill.json +31 -1
  124. package/wave.config.json +35 -6
  125. package/skills/role-evaluator/SKILL.md +0 -6
  126. package/skills/role-evaluator/skill.json +0 -5
@@ -0,0 +1,1331 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import readline from "node:readline/promises";
5
+ import { stdin, stderr } from "node:process";
6
+ import { loadWaveConfig } from "./config.mjs";
7
+ import {
8
+ buildDefaultProjectProfile,
9
+ readProjectProfile,
10
+ } from "./project-profile.mjs";
11
+ import { runLauncherCli } from "./launcher.mjs";
12
+ import { renderWaveMarkdown } from "./planner.mjs";
13
+ import {
14
+ applyExecutorSelectionsToWave,
15
+ parseWaveFile,
16
+ requiredDocumentationStewardPathsForWave,
17
+ SHARED_PLAN_DOC_PATHS,
18
+ validateWaveDefinition,
19
+ } from "./wave-files.mjs";
20
+ import {
21
+ buildLanePaths,
22
+ compactSingleLine,
23
+ ensureDirectory,
24
+ parseNonNegativeInt,
25
+ readJsonOrNull,
26
+ REPO_ROOT,
27
+ sanitizeAdhocRunId,
28
+ toIsoTimestamp,
29
+ writeJsonAtomic,
30
+ writeTextAtomic,
31
+ } from "./shared.mjs";
32
+
33
+ const ADHOC_SCHEMA_VERSION = 1;
34
+ const ADHOC_WAVE_NUMBER = 0;
35
+ const TASK_ROLE_VALUES = ["implementation", "research", "infra", "deploy"];
36
+ const SECURITY_KEYWORDS = [
37
+ "auth",
38
+ "authn",
39
+ "authz",
40
+ "permission",
41
+ "secret",
42
+ "token",
43
+ "credential",
44
+ "security",
45
+ "vuln",
46
+ "vulnerability",
47
+ "inject",
48
+ "injection",
49
+ "xss",
50
+ "csrf",
51
+ "oauth",
52
+ "login",
53
+ "shell",
54
+ "command",
55
+ "file upload",
56
+ "external input",
57
+ "sensitive",
58
+ ];
59
+ const EVAL_KEYWORDS = [
60
+ "eval",
61
+ "benchmark",
62
+ "quality",
63
+ "tune",
64
+ "tuning",
65
+ "score",
66
+ "regression",
67
+ "latency",
68
+ "output",
69
+ "compare",
70
+ ];
71
+ const RESEARCH_KEYWORDS = [
72
+ "investigate",
73
+ "analysis",
74
+ "analyze",
75
+ "research",
76
+ "root cause",
77
+ "triage",
78
+ "audit",
79
+ "inspect",
80
+ "review",
81
+ ];
82
+ const DEPLOY_KEYWORDS = [
83
+ "deploy",
84
+ "deployment",
85
+ "release",
86
+ "rollout",
87
+ "publish",
88
+ "ship",
89
+ "domain",
90
+ "rollback",
91
+ "production",
92
+ "prod",
93
+ ];
94
+ const INFRA_KEYWORDS = [
95
+ "infra",
96
+ "infrastructure",
97
+ "environment",
98
+ "env",
99
+ "kubernetes",
100
+ "docker",
101
+ "compose",
102
+ "cluster",
103
+ "service",
104
+ "ci",
105
+ "workflow",
106
+ "pipeline",
107
+ "migration",
108
+ "ops",
109
+ ];
110
+ const DOC_KEYWORDS = [
111
+ "doc",
112
+ "docs",
113
+ "readme",
114
+ "guide",
115
+ "reference",
116
+ "changelog",
117
+ "write up",
118
+ ];
119
+ const PROVIDER_KEYWORDS = [
120
+ ["railway", "railway-cli"],
121
+ ["docker compose", "docker-compose"],
122
+ ["docker-compose", "docker-compose"],
123
+ ["kubernetes", "kubernetes"],
124
+ ["k8s", "kubernetes"],
125
+ ["aws", "aws"],
126
+ ["github release", "github-release"],
127
+ ["release artifact", "github-release"],
128
+ ["ssh", "ssh-manual"],
129
+ ];
130
+ const GENERATED_SPECIAL_AGENT_TITLES = new Set([
131
+ "cont-QA",
132
+ "cont-EVAL",
133
+ "Integration Steward",
134
+ "Documentation Steward",
135
+ "Security Reviewer",
136
+ ]);
137
+
138
+ function cleanText(value) {
139
+ return String(value ?? "").trim();
140
+ }
141
+
142
+ function uniqueStrings(values) {
143
+ return Array.from(
144
+ new Set(
145
+ (Array.isArray(values) ? values : [])
146
+ .map((value) => cleanText(value))
147
+ .filter(Boolean),
148
+ ),
149
+ );
150
+ }
151
+
152
+ function repoRelativePath(targetPath) {
153
+ return path.relative(REPO_ROOT, targetPath).replaceAll(path.sep, "/");
154
+ }
155
+
156
+ function normalizeRepoRelativePath(value, label) {
157
+ const normalized = cleanText(value)
158
+ .replaceAll("\\", "/")
159
+ .replace(/^\.\/+/, "");
160
+ if (!normalized) {
161
+ throw new Error(`${label} is required`);
162
+ }
163
+ if (normalized.startsWith("/") || normalized.startsWith("../") || normalized.includes("/../")) {
164
+ throw new Error(`${label} must stay inside the repository`);
165
+ }
166
+ return normalized;
167
+ }
168
+
169
+ function isLikelyExternalPathHint(value) {
170
+ const normalized = cleanText(value).replaceAll("\\", "/");
171
+ if (!normalized) {
172
+ return false;
173
+ }
174
+ if (/^[a-z][a-z0-9+.-]*:/i.test(normalized) || normalized.startsWith("//")) {
175
+ return true;
176
+ }
177
+ const firstSegment = normalized.split("/")[0] || "";
178
+ return (
179
+ !firstSegment.startsWith(".") &&
180
+ /^(?:www\.)?(?:[a-z0-9-]+\.)+[a-z]{2,}$/i.test(firstSegment)
181
+ );
182
+ }
183
+
184
+ function buildAdhocRunId() {
185
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
186
+ const random = crypto.randomBytes(3).toString("hex");
187
+ return sanitizeAdhocRunId(`adhoc-${stamp}-${random}`);
188
+ }
189
+
190
+ function readEffectiveProjectProfile(config) {
191
+ return readProjectProfile({ config }) || buildDefaultProjectProfile(config);
192
+ }
193
+
194
+ function detectKeyword(text, keywords) {
195
+ return keywords.some((keyword) => text.includes(keyword));
196
+ }
197
+
198
+ function inferDeployKind(taskText, profile) {
199
+ for (const [needle, kind] of PROVIDER_KEYWORDS) {
200
+ if (taskText.includes(needle)) {
201
+ return kind;
202
+ }
203
+ }
204
+ if (Array.isArray(profile?.deployEnvironments) && profile.deployEnvironments.length > 0) {
205
+ return (
206
+ profile.deployEnvironments.find((entry) => entry.isDefault)?.kind ||
207
+ profile.deployEnvironments[0]?.kind ||
208
+ null
209
+ );
210
+ }
211
+ if (detectKeyword(taskText, DEPLOY_KEYWORDS) || detectKeyword(taskText, INFRA_KEYWORDS)) {
212
+ return "custom";
213
+ }
214
+ return null;
215
+ }
216
+
217
+ function extractRepoPathHints(task) {
218
+ const text = String(task || "");
219
+ const matches = [];
220
+ const pushPath = (value) => {
221
+ const normalized = cleanText(value).replace(/^["'`(]+|["'`),.:;]+$/g, "");
222
+ if (!normalized || isLikelyExternalPathHint(normalized)) {
223
+ return;
224
+ }
225
+ const looksLikePath =
226
+ normalized.includes("/") ||
227
+ /^(README|CHANGELOG|package|pnpm-workspace|tsconfig|wave\.config)\.[a-z0-9._-]+$/i.test(
228
+ normalized,
229
+ );
230
+ if (!looksLikePath) {
231
+ return;
232
+ }
233
+ let repoPath = null;
234
+ try {
235
+ repoPath = normalizeRepoRelativePath(normalized, "task path hint");
236
+ } catch {
237
+ return;
238
+ }
239
+ const lastSegment = repoPath.split("/").at(-1) || repoPath;
240
+ const looksLikeFile = lastSegment.includes(".");
241
+ matches.push(looksLikeFile || repoPath.endsWith("/") ? repoPath : `${repoPath}/`);
242
+ };
243
+ for (const match of text.matchAll(/`([^`]+)`/g)) {
244
+ pushPath(match[1]);
245
+ }
246
+ for (const match of text.matchAll(/(?:^|\s)([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?:\/)?)(?=$|\s)/g)) {
247
+ pushPath(match[1]);
248
+ }
249
+ for (const match of text.matchAll(
250
+ /(?:^|\s)((?:README|CHANGELOG|package|pnpm-workspace|tsconfig|wave\.config)\.[A-Za-z0-9._-]+)(?=$|\s)/g,
251
+ )) {
252
+ pushPath(match[1]);
253
+ }
254
+ return uniqueStrings(matches);
255
+ }
256
+
257
+ function fallbackOwnedPathsForRole(roleKind) {
258
+ if (roleKind === "deploy") {
259
+ return ["deploy/", ".github/", "scripts/", "docs/"];
260
+ }
261
+ if (roleKind === "infra") {
262
+ return ["infra/", "ops/", "deploy/", "scripts/", ".github/", "docs/"];
263
+ }
264
+ if (roleKind === "research") {
265
+ return ["docs/", "scripts/", "test/", "tests/"];
266
+ }
267
+ return [
268
+ "README.md",
269
+ "docs/",
270
+ "scripts/",
271
+ "src/",
272
+ "app/",
273
+ "lib/",
274
+ "packages/",
275
+ "services/",
276
+ "test/",
277
+ "tests/",
278
+ "package.json",
279
+ ];
280
+ }
281
+
282
+ function inferTaskRole(taskText) {
283
+ if (detectKeyword(taskText, DEPLOY_KEYWORDS)) {
284
+ return "deploy";
285
+ }
286
+ if (detectKeyword(taskText, INFRA_KEYWORDS)) {
287
+ return "infra";
288
+ }
289
+ if (detectKeyword(taskText, RESEARCH_KEYWORDS)) {
290
+ return "research";
291
+ }
292
+ return "implementation";
293
+ }
294
+
295
+ function inferTaskTitle(roleKind, taskText, index) {
296
+ const compactTask = compactSingleLine(taskText, 80);
297
+ if (detectKeyword(taskText, DOC_KEYWORDS) && roleKind === "implementation") {
298
+ return `Documentation Task ${index + 1}`;
299
+ }
300
+ if (roleKind === "deploy") {
301
+ return compactTask ? `Deploy Task ${index + 1}` : `Deploy Task ${index + 1}`;
302
+ }
303
+ if (roleKind === "infra") {
304
+ return compactTask ? `Infra Task ${index + 1}` : `Infra Task ${index + 1}`;
305
+ }
306
+ if (roleKind === "research") {
307
+ return compactTask ? `Research Task ${index + 1}` : `Research Task ${index + 1}`;
308
+ }
309
+ return compactTask ? `Implementation Task ${index + 1}` : `Implementation Task ${index + 1}`;
310
+ }
311
+
312
+ function defaultExecutorProfile(roleKind) {
313
+ if (roleKind === "infra" || roleKind === "deploy" || roleKind === "research") {
314
+ return "ops-triage";
315
+ }
316
+ return "implement-fast";
317
+ }
318
+
319
+ function defaultExitContract(roleKind) {
320
+ if (roleKind === "infra" || roleKind === "deploy") {
321
+ return {
322
+ completion: "live",
323
+ durability: "durable",
324
+ proof: "live",
325
+ docImpact: "owned",
326
+ };
327
+ }
328
+ return {
329
+ completion: "contract",
330
+ durability: "none",
331
+ proof: "unit",
332
+ docImpact: "owned",
333
+ };
334
+ }
335
+
336
+ function defaultValidationCommand(roleKind) {
337
+ if (roleKind === "research") {
338
+ return "Manual review of captured evidence, exact findings, and named follow-up owners.";
339
+ }
340
+ if (roleKind === "infra" || roleKind === "deploy") {
341
+ return "pnpm exec wave launch --dry-run --no-dashboard";
342
+ }
343
+ return "pnpm test";
344
+ }
345
+
346
+ function defaultOutputSummary(roleKind) {
347
+ if (roleKind === "research") {
348
+ return "Summarize the findings, the exact evidence consulted, and the recommended next owners.";
349
+ }
350
+ if (roleKind === "infra" || roleKind === "deploy") {
351
+ return "Summarize the environment or rollout proof, the operator-visible risks, and rollback posture.";
352
+ }
353
+ return "Summarize the landed implementation, proof state, and exact follow-up owners.";
354
+ }
355
+
356
+ function deriveTemplate(workerRoles) {
357
+ if (workerRoles.length > 0 && workerRoles.every((role) => role === "deploy")) {
358
+ return "release";
359
+ }
360
+ if (workerRoles.length > 0 && workerRoles.every((role) => role === "infra")) {
361
+ return "infra";
362
+ }
363
+ return "implementation";
364
+ }
365
+
366
+ function analyzeTask(task, index, profile, lanePaths) {
367
+ const normalizedTask = cleanText(task);
368
+ const loweredTask = normalizedTask.toLowerCase();
369
+ const roleKind = inferTaskRole(loweredTask);
370
+ const pathHints = extractRepoPathHints(normalizedTask);
371
+ const sharedPlanDocs = new Set(lanePaths?.sharedPlanDocs || []);
372
+ const nonSharedPlanPathHints = pathHints.filter((ownedPath) => !sharedPlanDocs.has(ownedPath));
373
+ const touchesSharedPlanDocs = pathHints.some((ownedPath) => sharedPlanDocs.has(ownedPath));
374
+ const ownedPaths =
375
+ nonSharedPlanPathHints.length > 0 ? nonSharedPlanPathHints : fallbackOwnedPathsForRole(roleKind);
376
+ return {
377
+ index,
378
+ task: normalizedTask,
379
+ roleKind,
380
+ title: inferTaskTitle(roleKind, normalizedTask, index),
381
+ ownedPaths,
382
+ needsSecurity:
383
+ detectKeyword(loweredTask, SECURITY_KEYWORDS) ||
384
+ roleKind === "deploy" ||
385
+ roleKind === "infra",
386
+ needsEval: detectKeyword(loweredTask, EVAL_KEYWORDS),
387
+ docsHeavy:
388
+ touchesSharedPlanDocs ||
389
+ detectKeyword(loweredTask, DOC_KEYWORDS) ||
390
+ ownedPaths.every((ownedPath) =>
391
+ ["docs/", "README.md", "CHANGELOG.md"].some((prefix) => ownedPath.startsWith(prefix)),
392
+ ),
393
+ deployKind: inferDeployKind(loweredTask, profile),
394
+ };
395
+ }
396
+
397
+ function resolveDeployEnvironments(profile, analyzedTasks) {
398
+ const explicitKinds = uniqueStrings(
399
+ analyzedTasks.map((task) => task.deployKind).filter(Boolean),
400
+ );
401
+ if (explicitKinds.length === 0) {
402
+ return [];
403
+ }
404
+ if (Array.isArray(profile?.deployEnvironments) && profile.deployEnvironments.length > 0) {
405
+ return profile.deployEnvironments
406
+ .filter((environment) => explicitKinds.includes(environment.kind))
407
+ .map((environment) => ({
408
+ id: environment.id,
409
+ kind: environment.kind,
410
+ isDefault: environment.isDefault === true,
411
+ notes: environment.notes || null,
412
+ }));
413
+ }
414
+ return explicitKinds.map((kind, index) => ({
415
+ id: index === 0 ? "adhoc-default" : `adhoc-${index + 1}`,
416
+ kind,
417
+ isDefault: index === 0,
418
+ notes: "Synthesized from the ad-hoc task request.",
419
+ }));
420
+ }
421
+
422
+ function relativeStatePath(targetPath) {
423
+ return repoRelativePath(targetPath).replaceAll("\\", "/");
424
+ }
425
+
426
+ function buildCommonRequiredContext() {
427
+ return Array.from(
428
+ new Set([
429
+ "docs/reference/repository-guidance.md",
430
+ "docs/research/agent-context-sources.md",
431
+ ...SHARED_PLAN_DOC_PATHS,
432
+ ]),
433
+ );
434
+ }
435
+
436
+ function buildDocumentationOwnedPaths({ lanePaths, waveNumber, mode }) {
437
+ const canonicalPaths = requiredDocumentationStewardPathsForWave(waveNumber, {
438
+ laneProfile: lanePaths.laneProfile,
439
+ });
440
+ if (mode === "roadmap") {
441
+ return canonicalPaths;
442
+ }
443
+ return uniqueStrings([
444
+ `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-doc-closure.md`,
445
+ ...canonicalPaths,
446
+ ]);
447
+ }
448
+
449
+ function buildSpecialAgents({
450
+ lanePaths,
451
+ waveNumber,
452
+ includeContEval,
453
+ includeSecurity,
454
+ evalTargets,
455
+ mode,
456
+ }) {
457
+ const commonRequiredContext = buildCommonRequiredContext();
458
+ const contQaReportPath =
459
+ mode === "roadmap"
460
+ ? `docs/plans/waves/reviews/wave-${waveNumber}-cont-qa.md`
461
+ : `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-cont-qa.md`;
462
+ const contEvalReportPath =
463
+ mode === "roadmap"
464
+ ? `docs/plans/waves/reviews/wave-${waveNumber}-cont-eval.md`
465
+ : `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-cont-eval.md`;
466
+ const documentationOwnedPaths = buildDocumentationOwnedPaths({
467
+ lanePaths,
468
+ waveNumber,
469
+ mode,
470
+ });
471
+ const securityReportPath =
472
+ mode === "roadmap"
473
+ ? relativeStatePath(path.join(lanePaths.securityDir, `wave-${waveNumber}-review.md`))
474
+ : `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-security-review.md`;
475
+ const integrationOwnedPaths = [
476
+ relativeStatePath(path.join(lanePaths.integrationDir, `wave-${waveNumber}.md`)),
477
+ relativeStatePath(path.join(lanePaths.integrationDir, `wave-${waveNumber}.json`)),
478
+ ];
479
+ const agents = [
480
+ {
481
+ agentId: lanePaths.contQaAgentId,
482
+ title: "cont-QA",
483
+ rolePromptPaths: [lanePaths.contQaRolePromptPath],
484
+ skills: [],
485
+ executor: { profile: "deep-review" },
486
+ context7: { bundle: "none", query: "Architecture evaluation only; repository docs remain canonical" },
487
+ components: [],
488
+ capabilities: [],
489
+ exitContract: null,
490
+ primaryGoal: `Run continuous QA for Wave ${waveNumber} and publish the final closure verdict.`,
491
+ collaborationNotes: [
492
+ `Collect explicit verdicts from the implementation-facing agents plus ${lanePaths.integrationAgentId} and ${lanePaths.documentationAgentId} before closing the run.`,
493
+ "Do not publish PASS unless the evidence, documentation closure, and integration summary are all coherent.",
494
+ ],
495
+ requiredContext: commonRequiredContext,
496
+ earlierWaveOutputs: [],
497
+ ownedPaths: [contQaReportPath],
498
+ requirements: [
499
+ "Verify the generated run requirements are covered by landed evidence, not only by intent.",
500
+ "Record the smallest blocking set that prevents closure.",
501
+ ],
502
+ validationCommand:
503
+ "Re-read the changed reports and end the cont-QA report with `Verdict: PASS`, `Verdict: CONCERNS`, or `Verdict: BLOCKED`.",
504
+ outputSummary: "Summarize the cont-QA verdict and the top unresolved cross-cutting risks.",
505
+ deployEnvironmentId: null,
506
+ },
507
+ {
508
+ agentId: lanePaths.integrationAgentId,
509
+ title: "Integration Steward",
510
+ rolePromptPaths: [lanePaths.integrationRolePromptPath],
511
+ skills: [],
512
+ executor: { profile: "deep-review" },
513
+ context7: { bundle: "none", query: "Integration synthesis only; repository docs remain canonical" },
514
+ components: [],
515
+ capabilities: ["integration", "docs-shared-plan"],
516
+ exitContract: null,
517
+ primaryGoal: `Synthesize the final Wave ${waveNumber} state before documentation and cont-QA closure.`,
518
+ collaborationNotes: [
519
+ "Re-read the message board, compiled inboxes, and latest artifacts before final output.",
520
+ "Treat contradictions, missing proof, or stale assumptions as integration failures.",
521
+ ],
522
+ requiredContext: commonRequiredContext,
523
+ earlierWaveOutputs: [],
524
+ ownedPaths: integrationOwnedPaths,
525
+ requirements: [
526
+ "Produce a closure-ready summary of claims, conflicts, blockers, and remaining follow-up owners.",
527
+ "Decide whether the wave is `ready-for-doc-closure` or `needs-more-work`.",
528
+ ],
529
+ validationCommand:
530
+ "Re-read the generated integration artifact and the latest changed proof docs before final output.",
531
+ outputSummary: "Summarize the integration verdict, blockers, and exact closure recommendation.",
532
+ deployEnvironmentId: null,
533
+ },
534
+ {
535
+ agentId: lanePaths.documentationAgentId,
536
+ title: "Documentation Steward",
537
+ rolePromptPaths: [lanePaths.documentationRolePromptPath],
538
+ skills: [],
539
+ executor: { profile: "docs-pass" },
540
+ context7: { bundle: "none", query: "Documentation closure only; repository docs remain canonical" },
541
+ components: [],
542
+ capabilities: [],
543
+ exitContract: null,
544
+ primaryGoal:
545
+ mode === "roadmap"
546
+ ? `Keep shared plan docs aligned with Wave ${waveNumber} end-to-end.`
547
+ : `Close the ad-hoc run documentation surface and reconcile canonical shared-plan docs when the run changes them.`,
548
+ collaborationNotes: [
549
+ `Coordinate with implementation-facing agents and ${lanePaths.integrationAgentId} before final output.`,
550
+ "When no shared-plan delta is required, leave an exact-scope `no-change` closure note instead of editing shared docs.",
551
+ ],
552
+ requiredContext: commonRequiredContext,
553
+ earlierWaveOutputs: [],
554
+ ownedPaths: documentationOwnedPaths,
555
+ requirements: [
556
+ "Track which landed outcomes change status, sequencing, ownership, or proof expectations.",
557
+ "Leave an explicit `closed` or `no-change` documentation closure marker.",
558
+ ],
559
+ validationCommand: "Manual review of documentation closure against the landed run deliverables.",
560
+ outputSummary: "Summarize the documentation closure decision and remaining follow-ups.",
561
+ deployEnvironmentId: null,
562
+ },
563
+ ];
564
+ if (includeContEval) {
565
+ agents.splice(1, 0, {
566
+ agentId: lanePaths.contEvalAgentId,
567
+ title: "cont-EVAL",
568
+ rolePromptPaths: [lanePaths.contEvalRolePromptPath],
569
+ skills: [],
570
+ executor: { profile: "eval-tuning" },
571
+ context7: { bundle: "none", query: "Eval tuning only; repository docs remain canonical" },
572
+ components: [],
573
+ capabilities: ["eval"],
574
+ exitContract: null,
575
+ primaryGoal: `Run the Wave ${waveNumber} eval tuning loop until the declared eval targets are satisfied or explicitly blocked.`,
576
+ collaborationNotes: [
577
+ "Treat the run's eval targets as the governing contract for benchmark choice and tuning depth.",
578
+ "Stay report-only unless the run explicitly assigns non-report owned paths.",
579
+ ],
580
+ requiredContext: commonRequiredContext,
581
+ earlierWaveOutputs: [],
582
+ ownedPaths: [contEvalReportPath],
583
+ requirements: [
584
+ "Record the selected benchmark set, the commands run, observed output gaps, and regressions.",
585
+ `Emit a final \`[wave-eval]\` marker whose target_ids match ${(evalTargets || []).map((target) => target.id).join(", ")}.`,
586
+ ],
587
+ validationCommand:
588
+ "Re-run the selected benchmarks or service-output checks and end with a final `[wave-eval]` marker.",
589
+ outputSummary: "Summarize the selected benchmarks, tuning outcome, regressions, and remaining owners.",
590
+ deployEnvironmentId: null,
591
+ });
592
+ }
593
+ if (includeSecurity) {
594
+ agents.splice(Math.max(agents.length - 2, 1), 0, {
595
+ agentId: "A7",
596
+ title: "Security Reviewer",
597
+ rolePromptPaths: [lanePaths.securityRolePromptPath],
598
+ skills: [],
599
+ executor: { profile: "security-review" },
600
+ context7: { bundle: "none", query: "Threat-model-first security review only; repository docs remain canonical" },
601
+ components: [],
602
+ capabilities: ["security-review"],
603
+ exitContract: null,
604
+ primaryGoal: `Review Wave ${waveNumber} for security risks and route exact fixes before integration closure.`,
605
+ collaborationNotes: [
606
+ "Do a threat-model pass before finalizing conclusions.",
607
+ "Keep the final output short enough to drive relaunch decisions and closure gates.",
608
+ ],
609
+ requiredContext: commonRequiredContext,
610
+ earlierWaveOutputs: [],
611
+ ownedPaths: [securityReportPath],
612
+ requirements: [
613
+ "Record findings with severity, concrete surface, exploit or failure mode, and the owner expected to fix it.",
614
+ "Emit one final `[wave-security]` marker with a fail-closed disposition.",
615
+ ],
616
+ validationCommand:
617
+ "Re-read the final security report and ensure the `[wave-security]` marker matches the findings and approval counts.",
618
+ outputSummary: "Summarize the threat model, findings, required approvals, and final security disposition.",
619
+ deployEnvironmentId: null,
620
+ });
621
+ }
622
+ return agents;
623
+ }
624
+
625
+ function buildEvalTargets(analyzedTasks) {
626
+ const scopedTasks = analyzedTasks.filter((task) => task.needsEval);
627
+ if (scopedTasks.length === 0) {
628
+ return [];
629
+ }
630
+ return scopedTasks.map((task, index) => ({
631
+ id: index === 0 ? "adhoc-service-output" : `adhoc-eval-${index + 1}`,
632
+ selection: "delegated",
633
+ benchmarkFamily: "service-output",
634
+ benchmarks: [],
635
+ objective: compactSingleLine(task.task, 140),
636
+ threshold: "Selected checks green with no unresolved regressions.",
637
+ }));
638
+ }
639
+
640
+ function workerAgentIdForIndex(index) {
641
+ return index < 6 ? `A${index + 1}` : `A${index + 4}`;
642
+ }
643
+
644
+ function buildWorkerAgent(taskSpec, index, lanePaths, deployEnvironments) {
645
+ const defaultDeployEnvironment =
646
+ deployEnvironments.find((environment) => environment.isDefault)?.id ||
647
+ deployEnvironments[0]?.id ||
648
+ null;
649
+ const capabilities = [];
650
+ if (taskSpec.roleKind !== "implementation") {
651
+ capabilities.push(taskSpec.roleKind);
652
+ }
653
+ if (taskSpec.docsHeavy && !capabilities.includes("docs-shared-plan")) {
654
+ capabilities.push("docs-shared-plan");
655
+ }
656
+ return {
657
+ agentId: workerAgentIdForIndex(index),
658
+ title: taskSpec.title,
659
+ rolePromptPaths: [],
660
+ skills: [],
661
+ executor: {
662
+ profile: defaultExecutorProfile(taskSpec.roleKind),
663
+ },
664
+ context7: {
665
+ bundle: "none",
666
+ query: null,
667
+ },
668
+ components: [],
669
+ capabilities,
670
+ exitContract: defaultExitContract(taskSpec.roleKind),
671
+ primaryGoal: taskSpec.task,
672
+ collaborationNotes: [
673
+ "Re-read the wave message board before major decisions, before validation, and before final output.",
674
+ `Notify Agent ${lanePaths.contQaAgentId} when your evidence changes the closure picture.`,
675
+ ],
676
+ requiredContext: buildCommonRequiredContext(),
677
+ earlierWaveOutputs: [],
678
+ ownedPaths: taskSpec.ownedPaths,
679
+ requirements: [
680
+ `Execute this task exactly: ${taskSpec.task}`,
681
+ "Keep ownership explicit and leave exact proof and doc deltas in the final output.",
682
+ ],
683
+ validationCommand: defaultValidationCommand(taskSpec.roleKind),
684
+ outputSummary: defaultOutputSummary(taskSpec.roleKind),
685
+ deployEnvironmentId:
686
+ taskSpec.roleKind === "deploy" || taskSpec.roleKind === "infra"
687
+ ? defaultDeployEnvironment
688
+ : null,
689
+ };
690
+ }
691
+
692
+ function buildRunTitle(tasks) {
693
+ const firstTask = compactSingleLine(tasks[0] || "Ad-Hoc Task", 80);
694
+ return tasks.length === 1 ? `Ad-Hoc: ${firstTask}` : `Ad-Hoc: ${firstTask} +${tasks.length - 1}`;
695
+ }
696
+
697
+ function buildCommitMessage(analyzedTasks) {
698
+ if (analyzedTasks.every((task) => task.docsHeavy)) {
699
+ return "Docs: land ad-hoc documentation slice";
700
+ }
701
+ if (analyzedTasks.some((task) => task.roleKind === "deploy" || task.roleKind === "infra")) {
702
+ return "Build: land ad-hoc deploy or infra slice";
703
+ }
704
+ return "Feat: land ad-hoc implementation slice";
705
+ }
706
+
707
+ function buildAdhocRequest({ runId, lanePaths, profile, tasks, launcherArgs = [] }) {
708
+ return {
709
+ schemaVersion: ADHOC_SCHEMA_VERSION,
710
+ runKind: "adhoc",
711
+ runId,
712
+ lane: lanePaths.lane,
713
+ createdAt: toIsoTimestamp(),
714
+ oversightMode: profile.defaultOversightMode,
715
+ defaultTerminalSurface: profile.defaultTerminalSurface,
716
+ tasks: tasks.map((task, index) => ({
717
+ id: `task-${index + 1}`,
718
+ text: cleanText(task),
719
+ })),
720
+ launcherArgs,
721
+ };
722
+ }
723
+
724
+ function buildAdhocSpec({ runId, lanePaths, profile, request, mode = "adhoc", waveNumber = ADHOC_WAVE_NUMBER }) {
725
+ const tasks = request.tasks.map((task) => task.text);
726
+ const analyzedTasks = tasks.map((task, index) => analyzeTask(task, index, profile, lanePaths));
727
+ const deployEnvironments = resolveDeployEnvironments(profile, analyzedTasks);
728
+ const evalTargets = buildEvalTargets(analyzedTasks);
729
+ const workerAgents = analyzedTasks.map((taskSpec, index) =>
730
+ buildWorkerAgent(taskSpec, index, lanePaths, deployEnvironments),
731
+ );
732
+ const includeContEval = evalTargets.length > 0;
733
+ const includeSecurity = analyzedTasks.some((task) => task.needsSecurity);
734
+ return {
735
+ schemaVersion: 1,
736
+ generatedAt: toIsoTimestamp(),
737
+ runKind: mode === "roadmap" ? "roadmap" : "adhoc",
738
+ runId: mode === "roadmap" ? null : runId,
739
+ sourceRunId: mode === "roadmap" ? runId : null,
740
+ projectProfile: {
741
+ projectName: profile.source?.projectName || lanePaths.config.projectName,
742
+ newProject: profile.newProject === true,
743
+ defaultTerminalSurface: profile.defaultTerminalSurface,
744
+ },
745
+ template: deriveTemplate(analyzedTasks.map((task) => task.roleKind)),
746
+ lane: lanePaths.lane,
747
+ wave: waveNumber,
748
+ title: buildRunTitle(tasks),
749
+ commitMessage: buildCommitMessage(analyzedTasks),
750
+ oversightMode: profile.defaultOversightMode,
751
+ sequencingNote:
752
+ "Generated from an operator ad-hoc request. Treat the stored task list as the authoritative scope for this run.",
753
+ referenceRule:
754
+ "Repository source, resolved runtime skills, and generated coordination artifacts remain authoritative over request paraphrases.",
755
+ deployEnvironments,
756
+ context7Defaults: {
757
+ bundle: "none",
758
+ query: null,
759
+ },
760
+ evalTargets,
761
+ componentPromotions: [],
762
+ componentsCatalog: [],
763
+ requestedTasks: request.tasks,
764
+ agents: [
765
+ ...buildSpecialAgents({
766
+ lanePaths,
767
+ waveNumber,
768
+ includeContEval,
769
+ includeSecurity,
770
+ evalTargets,
771
+ mode,
772
+ }),
773
+ ...workerAgents,
774
+ ],
775
+ };
776
+ }
777
+
778
+ function isGeneratedSpecialAgent(agent) {
779
+ return GENERATED_SPECIAL_AGENT_TITLES.has(cleanText(agent?.title));
780
+ }
781
+
782
+ function buildPromotedRoadmapSpec(storedSpec, lanePaths, waveNumber, runId) {
783
+ const includeContEval =
784
+ (Array.isArray(storedSpec?.evalTargets) && storedSpec.evalTargets.length > 0) ||
785
+ (storedSpec?.agents || []).some((agent) => cleanText(agent?.title) === "cont-EVAL");
786
+ const includeSecurity = (storedSpec?.agents || []).some(
787
+ (agent) => cleanText(agent?.title) === "Security Reviewer",
788
+ );
789
+ const workerAgents = (storedSpec?.agents || []).filter((agent) => !isGeneratedSpecialAgent(agent));
790
+ return {
791
+ ...storedSpec,
792
+ runKind: "roadmap",
793
+ runId: null,
794
+ sourceRunId: cleanText(storedSpec?.sourceRunId) || cleanText(storedSpec?.runId) || runId || null,
795
+ lane: lanePaths.lane,
796
+ wave: waveNumber,
797
+ agents: [
798
+ ...buildSpecialAgents({
799
+ lanePaths,
800
+ waveNumber,
801
+ includeContEval,
802
+ includeSecurity,
803
+ evalTargets: storedSpec?.evalTargets || [],
804
+ mode: "roadmap",
805
+ }),
806
+ ...workerAgents,
807
+ ],
808
+ };
809
+ }
810
+
811
+ function validateGeneratedRun(lanePaths) {
812
+ const parsedWave = parseWaveFile(lanePaths.adhocWavePath, {
813
+ laneProfile: lanePaths.laneProfile,
814
+ });
815
+ validateWaveDefinition(
816
+ applyExecutorSelectionsToWave(parsedWave, {
817
+ laneProfile: lanePaths.laneProfile,
818
+ }),
819
+ { laneProfile: lanePaths.laneProfile },
820
+ );
821
+ }
822
+
823
+ function buildResultRecord(lanePaths, request, spec, status, extra = {}) {
824
+ const now = toIsoTimestamp();
825
+ const previous = readJsonOrNull(lanePaths.adhocResultPath) || {};
826
+ const { stateLanePaths = lanePaths, ...rest } = extra;
827
+ return {
828
+ schemaVersion: ADHOC_SCHEMA_VERSION,
829
+ runKind: "adhoc",
830
+ runId: request.runId,
831
+ lane: request.lane,
832
+ title: spec.title,
833
+ status,
834
+ createdAt: previous.createdAt || request.createdAt || now,
835
+ updatedAt: now,
836
+ taskCount: request.tasks.length,
837
+ tasks: request.tasks,
838
+ requestPath: repoRelativePath(lanePaths.adhocRequestPath),
839
+ specPath: repoRelativePath(lanePaths.adhocSpecPath),
840
+ wavePath: repoRelativePath(lanePaths.adhocWavePath),
841
+ launcherStateDir: repoRelativePath(stateLanePaths.stateDir),
842
+ tracesDir: repoRelativePath(stateLanePaths.tracesDir),
843
+ promotedWave: previous.promotedWave || null,
844
+ ...rest,
845
+ };
846
+ }
847
+
848
+ function readAdhocIndex(indexPath) {
849
+ const payload = readJsonOrNull(indexPath);
850
+ if (!payload || typeof payload !== "object") {
851
+ return {
852
+ schemaVersion: ADHOC_SCHEMA_VERSION,
853
+ updatedAt: toIsoTimestamp(),
854
+ runs: [],
855
+ };
856
+ }
857
+ return {
858
+ schemaVersion: ADHOC_SCHEMA_VERSION,
859
+ updatedAt: cleanText(payload.updatedAt) || toIsoTimestamp(),
860
+ runs: Array.isArray(payload.runs) ? payload.runs : [],
861
+ };
862
+ }
863
+
864
+ function writeAdhocIndex(indexPath, index) {
865
+ ensureDirectory(path.dirname(indexPath));
866
+ writeJsonAtomic(indexPath, {
867
+ schemaVersion: ADHOC_SCHEMA_VERSION,
868
+ updatedAt: toIsoTimestamp(),
869
+ runs: (index.runs || []).toSorted((left, right) =>
870
+ String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")),
871
+ ),
872
+ });
873
+ }
874
+
875
+ function upsertAdhocIndexEntry(indexPath, result) {
876
+ const index = readAdhocIndex(indexPath);
877
+ const entry = {
878
+ runId: result.runId,
879
+ lane: result.lane,
880
+ title: result.title,
881
+ status: result.status,
882
+ taskCount: result.taskCount,
883
+ createdAt: result.createdAt,
884
+ updatedAt: result.updatedAt,
885
+ promotedWave: result.promotedWave || null,
886
+ };
887
+ const nextRuns = index.runs.filter((run) => run.runId !== result.runId);
888
+ nextRuns.push(entry);
889
+ writeAdhocIndex(indexPath, { ...index, runs: nextRuns });
890
+ }
891
+
892
+ function ensureAdhocRunArtifacts(lanePaths, request, spec) {
893
+ ensureDirectory(lanePaths.adhocRunDir);
894
+ writeJsonAtomic(lanePaths.adhocRequestPath, request);
895
+ writeJsonAtomic(lanePaths.adhocSpecPath, spec);
896
+ writeTextAtomic(lanePaths.adhocWavePath, `${renderWaveMarkdown(spec, lanePaths)}\n`);
897
+ validateGeneratedRun(lanePaths);
898
+ }
899
+
900
+ function summarizePlan(spec, lanePaths) {
901
+ return {
902
+ runId: lanePaths.runId,
903
+ lane: lanePaths.lane,
904
+ title: spec.title,
905
+ tasks: spec.requestedTasks || [],
906
+ agents: spec.agents.map((agent) => ({
907
+ agentId: agent.agentId,
908
+ title: agent.title,
909
+ profile: agent.executor?.profile || null,
910
+ deployEnvironmentId: agent.deployEnvironmentId || null,
911
+ })),
912
+ requestPath: repoRelativePath(lanePaths.adhocRequestPath),
913
+ specPath: repoRelativePath(lanePaths.adhocSpecPath),
914
+ wavePath: repoRelativePath(lanePaths.adhocWavePath),
915
+ stateDir: repoRelativePath(lanePaths.stateDir),
916
+ };
917
+ }
918
+
919
+ async function confirmLaunch(runSummary) {
920
+ if (!stdin.isTTY) {
921
+ throw new Error("Non-interactive ad-hoc launch requires --yes.");
922
+ }
923
+ const rl = readline.createInterface({
924
+ input: stdin,
925
+ output: stderr,
926
+ terminal: true,
927
+ });
928
+ try {
929
+ const answer = cleanText(await rl.question(`Launch ad-hoc run ${runSummary.runId}? (y/n): `)).toLowerCase();
930
+ if (!["y", "yes"].includes(answer)) {
931
+ throw new Error(`Ad-hoc run ${runSummary.runId} cancelled.`);
932
+ }
933
+ } finally {
934
+ rl.close();
935
+ }
936
+ }
937
+
938
+ function renderHumanPlanSummary(runSummary) {
939
+ console.log(`[wave:adhoc] run=${runSummary.runId}`);
940
+ console.log(`[wave:adhoc] lane=${runSummary.lane}`);
941
+ console.log(`[wave:adhoc] title=${runSummary.title}`);
942
+ console.log(`[wave:adhoc] request=${runSummary.requestPath}`);
943
+ console.log(`[wave:adhoc] spec=${runSummary.specPath}`);
944
+ console.log(`[wave:adhoc] markdown=${runSummary.wavePath}`);
945
+ console.log(`[wave:adhoc] state=${runSummary.stateDir}`);
946
+ for (const task of runSummary.tasks) {
947
+ console.log(`[wave:adhoc] task=${task.text}`);
948
+ }
949
+ for (const agent of runSummary.agents) {
950
+ console.log(
951
+ `[wave:adhoc] agent=${agent.agentId}:${agent.title} profile=${agent.profile || "none"}${agent.deployEnvironmentId ? ` env=${agent.deployEnvironmentId}` : ""}`,
952
+ );
953
+ }
954
+ }
955
+
956
+ function collectStoredRuns(indexPath) {
957
+ const index = readAdhocIndex(indexPath);
958
+ const runsRoot = path.join(path.dirname(indexPath), "runs");
959
+ const runDirs = fs.existsSync(runsRoot)
960
+ ? fs
961
+ .readdirSync(runsRoot, { withFileTypes: true })
962
+ .filter((entry) => entry.isDirectory())
963
+ .map((entry) => entry.name)
964
+ : [];
965
+ const known = new Set(index.runs.map((run) => run.runId));
966
+ const materialized = [...index.runs];
967
+ for (const runId of runDirs) {
968
+ if (known.has(runId)) {
969
+ continue;
970
+ }
971
+ const result = readJsonOrNull(path.join(path.dirname(indexPath), "runs", runId, "result.json"));
972
+ if (result) {
973
+ materialized.push({
974
+ runId: result.runId,
975
+ lane: result.lane,
976
+ title: result.title,
977
+ status: result.status,
978
+ taskCount: result.taskCount,
979
+ createdAt: result.createdAt,
980
+ updatedAt: result.updatedAt,
981
+ promotedWave: result.promotedWave || null,
982
+ });
983
+ }
984
+ }
985
+ return materialized.toSorted((left, right) =>
986
+ String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")),
987
+ );
988
+ }
989
+
990
+ function parseArgs(argv) {
991
+ const args = Array.isArray(argv) ? argv.slice() : [];
992
+ const subcommand = cleanText(args.shift()).toLowerCase();
993
+ const options = {
994
+ lane: "",
995
+ runId: "",
996
+ wave: null,
997
+ tasks: [],
998
+ yes: false,
999
+ json: false,
1000
+ force: false,
1001
+ launcherArgs: [],
1002
+ };
1003
+ while (args.length > 0) {
1004
+ const arg = args.shift();
1005
+ if (arg === "--task") {
1006
+ options.tasks.push(cleanText(args.shift()));
1007
+ } else if (arg === "--lane") {
1008
+ options.lane = cleanText(args.shift());
1009
+ } else if (arg === "--run") {
1010
+ options.runId = sanitizeAdhocRunId(args.shift());
1011
+ } else if (arg === "--wave") {
1012
+ options.wave = parseNonNegativeInt(args.shift(), "--wave");
1013
+ } else if (arg === "--yes") {
1014
+ options.yes = true;
1015
+ } else if (arg === "--json") {
1016
+ options.json = true;
1017
+ } else if (arg === "--force") {
1018
+ options.force = true;
1019
+ } else if (
1020
+ [
1021
+ "--dry-run",
1022
+ "--no-dashboard",
1023
+ "--keep-sessions",
1024
+ "--cleanup-sessions",
1025
+ "--keep-terminals",
1026
+ "--no-context7",
1027
+ ].includes(arg)
1028
+ ) {
1029
+ options.launcherArgs.push(arg);
1030
+ } else if (
1031
+ [
1032
+ "--terminal-surface",
1033
+ "--executor",
1034
+ "--codex-sandbox",
1035
+ "--timeout-minutes",
1036
+ "--max-retries-per-wave",
1037
+ "--agent-rate-limit-retries",
1038
+ "--agent-rate-limit-base-delay-seconds",
1039
+ "--agent-rate-limit-max-delay-seconds",
1040
+ "--agent-launch-stagger-ms",
1041
+ "--coordination-note",
1042
+ "--orchestrator-id",
1043
+ "--orchestrator-board",
1044
+ ].includes(arg)
1045
+ ) {
1046
+ const value = cleanText(args.shift());
1047
+ options.launcherArgs.push(arg, value);
1048
+ } else if (arg === "--help" || arg === "-h") {
1049
+ return { help: true, subcommand, options };
1050
+ } else if (arg) {
1051
+ throw new Error(`Unknown argument: ${arg}`);
1052
+ }
1053
+ }
1054
+ return { help: false, subcommand, options };
1055
+ }
1056
+
1057
+ function printUsage() {
1058
+ console.log(`Usage:
1059
+ wave adhoc plan --task <text> [--task <text>] [--lane <lane>] [--json]
1060
+ wave adhoc run --task <text> [--task <text>] [--lane <lane>] [--yes] [--json] [launcher options]
1061
+ wave adhoc list [--lane <lane>] [--json]
1062
+ wave adhoc show --run <id> [--json]
1063
+ wave adhoc promote --run <id> --wave <n> [--force] [--json]
1064
+ `);
1065
+ }
1066
+
1067
+ function resolveLaneForOptions(config, options) {
1068
+ const profile = readEffectiveProjectProfile(config);
1069
+ return cleanText(options.lane) || profile.plannerDefaults?.lane || config.defaultLane;
1070
+ }
1071
+
1072
+ function createStoredRun({ config, options }) {
1073
+ const profile = readEffectiveProjectProfile(config);
1074
+ const lane = resolveLaneForOptions(config, options);
1075
+ const runId = buildAdhocRunId();
1076
+ const lanePaths = buildLanePaths(lane, { config, adhocRunId: runId });
1077
+ const request = buildAdhocRequest({
1078
+ runId,
1079
+ lanePaths,
1080
+ profile,
1081
+ tasks: options.tasks,
1082
+ launcherArgs: options.launcherArgs,
1083
+ });
1084
+ const spec = buildAdhocSpec({
1085
+ runId,
1086
+ lanePaths,
1087
+ profile,
1088
+ request,
1089
+ mode: "adhoc",
1090
+ });
1091
+ ensureAdhocRunArtifacts(lanePaths, request, spec);
1092
+ const result = buildResultRecord(lanePaths, request, spec, "planned");
1093
+ writeJsonAtomic(lanePaths.adhocResultPath, result);
1094
+ upsertAdhocIndexEntry(lanePaths.adhocIndexPath, result);
1095
+ return { lanePaths, request, spec, result };
1096
+ }
1097
+
1098
+ function readStoredRun(runId) {
1099
+ const lanePaths = buildLanePaths(DEFAULT_LANE_PLACEHOLDER, { adhocRunId: runId });
1100
+ const request = readJsonOrNull(lanePaths.adhocRequestPath);
1101
+ const spec = readJsonOrNull(lanePaths.adhocSpecPath);
1102
+ const result = readJsonOrNull(lanePaths.adhocResultPath);
1103
+ if (!request || !spec || !result) {
1104
+ throw new Error(`Ad-hoc run not found: ${runId}`);
1105
+ }
1106
+ return { lanePaths, request, spec, result };
1107
+ }
1108
+
1109
+ const DEFAULT_LANE_PLACEHOLDER = "main";
1110
+
1111
+ export async function runAdhocCli(argv) {
1112
+ const parsed = parseArgs(argv);
1113
+ if (parsed.help || !parsed.subcommand) {
1114
+ printUsage();
1115
+ return;
1116
+ }
1117
+ const { subcommand, options } = parsed;
1118
+ const config = loadWaveConfig();
1119
+
1120
+ if (subcommand === "plan") {
1121
+ if (options.tasks.length === 0) {
1122
+ throw new Error("At least one --task is required for `wave adhoc plan`.");
1123
+ }
1124
+ const { lanePaths, spec } = createStoredRun({ config, options });
1125
+ const summary = summarizePlan(spec, lanePaths);
1126
+ if (options.json) {
1127
+ console.log(JSON.stringify(summary, null, 2));
1128
+ return;
1129
+ }
1130
+ renderHumanPlanSummary(summary);
1131
+ return;
1132
+ }
1133
+
1134
+ if (subcommand === "run") {
1135
+ if (options.tasks.length === 0) {
1136
+ throw new Error("At least one --task is required for `wave adhoc run`.");
1137
+ }
1138
+ const stored = createStoredRun({ config, options });
1139
+ const summary = summarizePlan(stored.spec, stored.lanePaths);
1140
+ if (options.json) {
1141
+ console.log(JSON.stringify(summary, null, 2));
1142
+ } else {
1143
+ renderHumanPlanSummary(summary);
1144
+ }
1145
+ if (!options.yes) {
1146
+ await confirmLaunch(summary);
1147
+ }
1148
+ const launchLanePaths = buildLanePaths(stored.lanePaths.lane, {
1149
+ config,
1150
+ adhocRunId: stored.lanePaths.runId,
1151
+ runVariant: options.launcherArgs.includes("--dry-run") ? "dry-run" : undefined,
1152
+ });
1153
+ const runningResult = buildResultRecord(stored.lanePaths, stored.request, stored.spec, "running", {
1154
+ launcherArgs: options.launcherArgs,
1155
+ stateLanePaths: launchLanePaths,
1156
+ });
1157
+ writeJsonAtomic(stored.lanePaths.adhocResultPath, runningResult);
1158
+ upsertAdhocIndexEntry(stored.lanePaths.adhocIndexPath, runningResult);
1159
+ try {
1160
+ await runLauncherCli([
1161
+ "--lane",
1162
+ stored.lanePaths.lane,
1163
+ "--adhoc-run",
1164
+ stored.lanePaths.runId,
1165
+ "--start-wave",
1166
+ String(ADHOC_WAVE_NUMBER),
1167
+ "--end-wave",
1168
+ String(ADHOC_WAVE_NUMBER),
1169
+ ...options.launcherArgs,
1170
+ ]);
1171
+ const completedResult = buildResultRecord(
1172
+ stored.lanePaths,
1173
+ stored.request,
1174
+ stored.spec,
1175
+ "completed",
1176
+ {
1177
+ launcherArgs: options.launcherArgs,
1178
+ stateLanePaths: launchLanePaths,
1179
+ },
1180
+ );
1181
+ writeJsonAtomic(stored.lanePaths.adhocResultPath, completedResult);
1182
+ upsertAdhocIndexEntry(stored.lanePaths.adhocIndexPath, completedResult);
1183
+ if (!options.json) {
1184
+ console.log(`[wave:adhoc] completed=${stored.lanePaths.runId}`);
1185
+ }
1186
+ return;
1187
+ } catch (error) {
1188
+ const failedResult = buildResultRecord(stored.lanePaths, stored.request, stored.spec, "failed", {
1189
+ launcherArgs: options.launcherArgs,
1190
+ error: error instanceof Error ? error.message : String(error),
1191
+ stateLanePaths: launchLanePaths,
1192
+ });
1193
+ writeJsonAtomic(stored.lanePaths.adhocResultPath, failedResult);
1194
+ upsertAdhocIndexEntry(stored.lanePaths.adhocIndexPath, failedResult);
1195
+ throw error;
1196
+ }
1197
+ }
1198
+
1199
+ if (subcommand === "list") {
1200
+ const lane = cleanText(options.lane);
1201
+ const lanePaths = buildLanePaths(lane || config.defaultLane, { config });
1202
+ const runs = collectStoredRuns(lanePaths.adhocIndexPath).filter((run) =>
1203
+ lane ? run.lane === lane : true,
1204
+ );
1205
+ if (options.json) {
1206
+ console.log(JSON.stringify(runs, null, 2));
1207
+ return;
1208
+ }
1209
+ for (const run of runs) {
1210
+ console.log(
1211
+ `${run.runId} ${run.status} lane=${run.lane} tasks=${run.taskCount} updated=${run.updatedAt} ${run.title}`,
1212
+ );
1213
+ }
1214
+ return;
1215
+ }
1216
+
1217
+ if (subcommand === "show") {
1218
+ if (!options.runId) {
1219
+ throw new Error("--run <id> is required for `wave adhoc show`.");
1220
+ }
1221
+ const { lanePaths, request, spec, result } = readStoredRun(options.runId);
1222
+ const payload = {
1223
+ runId: options.runId,
1224
+ lane: result.lane,
1225
+ status: result.status,
1226
+ title: result.title,
1227
+ tasks: request.tasks,
1228
+ requestPath: repoRelativePath(lanePaths.adhocRequestPath),
1229
+ specPath: repoRelativePath(lanePaths.adhocSpecPath),
1230
+ wavePath: repoRelativePath(lanePaths.adhocWavePath),
1231
+ launcherStateDir: result.launcherStateDir,
1232
+ tracesDir: result.tracesDir,
1233
+ agents: spec.agents.map((agent) => ({
1234
+ agentId: agent.agentId,
1235
+ title: agent.title,
1236
+ profile: agent.executor?.profile || null,
1237
+ })),
1238
+ promotedWave: result.promotedWave || null,
1239
+ };
1240
+ if (options.json) {
1241
+ console.log(JSON.stringify(payload, null, 2));
1242
+ return;
1243
+ }
1244
+ console.log(`[wave:adhoc] run=${payload.runId}`);
1245
+ console.log(`[wave:adhoc] lane=${payload.lane}`);
1246
+ console.log(`[wave:adhoc] status=${payload.status}`);
1247
+ console.log(`[wave:adhoc] title=${payload.title}`);
1248
+ console.log(`[wave:adhoc] request=${payload.requestPath}`);
1249
+ console.log(`[wave:adhoc] spec=${payload.specPath}`);
1250
+ console.log(`[wave:adhoc] markdown=${payload.wavePath}`);
1251
+ console.log(`[wave:adhoc] traces=${payload.tracesDir}`);
1252
+ for (const task of payload.tasks) {
1253
+ console.log(`[wave:adhoc] task=${task.text}`);
1254
+ }
1255
+ for (const agent of payload.agents) {
1256
+ console.log(`[wave:adhoc] agent=${agent.agentId}:${agent.title} profile=${agent.profile || "none"}`);
1257
+ }
1258
+ if (payload.promotedWave !== null) {
1259
+ console.log(`[wave:adhoc] promoted_wave=${payload.promotedWave}`);
1260
+ }
1261
+ return;
1262
+ }
1263
+
1264
+ if (subcommand === "promote") {
1265
+ if (!options.runId) {
1266
+ throw new Error("--run <id> is required for `wave adhoc promote`.");
1267
+ }
1268
+ if (!Number.isFinite(options.wave) || options.wave < 0) {
1269
+ throw new Error("--wave <n> is required for `wave adhoc promote`.");
1270
+ }
1271
+ const stored = readStoredRun(options.runId);
1272
+ const lane = cleanText(options.lane) || stored.result.lane || config.defaultLane;
1273
+ const lanePaths = buildLanePaths(lane, { config });
1274
+ const wavePath = path.join(lanePaths.wavesDir, `wave-${options.wave}.md`);
1275
+ const specPath = path.join(lanePaths.wavesDir, "specs", `wave-${options.wave}.json`);
1276
+ if (!options.force && (fs.existsSync(wavePath) || fs.existsSync(specPath))) {
1277
+ throw new Error(
1278
+ `Wave ${options.wave} already exists. Re-run with --force to overwrite ${repoRelativePath(wavePath)} and ${repoRelativePath(specPath)}.`,
1279
+ );
1280
+ }
1281
+ const promotedSpec = buildPromotedRoadmapSpec(
1282
+ stored.spec,
1283
+ lanePaths,
1284
+ options.wave,
1285
+ stored.result.runId,
1286
+ );
1287
+ ensureDirectory(path.dirname(specPath));
1288
+ writeJsonAtomic(specPath, promotedSpec);
1289
+ writeTextAtomic(wavePath, `${renderWaveMarkdown(promotedSpec, lanePaths)}\n`);
1290
+ let validationError = null;
1291
+ try {
1292
+ const parsedWave = parseWaveFile(wavePath, { laneProfile: lanePaths.laneProfile });
1293
+ validateWaveDefinition(
1294
+ applyExecutorSelectionsToWave(parsedWave, {
1295
+ laneProfile: lanePaths.laneProfile,
1296
+ }),
1297
+ { laneProfile: lanePaths.laneProfile },
1298
+ );
1299
+ } catch (error) {
1300
+ validationError = error instanceof Error ? error.message : String(error);
1301
+ }
1302
+ const nextResult = {
1303
+ ...stored.result,
1304
+ promotedWave: options.wave,
1305
+ updatedAt: toIsoTimestamp(),
1306
+ promotionValidationError: validationError,
1307
+ };
1308
+ writeJsonAtomic(stored.lanePaths.adhocResultPath, nextResult);
1309
+ upsertAdhocIndexEntry(stored.lanePaths.adhocIndexPath, nextResult);
1310
+ const payload = {
1311
+ runId: stored.result.runId,
1312
+ wave: options.wave,
1313
+ specPath: repoRelativePath(specPath),
1314
+ wavePath: repoRelativePath(wavePath),
1315
+ validationError,
1316
+ };
1317
+ if (options.json) {
1318
+ console.log(JSON.stringify(payload, null, 2));
1319
+ return;
1320
+ }
1321
+ console.log(`[wave:adhoc] promoted=${payload.runId}`);
1322
+ console.log(`[wave:adhoc] spec=${payload.specPath}`);
1323
+ console.log(`[wave:adhoc] markdown=${payload.wavePath}`);
1324
+ if (payload.validationError) {
1325
+ console.log(`[wave:adhoc] validation_warning=${payload.validationError}`);
1326
+ }
1327
+ return;
1328
+ }
1329
+
1330
+ throw new Error(`Unknown adhoc subcommand: ${subcommand}`);
1331
+ }