@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.
- package/CHANGELOG.md +40 -0
- package/README.md +9 -8
- package/docs/guides/planner.md +19 -0
- package/docs/guides/terminal-surfaces.md +12 -0
- package/docs/plans/component-cutover-matrix.json +50 -3
- package/docs/plans/current-state.md +1 -1
- package/docs/plans/end-state-architecture.md +927 -0
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +26 -0
- package/docs/plans/wave-orchestrator.md +4 -7
- package/docs/plans/waves/wave-1.md +376 -0
- package/docs/plans/waves/wave-2.md +292 -0
- package/docs/plans/waves/wave-3.md +342 -0
- package/docs/plans/waves/wave-4.md +391 -0
- package/docs/plans/waves/wave-5.md +382 -0
- package/docs/plans/waves/wave-6.md +321 -0
- package/docs/reference/cli-reference.md +547 -0
- package/docs/reference/coordination-and-closure.md +1 -1
- package/docs/reference/npmjs-trusted-publishing.md +2 -2
- package/docs/reference/runtime-config/README.md +2 -2
- package/docs/reference/runtime-config/codex.md +2 -1
- package/docs/reference/sample-waves.md +4 -4
- package/package.json +1 -1
- package/releases/manifest.json +43 -2
- package/scripts/wave-orchestrator/agent-state.mjs +458 -35
- package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
- package/scripts/wave-orchestrator/control-cli.mjs +119 -20
- package/scripts/wave-orchestrator/coordination.mjs +11 -10
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +82 -2
- package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
- package/scripts/wave-orchestrator/install.mjs +120 -3
- package/scripts/wave-orchestrator/launcher-derived-state.mjs +915 -0
- package/scripts/wave-orchestrator/launcher-gates.mjs +1061 -0
- package/scripts/wave-orchestrator/launcher-retry.mjs +873 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +9 -9
- package/scripts/wave-orchestrator/launcher-supervisor.mjs +704 -0
- package/scripts/wave-orchestrator/launcher.mjs +317 -2999
- package/scripts/wave-orchestrator/task-entity.mjs +557 -0
- package/scripts/wave-orchestrator/terminals.mjs +1 -1
- package/scripts/wave-orchestrator/wave-files.mjs +138 -20
- package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
- 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
|
-
...
|
|
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 (
|
|
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
|
};
|