@femtomc/mu-agent 26.2.86 → 26.2.88
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/README.md +4 -4
- package/dist/extensions/planning-ui.d.ts.map +1 -1
- package/dist/extensions/planning-ui.js +728 -92
- package/dist/extensions/subagents-ui.d.ts.map +1 -1
- package/dist/extensions/subagents-ui.js +762 -223
- package/package.json +2 -2
- package/prompts/skills/planning/SKILL.md +31 -1
- package/prompts/skills/subagents/SKILL.md +36 -4
|
@@ -5,12 +5,61 @@ const DEFAULT_STEPS = [
|
|
|
5
5
|
"Present plan with IDs, ordering, risks",
|
|
6
6
|
"Refine with user feedback until approved",
|
|
7
7
|
];
|
|
8
|
+
const BAR_CHARS = ["░", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
9
|
+
function phaseTone(phase) {
|
|
10
|
+
switch (phase) {
|
|
11
|
+
case "investigating":
|
|
12
|
+
return "dim";
|
|
13
|
+
case "drafting":
|
|
14
|
+
return "accent";
|
|
15
|
+
case "reviewing":
|
|
16
|
+
return "warning";
|
|
17
|
+
case "waiting_user":
|
|
18
|
+
return "warning";
|
|
19
|
+
case "blocked":
|
|
20
|
+
return "warning";
|
|
21
|
+
case "executing":
|
|
22
|
+
return "accent";
|
|
23
|
+
case "approved":
|
|
24
|
+
return "success";
|
|
25
|
+
case "done":
|
|
26
|
+
return "success";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function confidenceTone(confidence) {
|
|
30
|
+
switch (confidence) {
|
|
31
|
+
case "low":
|
|
32
|
+
return "warning";
|
|
33
|
+
case "medium":
|
|
34
|
+
return "accent";
|
|
35
|
+
case "high":
|
|
36
|
+
return "success";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function progressBar(done, total, width = 10) {
|
|
40
|
+
if (width <= 0 || total <= 0) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
const clampedDone = Math.max(0, Math.min(total, done));
|
|
44
|
+
const filled = (clampedDone / total) * width;
|
|
45
|
+
const full = Math.floor(filled);
|
|
46
|
+
const frac = filled - full;
|
|
47
|
+
const fracIdx = Math.round(frac * (BAR_CHARS.length - 1));
|
|
48
|
+
const empty = width - full - (fracIdx > 0 ? 1 : 0);
|
|
49
|
+
return (BAR_CHARS[BAR_CHARS.length - 1].repeat(full) +
|
|
50
|
+
(fracIdx > 0 ? BAR_CHARS[fracIdx] : "") +
|
|
51
|
+
BAR_CHARS[0].repeat(Math.max(0, empty)));
|
|
52
|
+
}
|
|
8
53
|
function createDefaultState() {
|
|
9
54
|
return {
|
|
10
55
|
enabled: false,
|
|
11
56
|
phase: "investigating",
|
|
12
57
|
rootIssueId: null,
|
|
13
58
|
steps: DEFAULT_STEPS.map((label) => ({ label, done: false })),
|
|
59
|
+
waitingOnUser: false,
|
|
60
|
+
nextAction: null,
|
|
61
|
+
blocker: null,
|
|
62
|
+
confidence: "medium",
|
|
14
63
|
};
|
|
15
64
|
}
|
|
16
65
|
function summarizePhase(phase) {
|
|
@@ -21,9 +70,176 @@ function summarizePhase(phase) {
|
|
|
21
70
|
return "drafting";
|
|
22
71
|
case "reviewing":
|
|
23
72
|
return "reviewing";
|
|
73
|
+
case "waiting_user":
|
|
74
|
+
return "waiting-user";
|
|
75
|
+
case "blocked":
|
|
76
|
+
return "blocked";
|
|
77
|
+
case "executing":
|
|
78
|
+
return "executing";
|
|
24
79
|
case "approved":
|
|
25
80
|
return "approved";
|
|
81
|
+
case "done":
|
|
82
|
+
return "done";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function parsePlanningPhase(raw) {
|
|
86
|
+
const value = raw.trim().toLowerCase();
|
|
87
|
+
if (value === "investigating" ||
|
|
88
|
+
value === "drafting" ||
|
|
89
|
+
value === "reviewing" ||
|
|
90
|
+
value === "waiting_user" ||
|
|
91
|
+
value === "waiting-user" ||
|
|
92
|
+
value === "blocked" ||
|
|
93
|
+
value === "executing" ||
|
|
94
|
+
value === "approved" ||
|
|
95
|
+
value === "done") {
|
|
96
|
+
return value === "waiting-user" ? "waiting_user" : value;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function parsePlanningConfidence(raw) {
|
|
101
|
+
const value = raw.trim().toLowerCase();
|
|
102
|
+
if (value === "low" || value === "medium" || value === "high") {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function parseSnapshotFormat(raw) {
|
|
108
|
+
const value = (raw ?? "compact").trim().toLowerCase();
|
|
109
|
+
return value === "multiline" ? "multiline" : "compact";
|
|
110
|
+
}
|
|
111
|
+
function normalizeMaybeClear(raw) {
|
|
112
|
+
const trimmed = raw.trim();
|
|
113
|
+
if (trimmed.length === 0) {
|
|
114
|
+
return { ok: false, error: "Value must not be empty." };
|
|
115
|
+
}
|
|
116
|
+
if (trimmed.toLowerCase() === "clear") {
|
|
117
|
+
return { ok: true, value: null };
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, value: trimmed };
|
|
120
|
+
}
|
|
121
|
+
function normalizeSteps(labelsRaw) {
|
|
122
|
+
if (!Array.isArray(labelsRaw)) {
|
|
123
|
+
return { ok: false, error: "Steps must be an array of strings." };
|
|
124
|
+
}
|
|
125
|
+
const labels = [];
|
|
126
|
+
for (let i = 0; i < labelsRaw.length; i += 1) {
|
|
127
|
+
const value = labelsRaw[i];
|
|
128
|
+
if (typeof value !== "string") {
|
|
129
|
+
return { ok: false, error: `Step ${i + 1} must be a string.` };
|
|
130
|
+
}
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
if (trimmed.length === 0) {
|
|
133
|
+
return { ok: false, error: `Step ${i + 1} must not be empty.` };
|
|
134
|
+
}
|
|
135
|
+
labels.push(trimmed);
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, labels };
|
|
138
|
+
}
|
|
139
|
+
function validateStepIndex(step, max, allowAppend = false) {
|
|
140
|
+
if (typeof step !== "number" || !Number.isFinite(step)) {
|
|
141
|
+
return { ok: false, error: "Step index must be a number." };
|
|
142
|
+
}
|
|
143
|
+
const parsed = Math.trunc(step);
|
|
144
|
+
const upperBound = allowAppend ? max + 1 : max;
|
|
145
|
+
if (parsed < 1 || parsed > upperBound) {
|
|
146
|
+
return { ok: false, error: `Step index out of range (1-${upperBound}).` };
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, index: parsed - 1 };
|
|
149
|
+
}
|
|
150
|
+
function applyStepUpdates(state, updatesRaw) {
|
|
151
|
+
if (!Array.isArray(updatesRaw)) {
|
|
152
|
+
return { ok: false, error: "step_updates must be an array." };
|
|
153
|
+
}
|
|
154
|
+
let changed = 0;
|
|
155
|
+
for (let i = 0; i < updatesRaw.length; i += 1) {
|
|
156
|
+
const raw = updatesRaw[i];
|
|
157
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
158
|
+
return { ok: false, error: `step_updates[${i}] must be an object.` };
|
|
159
|
+
}
|
|
160
|
+
const update = raw;
|
|
161
|
+
const indexRaw = update.index;
|
|
162
|
+
if (typeof indexRaw !== "number" || !Number.isFinite(indexRaw)) {
|
|
163
|
+
return { ok: false, error: `step_updates[${i}].index must be a number.` };
|
|
164
|
+
}
|
|
165
|
+
const stepIndex = Math.trunc(indexRaw);
|
|
166
|
+
if (stepIndex < 1 || stepIndex > state.steps.length) {
|
|
167
|
+
return { ok: false, error: `step_updates[${i}].index out of range (1-${state.steps.length}).` };
|
|
168
|
+
}
|
|
169
|
+
const doneRaw = update.done;
|
|
170
|
+
const labelRaw = update.label;
|
|
171
|
+
if (doneRaw === undefined && labelRaw === undefined) {
|
|
172
|
+
return { ok: false, error: `step_updates[${i}] must include done and/or label.` };
|
|
173
|
+
}
|
|
174
|
+
const step = state.steps[stepIndex - 1];
|
|
175
|
+
if (!step) {
|
|
176
|
+
return { ok: false, error: `step_updates[${i}] references missing step.` };
|
|
177
|
+
}
|
|
178
|
+
if (doneRaw !== undefined) {
|
|
179
|
+
if (typeof doneRaw !== "boolean") {
|
|
180
|
+
return { ok: false, error: `step_updates[${i}].done must be a boolean.` };
|
|
181
|
+
}
|
|
182
|
+
if (step.done !== doneRaw) {
|
|
183
|
+
step.done = doneRaw;
|
|
184
|
+
changed += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (labelRaw !== undefined) {
|
|
188
|
+
if (typeof labelRaw !== "string") {
|
|
189
|
+
return { ok: false, error: `step_updates[${i}].label must be a string.` };
|
|
190
|
+
}
|
|
191
|
+
const trimmed = labelRaw.trim();
|
|
192
|
+
if (trimmed.length === 0) {
|
|
193
|
+
return { ok: false, error: `step_updates[${i}].label must not be empty.` };
|
|
194
|
+
}
|
|
195
|
+
if (step.label !== trimmed) {
|
|
196
|
+
step.label = trimmed;
|
|
197
|
+
changed += 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, changed };
|
|
202
|
+
}
|
|
203
|
+
function shortLabel(value, fallback, maxLen = 48) {
|
|
204
|
+
if (!value || value.trim().length === 0) {
|
|
205
|
+
return fallback;
|
|
206
|
+
}
|
|
207
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
208
|
+
if (compact.length <= maxLen) {
|
|
209
|
+
return compact;
|
|
210
|
+
}
|
|
211
|
+
return `${compact.slice(0, Math.max(0, maxLen - 1))}…`;
|
|
212
|
+
}
|
|
213
|
+
function planningSnapshot(state, format) {
|
|
214
|
+
const done = state.steps.filter((step) => step.done).length;
|
|
215
|
+
const total = state.steps.length;
|
|
216
|
+
const phase = summarizePhase(state.phase);
|
|
217
|
+
const root = state.rootIssueId ?? "(unset)";
|
|
218
|
+
const waiting = state.waitingOnUser ? "yes" : "no";
|
|
219
|
+
const next = shortLabel(state.nextAction, "(unset)");
|
|
220
|
+
const blocker = shortLabel(state.blocker, "(none)");
|
|
221
|
+
if (format === "multiline") {
|
|
222
|
+
return [
|
|
223
|
+
`Planning HUD snapshot`,
|
|
224
|
+
`phase: ${phase}`,
|
|
225
|
+
`root: ${root}`,
|
|
226
|
+
`steps: ${done}/${total}`,
|
|
227
|
+
`waiting_on_user: ${waiting}`,
|
|
228
|
+
`confidence: ${state.confidence}`,
|
|
229
|
+
`next_action: ${next}`,
|
|
230
|
+
`blocker: ${blocker}`,
|
|
231
|
+
].join("\n");
|
|
26
232
|
}
|
|
233
|
+
return [
|
|
234
|
+
`HUD(plan)`,
|
|
235
|
+
`phase=${phase}`,
|
|
236
|
+
`root=${root}`,
|
|
237
|
+
`steps=${done}/${total}`,
|
|
238
|
+
`waiting=${waiting}`,
|
|
239
|
+
`confidence=${state.confidence}`,
|
|
240
|
+
`next=${next}`,
|
|
241
|
+
`blocker=${blocker}`,
|
|
242
|
+
].join(" · ");
|
|
27
243
|
}
|
|
28
244
|
function renderPlanningUi(ctx, state) {
|
|
29
245
|
if (!ctx.hasUI) {
|
|
@@ -37,36 +253,110 @@ function renderPlanningUi(ctx, state) {
|
|
|
37
253
|
const done = state.steps.filter((step) => step.done).length;
|
|
38
254
|
const total = state.steps.length;
|
|
39
255
|
const phase = summarizePhase(state.phase);
|
|
40
|
-
const
|
|
41
|
-
|
|
256
|
+
const phaseColor = phaseTone(state.phase);
|
|
257
|
+
const confidenceColor = confidenceTone(state.confidence);
|
|
258
|
+
const rootLabel = state.rootIssueId ?? "(unset)";
|
|
259
|
+
const meter = progressBar(done, total, 10);
|
|
260
|
+
const waitingLabel = state.waitingOnUser ? "yes" : "no";
|
|
261
|
+
const waitingColor = state.waitingOnUser ? "warning" : "dim";
|
|
262
|
+
const blockerLabel = shortLabel(state.blocker, "(none)", 56);
|
|
263
|
+
const blockerColor = state.blocker ? "warning" : "dim";
|
|
264
|
+
ctx.ui.setStatus("mu-planning", [
|
|
265
|
+
ctx.ui.theme.fg("dim", "plan"),
|
|
266
|
+
ctx.ui.theme.fg(phaseColor, phase),
|
|
267
|
+
ctx.ui.theme.fg("dim", `${done}/${total}`),
|
|
268
|
+
ctx.ui.theme.fg(phaseColor, meter),
|
|
269
|
+
ctx.ui.theme.fg(waitingColor, `wait:${waitingLabel}`),
|
|
270
|
+
ctx.ui.theme.fg("muted", `root:${rootLabel}`),
|
|
271
|
+
].join(` ${ctx.ui.theme.fg("muted", "·")} `));
|
|
42
272
|
const lines = [
|
|
43
|
-
ctx.ui.theme.fg("accent",
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
273
|
+
ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Planning board")),
|
|
274
|
+
` ${ctx.ui.theme.fg("muted", "phase:")} ${ctx.ui.theme.fg(phaseColor, phase)}`,
|
|
275
|
+
` ${ctx.ui.theme.fg("muted", "progress:")} ${ctx.ui.theme.fg("dim", `${done}/${total}`)} ${ctx.ui.theme.fg(phaseColor, meter)}`,
|
|
276
|
+
` ${ctx.ui.theme.fg("muted", "root:")} ${ctx.ui.theme.fg("dim", rootLabel)}`,
|
|
277
|
+
` ${ctx.ui.theme.fg("muted", "waiting_on_user:")} ${ctx.ui.theme.fg(waitingColor, waitingLabel)}`,
|
|
278
|
+
` ${ctx.ui.theme.fg("muted", "confidence:")} ${ctx.ui.theme.fg(confidenceColor, state.confidence)}`,
|
|
279
|
+
` ${ctx.ui.theme.fg("muted", "next_action:")} ${ctx.ui.theme.fg("dim", shortLabel(state.nextAction, "(unset)", 72))}`,
|
|
280
|
+
` ${ctx.ui.theme.fg("muted", "blocker:")} ${ctx.ui.theme.fg(blockerColor, blockerLabel)}`,
|
|
281
|
+
` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`,
|
|
51
282
|
];
|
|
283
|
+
if (state.steps.length === 0) {
|
|
284
|
+
lines.push(ctx.ui.theme.fg("muted", " (no checklist steps configured)"));
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
for (let index = 0; index < state.steps.length; index += 1) {
|
|
288
|
+
const step = state.steps[index];
|
|
289
|
+
const mark = step.done ? ctx.ui.theme.fg("success", "☑") : ctx.ui.theme.fg("muted", "☐");
|
|
290
|
+
const label = step.done ? ctx.ui.theme.fg("dim", step.label) : ctx.ui.theme.fg("text", step.label);
|
|
291
|
+
lines.push(`${mark} ${ctx.ui.theme.fg("muted", `${index + 1}.`)} ${label}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
lines.push(ctx.ui.theme.fg("muted", " /mu plan status · /mu plan snapshot"));
|
|
52
295
|
ctx.ui.setWidget("mu-planning", lines, { placement: "belowEditor" });
|
|
53
296
|
}
|
|
54
297
|
function planningUsageText() {
|
|
55
298
|
return [
|
|
56
299
|
"Usage:",
|
|
57
|
-
" /mu plan on|off|toggle|status|reset",
|
|
58
|
-
" /mu plan phase <investigating|drafting|reviewing|approved>",
|
|
300
|
+
" /mu plan on|off|toggle|status|reset|snapshot",
|
|
301
|
+
" /mu plan phase <investigating|drafting|reviewing|waiting-user|blocked|executing|approved|done>",
|
|
59
302
|
" /mu plan root <issue-id|clear>",
|
|
60
303
|
" /mu plan check <n> | /mu plan uncheck <n> | /mu plan toggle-step <n>",
|
|
304
|
+
" /mu plan add-step <label> | remove-step <n> | relabel-step <n> <label>",
|
|
305
|
+
" /mu plan waiting <on|off> | confidence <low|medium|high>",
|
|
306
|
+
" /mu plan next <text|clear> | blocker <text|clear>",
|
|
61
307
|
].join("\n");
|
|
62
308
|
}
|
|
63
|
-
function
|
|
64
|
-
const value = raw.trim().toLowerCase();
|
|
65
|
-
if (value === "
|
|
66
|
-
return
|
|
309
|
+
function parseOnOff(raw) {
|
|
310
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
311
|
+
if (value === "on" || value === "yes" || value === "true" || value === "1") {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (value === "off" || value === "no" || value === "false" || value === "0") {
|
|
315
|
+
return false;
|
|
67
316
|
}
|
|
68
317
|
return null;
|
|
69
318
|
}
|
|
319
|
+
function planningDetails(state) {
|
|
320
|
+
return {
|
|
321
|
+
enabled: state.enabled,
|
|
322
|
+
phase: state.phase,
|
|
323
|
+
root_issue_id: state.rootIssueId,
|
|
324
|
+
waiting_on_user: state.waitingOnUser,
|
|
325
|
+
next_action: state.nextAction,
|
|
326
|
+
blocker: state.blocker,
|
|
327
|
+
confidence: state.confidence,
|
|
328
|
+
steps: state.steps.map((step, index) => ({
|
|
329
|
+
index: index + 1,
|
|
330
|
+
label: step.label,
|
|
331
|
+
done: step.done,
|
|
332
|
+
})),
|
|
333
|
+
snapshot_compact: planningSnapshot(state, "compact"),
|
|
334
|
+
snapshot_multiline: planningSnapshot(state, "multiline"),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function planningStatusSummary(state) {
|
|
338
|
+
const done = state.steps.filter((step) => step.done).length;
|
|
339
|
+
const root = state.rootIssueId ?? "(unset)";
|
|
340
|
+
return [
|
|
341
|
+
`Planning HUD: ${state.enabled ? "enabled" : "disabled"}`,
|
|
342
|
+
`phase: ${state.phase}`,
|
|
343
|
+
`root: ${root}`,
|
|
344
|
+
`steps: ${done}/${state.steps.length}`,
|
|
345
|
+
`waiting_on_user: ${state.waitingOnUser ? "yes" : "no"}`,
|
|
346
|
+
`confidence: ${state.confidence}`,
|
|
347
|
+
`next_action: ${shortLabel(state.nextAction, "(unset)", 120)}`,
|
|
348
|
+
`blocker: ${shortLabel(state.blocker, "(none)", 120)}`,
|
|
349
|
+
].join("\n");
|
|
350
|
+
}
|
|
351
|
+
function planningToolError(message) {
|
|
352
|
+
return {
|
|
353
|
+
content: [{ type: "text", text: message }],
|
|
354
|
+
details: {
|
|
355
|
+
ok: false,
|
|
356
|
+
error: message,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
70
360
|
export function planningUiExtension(pi) {
|
|
71
361
|
let state = createDefaultState();
|
|
72
362
|
const notify = (ctx, message, level = "info") => {
|
|
@@ -75,6 +365,264 @@ export function planningUiExtension(pi) {
|
|
|
75
365
|
const refresh = (ctx) => {
|
|
76
366
|
renderPlanningUi(ctx, state);
|
|
77
367
|
};
|
|
368
|
+
const applyPlanningAction = (params) => {
|
|
369
|
+
switch (params.action) {
|
|
370
|
+
case "status":
|
|
371
|
+
return { ok: true, message: planningStatusSummary(state), level: "info" };
|
|
372
|
+
case "snapshot": {
|
|
373
|
+
const format = parseSnapshotFormat(params.snapshot_format);
|
|
374
|
+
return { ok: true, message: planningSnapshot(state, format), level: "info" };
|
|
375
|
+
}
|
|
376
|
+
case "on":
|
|
377
|
+
state.enabled = true;
|
|
378
|
+
return { ok: true, message: "Planning HUD enabled.", level: "info" };
|
|
379
|
+
case "off":
|
|
380
|
+
state.enabled = false;
|
|
381
|
+
return { ok: true, message: "Planning HUD disabled.", level: "info" };
|
|
382
|
+
case "toggle":
|
|
383
|
+
state.enabled = !state.enabled;
|
|
384
|
+
return { ok: true, message: `Planning HUD ${state.enabled ? "enabled" : "disabled"}.`, level: "info" };
|
|
385
|
+
case "reset":
|
|
386
|
+
state = createDefaultState();
|
|
387
|
+
return { ok: true, message: "Planning HUD state reset.", level: "info" };
|
|
388
|
+
case "phase": {
|
|
389
|
+
const phase = parsePlanningPhase(params.phase ?? "");
|
|
390
|
+
if (!phase) {
|
|
391
|
+
return { ok: false, message: "Invalid phase.", level: "error" };
|
|
392
|
+
}
|
|
393
|
+
state.phase = phase;
|
|
394
|
+
state.enabled = true;
|
|
395
|
+
return { ok: true, message: `Planning phase set to ${phase}.`, level: "info" };
|
|
396
|
+
}
|
|
397
|
+
case "root": {
|
|
398
|
+
const rootRaw = params.root_issue_id;
|
|
399
|
+
if (typeof rootRaw !== "string") {
|
|
400
|
+
return { ok: false, message: "Missing root issue id.", level: "error" };
|
|
401
|
+
}
|
|
402
|
+
const normalized = normalizeMaybeClear(rootRaw);
|
|
403
|
+
if (!normalized.ok) {
|
|
404
|
+
return { ok: false, message: "Missing root issue id.", level: "error" };
|
|
405
|
+
}
|
|
406
|
+
state.rootIssueId = normalized.value;
|
|
407
|
+
state.enabled = true;
|
|
408
|
+
return { ok: true, message: `Planning root set to ${state.rootIssueId ?? "(unset)"}.`, level: "info" };
|
|
409
|
+
}
|
|
410
|
+
case "check":
|
|
411
|
+
case "uncheck":
|
|
412
|
+
case "toggle_step": {
|
|
413
|
+
const parsedIndex = validateStepIndex(params.step, state.steps.length);
|
|
414
|
+
if (!parsedIndex.ok) {
|
|
415
|
+
return { ok: false, message: parsedIndex.error, level: "error" };
|
|
416
|
+
}
|
|
417
|
+
const step = state.steps[parsedIndex.index];
|
|
418
|
+
if (!step) {
|
|
419
|
+
return { ok: false, message: "Step index out of range.", level: "error" };
|
|
420
|
+
}
|
|
421
|
+
if (params.action === "check") {
|
|
422
|
+
step.done = true;
|
|
423
|
+
}
|
|
424
|
+
else if (params.action === "uncheck") {
|
|
425
|
+
step.done = false;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
step.done = !step.done;
|
|
429
|
+
}
|
|
430
|
+
state.enabled = true;
|
|
431
|
+
return { ok: true, message: `Planning step ${parsedIndex.index + 1} updated.`, level: "info" };
|
|
432
|
+
}
|
|
433
|
+
case "set_steps": {
|
|
434
|
+
const normalized = normalizeSteps(params.steps);
|
|
435
|
+
if (!normalized.ok) {
|
|
436
|
+
return { ok: false, message: normalized.error, level: "error" };
|
|
437
|
+
}
|
|
438
|
+
state.steps = normalized.labels.map((label) => ({ label, done: false }));
|
|
439
|
+
state.enabled = true;
|
|
440
|
+
return { ok: true, message: `Planning checklist replaced (${state.steps.length} steps).`, level: "info" };
|
|
441
|
+
}
|
|
442
|
+
case "add_step": {
|
|
443
|
+
const labelRaw = params.label;
|
|
444
|
+
if (typeof labelRaw !== "string" || labelRaw.trim().length === 0) {
|
|
445
|
+
return { ok: false, message: "Missing step label.", level: "error" };
|
|
446
|
+
}
|
|
447
|
+
const label = labelRaw.trim();
|
|
448
|
+
let insertIndex = state.steps.length;
|
|
449
|
+
if (params.step !== undefined) {
|
|
450
|
+
const parsedIndex = validateStepIndex(params.step, state.steps.length, true);
|
|
451
|
+
if (!parsedIndex.ok) {
|
|
452
|
+
return { ok: false, message: parsedIndex.error, level: "error" };
|
|
453
|
+
}
|
|
454
|
+
insertIndex = parsedIndex.index;
|
|
455
|
+
}
|
|
456
|
+
state.steps.splice(insertIndex, 0, { label, done: false });
|
|
457
|
+
state.enabled = true;
|
|
458
|
+
return { ok: true, message: `Added planning step ${insertIndex + 1}.`, level: "info" };
|
|
459
|
+
}
|
|
460
|
+
case "remove_step": {
|
|
461
|
+
const parsedIndex = validateStepIndex(params.step, state.steps.length);
|
|
462
|
+
if (!parsedIndex.ok) {
|
|
463
|
+
return { ok: false, message: parsedIndex.error, level: "error" };
|
|
464
|
+
}
|
|
465
|
+
state.steps.splice(parsedIndex.index, 1);
|
|
466
|
+
state.enabled = true;
|
|
467
|
+
return { ok: true, message: `Removed planning step ${parsedIndex.index + 1}.`, level: "info" };
|
|
468
|
+
}
|
|
469
|
+
case "set_step_label": {
|
|
470
|
+
const parsedIndex = validateStepIndex(params.step, state.steps.length);
|
|
471
|
+
if (!parsedIndex.ok) {
|
|
472
|
+
return { ok: false, message: parsedIndex.error, level: "error" };
|
|
473
|
+
}
|
|
474
|
+
const labelRaw = params.label;
|
|
475
|
+
if (typeof labelRaw !== "string" || labelRaw.trim().length === 0) {
|
|
476
|
+
return { ok: false, message: "Missing step label.", level: "error" };
|
|
477
|
+
}
|
|
478
|
+
const step = state.steps[parsedIndex.index];
|
|
479
|
+
if (!step) {
|
|
480
|
+
return { ok: false, message: "Step index out of range.", level: "error" };
|
|
481
|
+
}
|
|
482
|
+
step.label = labelRaw.trim();
|
|
483
|
+
state.enabled = true;
|
|
484
|
+
return { ok: true, message: `Planning step ${parsedIndex.index + 1} relabeled.`, level: "info" };
|
|
485
|
+
}
|
|
486
|
+
case "set_waiting": {
|
|
487
|
+
if (typeof params.waiting_on_user !== "boolean") {
|
|
488
|
+
return { ok: false, message: "waiting_on_user must be a boolean.", level: "error" };
|
|
489
|
+
}
|
|
490
|
+
state.waitingOnUser = params.waiting_on_user;
|
|
491
|
+
state.enabled = true;
|
|
492
|
+
return {
|
|
493
|
+
ok: true,
|
|
494
|
+
message: `Planning waiting_on_user set to ${state.waitingOnUser ? "yes" : "no"}.`,
|
|
495
|
+
level: "info",
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
case "set_next": {
|
|
499
|
+
const nextRaw = params.next_action;
|
|
500
|
+
if (typeof nextRaw !== "string") {
|
|
501
|
+
return { ok: false, message: "Missing next_action value.", level: "error" };
|
|
502
|
+
}
|
|
503
|
+
const normalized = normalizeMaybeClear(nextRaw);
|
|
504
|
+
if (!normalized.ok) {
|
|
505
|
+
return { ok: false, message: "Missing next_action value.", level: "error" };
|
|
506
|
+
}
|
|
507
|
+
state.nextAction = normalized.value;
|
|
508
|
+
state.enabled = true;
|
|
509
|
+
return {
|
|
510
|
+
ok: true,
|
|
511
|
+
message: `Planning next_action set to ${shortLabel(state.nextAction, "(unset)")}.`,
|
|
512
|
+
level: "info",
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
case "set_blocker": {
|
|
516
|
+
const blockerRaw = params.blocker;
|
|
517
|
+
if (typeof blockerRaw !== "string") {
|
|
518
|
+
return { ok: false, message: "Missing blocker value.", level: "error" };
|
|
519
|
+
}
|
|
520
|
+
const normalized = normalizeMaybeClear(blockerRaw);
|
|
521
|
+
if (!normalized.ok) {
|
|
522
|
+
return { ok: false, message: "Missing blocker value.", level: "error" };
|
|
523
|
+
}
|
|
524
|
+
state.blocker = normalized.value;
|
|
525
|
+
state.enabled = true;
|
|
526
|
+
return {
|
|
527
|
+
ok: true,
|
|
528
|
+
message: `Planning blocker set to ${shortLabel(state.blocker, "(none)")}.`,
|
|
529
|
+
level: "info",
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
case "set_confidence": {
|
|
533
|
+
const confidence = parsePlanningConfidence(params.confidence ?? "");
|
|
534
|
+
if (!confidence) {
|
|
535
|
+
return { ok: false, message: "Invalid confidence.", level: "error" };
|
|
536
|
+
}
|
|
537
|
+
state.confidence = confidence;
|
|
538
|
+
state.enabled = true;
|
|
539
|
+
return { ok: true, message: `Planning confidence set to ${confidence}.`, level: "info" };
|
|
540
|
+
}
|
|
541
|
+
case "update": {
|
|
542
|
+
const changed = [];
|
|
543
|
+
if (params.phase !== undefined) {
|
|
544
|
+
const phase = parsePlanningPhase(params.phase);
|
|
545
|
+
if (!phase) {
|
|
546
|
+
return { ok: false, message: "Invalid phase.", level: "error" };
|
|
547
|
+
}
|
|
548
|
+
state.phase = phase;
|
|
549
|
+
changed.push("phase");
|
|
550
|
+
}
|
|
551
|
+
if (params.root_issue_id !== undefined) {
|
|
552
|
+
if (typeof params.root_issue_id !== "string") {
|
|
553
|
+
return { ok: false, message: "root_issue_id must be a string.", level: "error" };
|
|
554
|
+
}
|
|
555
|
+
const normalized = normalizeMaybeClear(params.root_issue_id);
|
|
556
|
+
if (!normalized.ok) {
|
|
557
|
+
return { ok: false, message: "root_issue_id must not be empty.", level: "error" };
|
|
558
|
+
}
|
|
559
|
+
state.rootIssueId = normalized.value;
|
|
560
|
+
changed.push("root_issue_id");
|
|
561
|
+
}
|
|
562
|
+
if (params.waiting_on_user !== undefined) {
|
|
563
|
+
if (typeof params.waiting_on_user !== "boolean") {
|
|
564
|
+
return { ok: false, message: "waiting_on_user must be a boolean.", level: "error" };
|
|
565
|
+
}
|
|
566
|
+
state.waitingOnUser = params.waiting_on_user;
|
|
567
|
+
changed.push("waiting_on_user");
|
|
568
|
+
}
|
|
569
|
+
if (params.next_action !== undefined) {
|
|
570
|
+
if (typeof params.next_action !== "string") {
|
|
571
|
+
return { ok: false, message: "next_action must be a string.", level: "error" };
|
|
572
|
+
}
|
|
573
|
+
const normalized = normalizeMaybeClear(params.next_action);
|
|
574
|
+
if (!normalized.ok) {
|
|
575
|
+
return { ok: false, message: "next_action must not be empty.", level: "error" };
|
|
576
|
+
}
|
|
577
|
+
state.nextAction = normalized.value;
|
|
578
|
+
changed.push("next_action");
|
|
579
|
+
}
|
|
580
|
+
if (params.blocker !== undefined) {
|
|
581
|
+
if (typeof params.blocker !== "string") {
|
|
582
|
+
return { ok: false, message: "blocker must be a string.", level: "error" };
|
|
583
|
+
}
|
|
584
|
+
const normalized = normalizeMaybeClear(params.blocker);
|
|
585
|
+
if (!normalized.ok) {
|
|
586
|
+
return { ok: false, message: "blocker must not be empty.", level: "error" };
|
|
587
|
+
}
|
|
588
|
+
state.blocker = normalized.value;
|
|
589
|
+
changed.push("blocker");
|
|
590
|
+
}
|
|
591
|
+
if (params.confidence !== undefined) {
|
|
592
|
+
const confidence = parsePlanningConfidence(params.confidence);
|
|
593
|
+
if (!confidence) {
|
|
594
|
+
return { ok: false, message: "Invalid confidence.", level: "error" };
|
|
595
|
+
}
|
|
596
|
+
state.confidence = confidence;
|
|
597
|
+
changed.push("confidence");
|
|
598
|
+
}
|
|
599
|
+
if (params.steps !== undefined) {
|
|
600
|
+
const normalized = normalizeSteps(params.steps);
|
|
601
|
+
if (!normalized.ok) {
|
|
602
|
+
return { ok: false, message: normalized.error, level: "error" };
|
|
603
|
+
}
|
|
604
|
+
state.steps = normalized.labels.map((label) => ({ label, done: false }));
|
|
605
|
+
changed.push("steps");
|
|
606
|
+
}
|
|
607
|
+
if (params.step_updates !== undefined) {
|
|
608
|
+
const updated = applyStepUpdates(state, params.step_updates);
|
|
609
|
+
if (!updated.ok) {
|
|
610
|
+
return { ok: false, message: updated.error, level: "error" };
|
|
611
|
+
}
|
|
612
|
+
changed.push("step_updates");
|
|
613
|
+
}
|
|
614
|
+
if (changed.length === 0) {
|
|
615
|
+
return { ok: false, message: "No update fields provided.", level: "error" };
|
|
616
|
+
}
|
|
617
|
+
state.enabled = true;
|
|
618
|
+
return {
|
|
619
|
+
ok: true,
|
|
620
|
+
message: `Planning HUD updated (${changed.join(", ")}).`,
|
|
621
|
+
level: "info",
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
78
626
|
pi.on("session_start", async (_event, ctx) => {
|
|
79
627
|
refresh(ctx);
|
|
80
628
|
});
|
|
@@ -83,96 +631,184 @@ export function planningUiExtension(pi) {
|
|
|
83
631
|
});
|
|
84
632
|
registerMuSubcommand(pi, {
|
|
85
633
|
subcommand: "plan",
|
|
86
|
-
summary: "Planning HUD: phase + checklist
|
|
87
|
-
usage: "/mu plan on|off|toggle|status|phase|root|check|uncheck|toggle-step|
|
|
634
|
+
summary: "Planning HUD: phase + checklist + communication state for planning workflows",
|
|
635
|
+
usage: "/mu plan on|off|toggle|status|reset|snapshot|phase|root|check|uncheck|toggle-step|add-step|remove-step|relabel-step|waiting|next|blocker|confidence",
|
|
88
636
|
handler: async (args, ctx) => {
|
|
89
637
|
const tokens = args
|
|
90
638
|
.trim()
|
|
91
639
|
.split(/\s+/)
|
|
92
640
|
.filter((token) => token.length > 0);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ctx.ui.notify(`Planning HUD: ${state.enabled ? "enabled" : "disabled"}\nphase: ${state.phase}\nroot: ${root}\nsteps: ${done}/${state.steps.length}`, "info");
|
|
97
|
-
refresh(ctx);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
switch (tokens[0]) {
|
|
641
|
+
const command = tokens[0] ?? "status";
|
|
642
|
+
let params;
|
|
643
|
+
switch (command) {
|
|
101
644
|
case "on":
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ctx.ui.notify("Planning HUD enabled.", "info");
|
|
105
|
-
return;
|
|
645
|
+
params = { action: "on" };
|
|
646
|
+
break;
|
|
106
647
|
case "off":
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
ctx.ui.notify("Planning HUD disabled.", "info");
|
|
110
|
-
return;
|
|
648
|
+
params = { action: "off" };
|
|
649
|
+
break;
|
|
111
650
|
case "toggle":
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
651
|
+
params = { action: "toggle" };
|
|
652
|
+
break;
|
|
653
|
+
case "status":
|
|
654
|
+
params = { action: "status" };
|
|
655
|
+
break;
|
|
656
|
+
case "snapshot":
|
|
657
|
+
params = { action: "snapshot", snapshot_format: tokens[1] };
|
|
658
|
+
break;
|
|
116
659
|
case "reset":
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
state.phase = phase;
|
|
128
|
-
state.enabled = true;
|
|
129
|
-
refresh(ctx);
|
|
130
|
-
ctx.ui.notify(`Planning phase set to ${phase}.`, "info");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
case "root": {
|
|
134
|
-
const value = (tokens[1] ?? "").trim();
|
|
135
|
-
if (!value) {
|
|
136
|
-
notify(ctx, "Missing root issue id.", "error");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
state.rootIssueId = value.toLowerCase() === "clear" ? null : value;
|
|
140
|
-
state.enabled = true;
|
|
141
|
-
refresh(ctx);
|
|
142
|
-
ctx.ui.notify(`Planning root set to ${state.rootIssueId ?? "(unset)"}.`, "info");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
660
|
+
params = { action: "reset" };
|
|
661
|
+
break;
|
|
662
|
+
case "phase":
|
|
663
|
+
params = { action: "phase", phase: tokens[1] };
|
|
664
|
+
break;
|
|
665
|
+
case "root":
|
|
666
|
+
params = { action: "root", root_issue_id: tokens[1] };
|
|
667
|
+
break;
|
|
145
668
|
case "check":
|
|
669
|
+
params = { action: "check", step: Number.parseInt(tokens[1] ?? "", 10) };
|
|
670
|
+
break;
|
|
146
671
|
case "uncheck":
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
refresh(ctx);
|
|
170
|
-
ctx.ui.notify(`Planning step ${index + 1} updated.`, "info");
|
|
171
|
-
return;
|
|
672
|
+
params = { action: "uncheck", step: Number.parseInt(tokens[1] ?? "", 10) };
|
|
673
|
+
break;
|
|
674
|
+
case "toggle-step":
|
|
675
|
+
params = { action: "toggle_step", step: Number.parseInt(tokens[1] ?? "", 10) };
|
|
676
|
+
break;
|
|
677
|
+
case "add-step":
|
|
678
|
+
params = { action: "add_step", label: tokens.slice(1).join(" ") };
|
|
679
|
+
break;
|
|
680
|
+
case "remove-step":
|
|
681
|
+
params = { action: "remove_step", step: Number.parseInt(tokens[1] ?? "", 10) };
|
|
682
|
+
break;
|
|
683
|
+
case "relabel-step":
|
|
684
|
+
params = {
|
|
685
|
+
action: "set_step_label",
|
|
686
|
+
step: Number.parseInt(tokens[1] ?? "", 10),
|
|
687
|
+
label: tokens.slice(2).join(" "),
|
|
688
|
+
};
|
|
689
|
+
break;
|
|
690
|
+
case "waiting": {
|
|
691
|
+
const parsed = parseOnOff(tokens[1]);
|
|
692
|
+
params = { action: "set_waiting", waiting_on_user: parsed ?? undefined };
|
|
693
|
+
break;
|
|
172
694
|
}
|
|
695
|
+
case "next":
|
|
696
|
+
params = { action: "set_next", next_action: tokens.slice(1).join(" ") };
|
|
697
|
+
break;
|
|
698
|
+
case "blocker":
|
|
699
|
+
params = { action: "set_blocker", blocker: tokens.slice(1).join(" ") };
|
|
700
|
+
break;
|
|
701
|
+
case "confidence":
|
|
702
|
+
params = { action: "set_confidence", confidence: tokens[1] };
|
|
703
|
+
break;
|
|
173
704
|
default:
|
|
174
|
-
notify(ctx, `Unknown plan command: ${
|
|
705
|
+
notify(ctx, `Unknown plan command: ${command}`, "error");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const result = applyPlanningAction(params);
|
|
709
|
+
refresh(ctx);
|
|
710
|
+
if (!result.ok) {
|
|
711
|
+
notify(ctx, result.message, result.level ?? "error");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
ctx.ui.notify(result.message, result.level ?? "info");
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
pi.registerTool({
|
|
718
|
+
name: "mu_planning_hud",
|
|
719
|
+
label: "mu planning HUD",
|
|
720
|
+
description: "Control or inspect planning HUD state (phase, root issue, checklist, and communication metadata).",
|
|
721
|
+
parameters: {
|
|
722
|
+
type: "object",
|
|
723
|
+
properties: {
|
|
724
|
+
action: {
|
|
725
|
+
type: "string",
|
|
726
|
+
enum: [
|
|
727
|
+
"status",
|
|
728
|
+
"on",
|
|
729
|
+
"off",
|
|
730
|
+
"toggle",
|
|
731
|
+
"reset",
|
|
732
|
+
"phase",
|
|
733
|
+
"root",
|
|
734
|
+
"check",
|
|
735
|
+
"uncheck",
|
|
736
|
+
"toggle_step",
|
|
737
|
+
"set_steps",
|
|
738
|
+
"add_step",
|
|
739
|
+
"remove_step",
|
|
740
|
+
"set_step_label",
|
|
741
|
+
"set_waiting",
|
|
742
|
+
"set_next",
|
|
743
|
+
"set_blocker",
|
|
744
|
+
"set_confidence",
|
|
745
|
+
"update",
|
|
746
|
+
"snapshot",
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
phase: {
|
|
750
|
+
type: "string",
|
|
751
|
+
enum: [
|
|
752
|
+
"investigating",
|
|
753
|
+
"drafting",
|
|
754
|
+
"reviewing",
|
|
755
|
+
"waiting_user",
|
|
756
|
+
"blocked",
|
|
757
|
+
"executing",
|
|
758
|
+
"approved",
|
|
759
|
+
"done",
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
root_issue_id: { type: "string" },
|
|
763
|
+
step: { type: "integer", minimum: 1 },
|
|
764
|
+
label: { type: "string" },
|
|
765
|
+
waiting_on_user: { type: "boolean" },
|
|
766
|
+
next_action: { type: "string" },
|
|
767
|
+
blocker: { type: "string" },
|
|
768
|
+
confidence: {
|
|
769
|
+
type: "string",
|
|
770
|
+
enum: ["low", "medium", "high"],
|
|
771
|
+
},
|
|
772
|
+
steps: {
|
|
773
|
+
type: "array",
|
|
774
|
+
items: { type: "string" },
|
|
775
|
+
},
|
|
776
|
+
step_updates: {
|
|
777
|
+
type: "array",
|
|
778
|
+
items: {
|
|
779
|
+
type: "object",
|
|
780
|
+
properties: {
|
|
781
|
+
index: { type: "integer", minimum: 1 },
|
|
782
|
+
done: { type: "boolean" },
|
|
783
|
+
label: { type: "string" },
|
|
784
|
+
},
|
|
785
|
+
required: ["index"],
|
|
786
|
+
additionalProperties: false,
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
snapshot_format: {
|
|
790
|
+
type: "string",
|
|
791
|
+
enum: ["compact", "multiline"],
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
required: ["action"],
|
|
795
|
+
additionalProperties: false,
|
|
796
|
+
},
|
|
797
|
+
execute: async (_toolCallId, paramsRaw, _signal, _onUpdate, ctx) => {
|
|
798
|
+
const params = paramsRaw;
|
|
799
|
+
const result = applyPlanningAction(params);
|
|
800
|
+
refresh(ctx);
|
|
801
|
+
if (!result.ok) {
|
|
802
|
+
return planningToolError(result.message);
|
|
175
803
|
}
|
|
804
|
+
return {
|
|
805
|
+
content: [{ type: "text", text: `${result.message}\n\n${planningStatusSummary(state)}` }],
|
|
806
|
+
details: {
|
|
807
|
+
ok: true,
|
|
808
|
+
action: params.action,
|
|
809
|
+
...planningDetails(state),
|
|
810
|
+
},
|
|
811
|
+
};
|
|
176
812
|
},
|
|
177
813
|
});
|
|
178
814
|
}
|