@chllming/wave-orchestration 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +9 -8
  3. package/docs/guides/planner.md +19 -0
  4. package/docs/guides/terminal-surfaces.md +12 -0
  5. package/docs/plans/component-cutover-matrix.json +50 -3
  6. package/docs/plans/current-state.md +1 -1
  7. package/docs/plans/end-state-architecture.md +927 -0
  8. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  9. package/docs/plans/migration.md +26 -0
  10. package/docs/plans/wave-orchestrator.md +4 -7
  11. package/docs/plans/waves/wave-1.md +376 -0
  12. package/docs/plans/waves/wave-2.md +292 -0
  13. package/docs/plans/waves/wave-3.md +342 -0
  14. package/docs/plans/waves/wave-4.md +391 -0
  15. package/docs/plans/waves/wave-5.md +382 -0
  16. package/docs/plans/waves/wave-6.md +321 -0
  17. package/docs/reference/cli-reference.md +547 -0
  18. package/docs/reference/coordination-and-closure.md +1 -1
  19. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  20. package/docs/reference/runtime-config/README.md +2 -2
  21. package/docs/reference/runtime-config/codex.md +2 -1
  22. package/docs/reference/sample-waves.md +4 -4
  23. package/package.json +1 -1
  24. package/releases/manifest.json +43 -2
  25. package/scripts/wave-orchestrator/agent-state.mjs +458 -35
  26. package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
  27. package/scripts/wave-orchestrator/control-cli.mjs +119 -20
  28. package/scripts/wave-orchestrator/coordination.mjs +11 -10
  29. package/scripts/wave-orchestrator/dashboard-renderer.mjs +82 -2
  30. package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
  31. package/scripts/wave-orchestrator/install.mjs +120 -3
  32. package/scripts/wave-orchestrator/launcher-derived-state.mjs +915 -0
  33. package/scripts/wave-orchestrator/launcher-gates.mjs +1061 -0
  34. package/scripts/wave-orchestrator/launcher-retry.mjs +873 -0
  35. package/scripts/wave-orchestrator/launcher-runtime.mjs +9 -9
  36. package/scripts/wave-orchestrator/launcher-supervisor.mjs +704 -0
  37. package/scripts/wave-orchestrator/launcher.mjs +317 -2999
  38. package/scripts/wave-orchestrator/task-entity.mjs +557 -0
  39. package/scripts/wave-orchestrator/terminals.mjs +1 -1
  40. package/scripts/wave-orchestrator/wave-files.mjs +138 -20
  41. package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
  42. package/wave.config.json +1 -1
@@ -0,0 +1,289 @@
1
+ import { toIsoTimestamp } from "./shared.mjs";
2
+
3
+ // ── Human Input Workflow State Machine ──
4
+ //
5
+ // States: open -> pending -> answered -> resolved
6
+ // -> escalated -> resolved
7
+
8
+ export const HUMAN_INPUT_STATES = new Set([
9
+ "open",
10
+ "pending",
11
+ "answered",
12
+ "escalated",
13
+ "resolved",
14
+ ]);
15
+
16
+ export const HUMAN_INPUT_VALID_TRANSITIONS = {
17
+ open: ["pending", "escalated", "resolved"],
18
+ pending: ["answered", "escalated", "resolved"],
19
+ answered: ["resolved"],
20
+ escalated: ["answered", "resolved"],
21
+ resolved: [],
22
+ };
23
+
24
+ const BLOCKING_STATES = new Set(["open", "pending", "escalated"]);
25
+
26
+ const DEFAULT_TIMEOUT_POLICY = {
27
+ maxWaitMs: 300000,
28
+ escalateAfterMs: 120000,
29
+ };
30
+
31
+ const DEFAULT_REROUTE_POLICY = {
32
+ rerouteOnTimeout: true,
33
+ rerouteTo: "operator",
34
+ };
35
+
36
+ function isPlainObject(value) {
37
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
38
+ }
39
+
40
+ function normalizeText(value, fallback = null) {
41
+ const normalized = String(value ?? "").trim();
42
+ return normalized || fallback;
43
+ }
44
+
45
+ export function normalizeHumanInputRequest(request, defaults = {}) {
46
+ const source = isPlainObject(request) ? request : {};
47
+ const defaultSource = isPlainObject(defaults) ? defaults : {};
48
+ const now = toIsoTimestamp();
49
+
50
+ const timeoutPolicy = isPlainObject(source.timeoutPolicy)
51
+ ? {
52
+ maxWaitMs: Number.isFinite(source.timeoutPolicy.maxWaitMs)
53
+ ? source.timeoutPolicy.maxWaitMs
54
+ : DEFAULT_TIMEOUT_POLICY.maxWaitMs,
55
+ escalateAfterMs: Number.isFinite(source.timeoutPolicy.escalateAfterMs)
56
+ ? source.timeoutPolicy.escalateAfterMs
57
+ : DEFAULT_TIMEOUT_POLICY.escalateAfterMs,
58
+ }
59
+ : { ...DEFAULT_TIMEOUT_POLICY };
60
+
61
+ const reroutePolicy = isPlainObject(source.reroutePolicy)
62
+ ? {
63
+ rerouteOnTimeout: source.reroutePolicy.rerouteOnTimeout !== false,
64
+ rerouteTo: normalizeText(source.reroutePolicy.rerouteTo, DEFAULT_REROUTE_POLICY.rerouteTo),
65
+ }
66
+ : { ...DEFAULT_REROUTE_POLICY };
67
+
68
+ const rawState = normalizeText(source.state, normalizeText(defaultSource.state, "open"));
69
+ const state = HUMAN_INPUT_STATES.has(rawState) ? rawState : "open";
70
+
71
+ return {
72
+ requestId: normalizeText(source.requestId, normalizeText(defaultSource.requestId, null)),
73
+ kind: normalizeText(source.kind, normalizeText(defaultSource.kind, "human-input")),
74
+ state,
75
+ title: normalizeText(source.title, normalizeText(defaultSource.title, null)),
76
+ detail: normalizeText(source.detail, normalizeText(defaultSource.detail, null)),
77
+ requestedBy: normalizeText(source.requestedBy, normalizeText(defaultSource.requestedBy, null)),
78
+ assignedTo: normalizeText(source.assignedTo, normalizeText(defaultSource.assignedTo, null)),
79
+ timeoutPolicy,
80
+ reroutePolicy,
81
+ createdAt: normalizeText(source.createdAt, normalizeText(defaultSource.createdAt, now)),
82
+ updatedAt: normalizeText(source.updatedAt, normalizeText(defaultSource.updatedAt, now)),
83
+ answeredAt: normalizeText(source.answeredAt, null),
84
+ resolvedAt: normalizeText(source.resolvedAt, null),
85
+ escalatedAt: normalizeText(source.escalatedAt, null),
86
+ answer: normalizeText(source.answer, null),
87
+ resolution: normalizeText(source.resolution, null),
88
+ };
89
+ }
90
+
91
+ export function transitionHumanInputState(currentState, targetState) {
92
+ if (!HUMAN_INPUT_STATES.has(currentState)) {
93
+ throw new Error(`Invalid current state: ${currentState}`);
94
+ }
95
+ if (!HUMAN_INPUT_STATES.has(targetState)) {
96
+ throw new Error(`Invalid target state: ${targetState}`);
97
+ }
98
+ const allowed = HUMAN_INPUT_VALID_TRANSITIONS[currentState];
99
+ if (!allowed || !allowed.includes(targetState)) {
100
+ throw new Error(
101
+ `Invalid transition from "${currentState}" to "${targetState}". Allowed: [${(allowed || []).join(", ")}]`,
102
+ );
103
+ }
104
+ return targetState;
105
+ }
106
+
107
+ export function isHumanInputBlocking(request) {
108
+ const source = isPlainObject(request) ? request : {};
109
+ const state = normalizeText(source.state, "open");
110
+ return BLOCKING_STATES.has(state);
111
+ }
112
+
113
+ export function buildHumanInputRequests(coordinationState, feedbackRequests, options = {}) {
114
+ const results = [];
115
+ const coordState = isPlainObject(coordinationState) ? coordinationState : {};
116
+ const feedbackList = Array.isArray(feedbackRequests) ? feedbackRequests : [];
117
+ const now = toIsoTimestamp();
118
+
119
+ // Process clarification-request records from coordination state
120
+ const clarifications = Array.isArray(coordState.clarifications)
121
+ ? coordState.clarifications
122
+ : [];
123
+ for (const record of clarifications) {
124
+ if (!isPlainObject(record)) continue;
125
+ const kind = normalizeText(record.kind, null);
126
+ if (
127
+ kind !== "clarification-request" &&
128
+ kind !== "human-escalation" &&
129
+ kind !== "human-feedback"
130
+ ) {
131
+ continue;
132
+ }
133
+ const mappedKind =
134
+ kind === "clarification-request"
135
+ ? "clarification"
136
+ : kind === "human-escalation"
137
+ ? "escalation"
138
+ : "feedback";
139
+ const rawStatus = normalizeText(record.status, "open");
140
+ let mappedState = "open";
141
+ if (rawStatus === "in_progress" || rawStatus === "pending") {
142
+ mappedState = "pending";
143
+ } else if (rawStatus === "resolved" || rawStatus === "closed") {
144
+ mappedState = "resolved";
145
+ } else if (rawStatus === "answered") {
146
+ mappedState = "answered";
147
+ }
148
+ results.push(
149
+ normalizeHumanInputRequest({
150
+ requestId: normalizeText(record.id, null),
151
+ kind: mappedKind,
152
+ state: mappedState,
153
+ title: normalizeText(record.summary, null),
154
+ detail: normalizeText(record.detail, null),
155
+ requestedBy: normalizeText(record.agentId, null),
156
+ assignedTo: null,
157
+ createdAt: normalizeText(record.createdAt, now),
158
+ updatedAt: normalizeText(record.updatedAt, now),
159
+ }),
160
+ );
161
+ }
162
+
163
+ // Process human escalations from coordination state
164
+ const humanEscalations = Array.isArray(coordState.humanEscalations)
165
+ ? coordState.humanEscalations
166
+ : [];
167
+ for (const record of humanEscalations) {
168
+ if (!isPlainObject(record)) continue;
169
+ const rawStatus = normalizeText(record.status, "open");
170
+ let mappedState = "escalated";
171
+ if (rawStatus === "resolved" || rawStatus === "closed") {
172
+ mappedState = "resolved";
173
+ } else if (rawStatus === "answered") {
174
+ mappedState = "answered";
175
+ }
176
+ results.push(
177
+ normalizeHumanInputRequest({
178
+ requestId: normalizeText(record.id, null),
179
+ kind: "escalation",
180
+ state: mappedState,
181
+ title: normalizeText(record.summary, null),
182
+ detail: normalizeText(record.detail, null),
183
+ requestedBy: normalizeText(record.agentId, null),
184
+ assignedTo: "operator",
185
+ createdAt: normalizeText(record.createdAt, now),
186
+ updatedAt: normalizeText(record.updatedAt, now),
187
+ escalatedAt: normalizeText(record.createdAt, now),
188
+ }),
189
+ );
190
+ }
191
+
192
+ // Process feedback requests
193
+ for (const record of feedbackList) {
194
+ if (!isPlainObject(record)) continue;
195
+ const rawStatus = normalizeText(record.status, "pending");
196
+ let mappedState = "pending";
197
+ if (rawStatus === "answered") {
198
+ mappedState = "answered";
199
+ } else if (rawStatus === "resolved" || rawStatus === "closed") {
200
+ mappedState = "resolved";
201
+ }
202
+ results.push(
203
+ normalizeHumanInputRequest({
204
+ requestId: normalizeText(record.id, null),
205
+ kind: "feedback",
206
+ state: mappedState,
207
+ title: normalizeText(record.question, null),
208
+ detail: normalizeText(record.context, null),
209
+ requestedBy: normalizeText(record.agentId, null),
210
+ assignedTo: "operator",
211
+ createdAt: normalizeText(record.createdAt, now),
212
+ updatedAt: normalizeText(record.updatedAt, now),
213
+ answeredAt: normalizeText(record.response?.answeredAt, null),
214
+ answer: normalizeText(record.response?.text, null),
215
+ }),
216
+ );
217
+ }
218
+
219
+ return results;
220
+ }
221
+
222
+ export function evaluateHumanInputTimeout(request, now = Date.now()) {
223
+ const source = isPlainObject(request) ? request : {};
224
+ const createdAtMs = Date.parse(source.createdAt || "");
225
+ if (!Number.isFinite(createdAtMs)) {
226
+ return { expired: false, shouldEscalate: false, elapsedMs: 0 };
227
+ }
228
+ const elapsedMs = Math.max(0, now - createdAtMs);
229
+ const policy = isPlainObject(source.timeoutPolicy)
230
+ ? source.timeoutPolicy
231
+ : DEFAULT_TIMEOUT_POLICY;
232
+ const maxWaitMs = Number.isFinite(policy.maxWaitMs)
233
+ ? policy.maxWaitMs
234
+ : DEFAULT_TIMEOUT_POLICY.maxWaitMs;
235
+ const escalateAfterMs = Number.isFinite(policy.escalateAfterMs)
236
+ ? policy.escalateAfterMs
237
+ : DEFAULT_TIMEOUT_POLICY.escalateAfterMs;
238
+ const expired = elapsedMs >= maxWaitMs;
239
+ const shouldEscalate = elapsedMs >= escalateAfterMs;
240
+ return { expired, shouldEscalate, elapsedMs };
241
+ }
242
+
243
+ export function computeHumanInputMetrics(requests) {
244
+ const list = Array.isArray(requests) ? requests : [];
245
+ const counts = { open: 0, pending: 0, answered: 0, escalated: 0, resolved: 0 };
246
+ let blocking = 0;
247
+ let overdueCount = 0;
248
+ let totalResolutionMs = 0;
249
+ let resolvedWithTimesCount = 0;
250
+
251
+ for (const request of list) {
252
+ const source = isPlainObject(request) ? request : {};
253
+ const state = normalizeText(source.state, "open");
254
+ if (state in counts) {
255
+ counts[state] += 1;
256
+ }
257
+ if (BLOCKING_STATES.has(state)) {
258
+ blocking += 1;
259
+ }
260
+ // Check overdue based on timeout policy
261
+ const timeout = evaluateHumanInputTimeout(source);
262
+ if (timeout.expired && BLOCKING_STATES.has(state)) {
263
+ overdueCount += 1;
264
+ }
265
+ // Compute resolution time for resolved requests
266
+ if (state === "resolved" && source.createdAt && source.resolvedAt) {
267
+ const createdMs = Date.parse(source.createdAt);
268
+ const resolvedMs = Date.parse(source.resolvedAt);
269
+ if (Number.isFinite(createdMs) && Number.isFinite(resolvedMs) && resolvedMs >= createdMs) {
270
+ totalResolutionMs += resolvedMs - createdMs;
271
+ resolvedWithTimesCount += 1;
272
+ }
273
+ }
274
+ }
275
+
276
+ return {
277
+ total: list.length,
278
+ open: counts.open,
279
+ pending: counts.pending,
280
+ answered: counts.answered,
281
+ escalated: counts.escalated,
282
+ resolved: counts.resolved,
283
+ blocking,
284
+ overdueCount,
285
+ avgResolutionMs: resolvedWithTimesCount > 0
286
+ ? Math.round(totalResolutionMs / resolvedWithTimesCount)
287
+ : null,
288
+ };
289
+ }
@@ -92,6 +92,33 @@ const REQUIRED_GITIGNORE_ENTRIES = [
92
92
  "docs/research/papers/",
93
93
  "docs/research/articles/",
94
94
  ];
95
+ const PLANNER_MIGRATION_REQUIRED_SURFACES = [
96
+ {
97
+ id: "planner-role",
98
+ label: "docs/agents/wave-planner-role.md",
99
+ path: "docs/agents/wave-planner-role.md",
100
+ kind: "file",
101
+ },
102
+ {
103
+ id: "planner-skill",
104
+ label: "skills/role-planner/",
105
+ path: "skills/role-planner",
106
+ kind: "dir",
107
+ },
108
+ {
109
+ id: "planner-context7",
110
+ label: "docs/context7/planner-agent/",
111
+ path: "docs/context7/planner-agent",
112
+ kind: "dir",
113
+ },
114
+ {
115
+ id: "planner-lessons",
116
+ label: "docs/reference/wave-planning-lessons.md",
117
+ path: "docs/reference/wave-planning-lessons.md",
118
+ kind: "file",
119
+ },
120
+ ];
121
+ const PLANNER_REQUIRED_BUNDLE_ID = "planner-agentic";
95
122
 
96
123
  function collectDeclaredDeployKinds(waves = []) {
97
124
  return Array.from(
@@ -171,6 +198,28 @@ function copyTemplateFile(relPath) {
171
198
  throw new Error(`Missing packaged template: ${relPath}`);
172
199
  }
173
200
  ensureDirectory(path.dirname(targetPath));
201
+ if (relPath === "docs/plans/component-cutover-matrix.json") {
202
+ const payload = readJsonOrNull(sourcePath);
203
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
204
+ throw new Error(`Invalid packaged template JSON: ${relPath}`);
205
+ }
206
+ const components = Object.fromEntries(
207
+ Object.entries(payload.components || {}).map(([componentId, component]) => [
208
+ componentId,
209
+ {
210
+ ...component,
211
+ promotions: Array.isArray(component?.promotions)
212
+ ? component.promotions.filter((entry) => Number(entry?.wave) === 0)
213
+ : [],
214
+ },
215
+ ]),
216
+ );
217
+ writeJsonAtomic(targetPath, {
218
+ ...payload,
219
+ components,
220
+ });
221
+ return targetPath;
222
+ }
174
223
  fs.copyFileSync(sourcePath, targetPath);
175
224
  return targetPath;
176
225
  }
@@ -222,6 +271,8 @@ function slugifyVersion(value) {
222
271
  }
223
272
 
224
273
  function formatUpgradeReport(report) {
274
+ const plannerMigrationErrors = report.doctor.errors.filter((issue) => isPlannerMigrationIssue(issue));
275
+ const otherDoctorErrors = report.doctor.errors.filter((issue) => !isPlannerMigrationIssue(issue));
225
276
  return [
226
277
  `# Wave Upgrade Report`,
227
278
  "",
@@ -238,12 +289,27 @@ function formatUpgradeReport(report) {
238
289
  "",
239
290
  "- No repo-owned plans, waves, role prompts, or config files were overwritten.",
240
291
  "- New runtime behavior comes from the installed package version.",
292
+ ...(report.initMode === "adopt-existing" && plannerMigrationErrors.length > 0
293
+ ? [
294
+ "",
295
+ "## Adopted Repo Follow-Up",
296
+ "",
297
+ "- This workspace was adopted from an existing repo-owned Wave surface.",
298
+ "- `wave upgrade` does not copy new planner starter docs, skills, or Context7 bundle entries into adopted repos.",
299
+ ...plannerMigrationErrors.map((issue) => `- Error: ${issue}`),
300
+ ]
301
+ : []),
302
+ ...(report.initMode === "adopt-existing" && plannerMigrationErrors.length > 0
303
+ ? [
304
+ "- After syncing that planner surface, rerun `pnpm exec wave doctor` before relying on `wave draft --agentic` or planner-aware validation.",
305
+ ]
306
+ : []),
241
307
  ...(report.doctor.errors.length > 0 || report.doctor.warnings.length > 0
242
308
  ? [
243
309
  "",
244
310
  "## Follow-Up",
245
311
  "",
246
- ...report.doctor.errors.map((issue) => `- Error: ${issue}`),
312
+ ...otherDoctorErrors.map((issue) => `- Error: ${issue}`),
247
313
  ...report.doctor.warnings.map((issue) => `- Warning: ${issue}`),
248
314
  ]
249
315
  : []),
@@ -275,6 +341,48 @@ function plannerRequiredPaths() {
275
341
  ).sort();
276
342
  }
277
343
 
344
+ function isPlannerMigrationIssue(issue) {
345
+ return String(issue || "").startsWith("Planner starter surface is incomplete");
346
+ }
347
+
348
+ function missingPlannerMigrationSurfaceLabels() {
349
+ const missing = [];
350
+ for (const surface of PLANNER_MIGRATION_REQUIRED_SURFACES) {
351
+ const targetPath = path.join(REPO_ROOT, surface.path);
352
+ if (!fs.existsSync(targetPath)) {
353
+ missing.push(surface.label);
354
+ continue;
355
+ }
356
+ if (surface.kind === "dir") {
357
+ try {
358
+ if (fs.readdirSync(targetPath).length === 0) {
359
+ missing.push(surface.label);
360
+ }
361
+ } catch {
362
+ missing.push(surface.label);
363
+ }
364
+ }
365
+ }
366
+ return missing;
367
+ }
368
+
369
+ function plannerMigrationIssue(config, context7BundleIndex) {
370
+ const missing = missingPlannerMigrationSurfaceLabels();
371
+ const bundleId = String(config?.planner?.agentic?.context7Bundle || "").trim();
372
+ const bundleEntryMissing =
373
+ bundleId === "" || bundleId === PLANNER_REQUIRED_BUNDLE_ID
374
+ ? !context7BundleIndex?.bundles?.[PLANNER_REQUIRED_BUNDLE_ID]
375
+ : false;
376
+ if (missing.length === 0 && !bundleEntryMissing) {
377
+ return null;
378
+ }
379
+ const remediationItems = missing.slice();
380
+ if (bundleEntryMissing) {
381
+ remediationItems.push(`docs/context7/bundles.json#${PLANNER_REQUIRED_BUNDLE_ID}`);
382
+ }
383
+ return `Planner starter surface is incomplete for 0.7.x workspaces. Sync ${remediationItems.join(", ")} from the packaged release, then rerun \`pnpm exec wave doctor\`.`;
384
+ }
385
+
278
386
  export function runDoctor() {
279
387
  const errors = [];
280
388
  const warnings = [];
@@ -320,14 +428,22 @@ export function runDoctor() {
320
428
  }
321
429
  }
322
430
  const context7BundleIndex = loadContext7BundleIndex(lanePaths.context7BundleIndexPath);
431
+ const plannerMigration = plannerMigrationIssue(config, context7BundleIndex);
432
+ if (plannerMigration) {
433
+ errors.push(plannerMigration);
434
+ }
323
435
  const plannerPaths = plannerRequiredPaths();
324
436
  for (const relPath of plannerPaths) {
325
- if (!fs.existsSync(path.join(REPO_ROOT, relPath))) {
437
+ if (!fs.existsSync(path.join(REPO_ROOT, relPath)) && !plannerMigration) {
326
438
  errors.push(`Missing planner file: ${relPath}`);
327
439
  }
328
440
  }
329
441
  const plannerBundleId = String(config.planner?.agentic?.context7Bundle || "").trim();
330
- if (plannerBundleId && !context7BundleIndex.bundles[plannerBundleId]) {
442
+ if (
443
+ plannerBundleId &&
444
+ !context7BundleIndex.bundles[plannerBundleId] &&
445
+ !(plannerMigration && plannerBundleId === PLANNER_REQUIRED_BUNDLE_ID)
446
+ ) {
331
447
  errors.push(
332
448
  `planner.agentic.context7Bundle references unknown bundle "${plannerBundleId}".`,
333
449
  );
@@ -484,6 +600,7 @@ export function upgradeWorkspace() {
484
600
  previousVersion,
485
601
  currentVersion: metadata.version,
486
602
  generatedAt,
603
+ initMode: existingState.initMode || null,
487
604
  releases,
488
605
  doctor,
489
606
  };