@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.
@@ -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 rootSuffix = state.rootIssueId ? ` root:${state.rootIssueId}` : "";
41
- ctx.ui.setStatus("mu-planning", ctx.ui.theme.fg("dim", `planning ${done}/${total} · ${phase}${rootSuffix}`));
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", `Planning (${phase})`),
44
- state.rootIssueId
45
- ? ctx.ui.theme.fg("dim", ` root issue: ${state.rootIssueId}`)
46
- : ctx.ui.theme.fg("dim", " root issue: (unset)"),
47
- ...state.steps.map((step, index) => {
48
- const mark = step.done ? ctx.ui.theme.fg("success", "") : ctx.ui.theme.fg("muted", "☐");
49
- return `${mark} ${index + 1}. ${step.label}`;
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 parsePlanningPhase(raw) {
64
- const value = raw.trim().toLowerCase();
65
- if (value === "investigating" || value === "drafting" || value === "reviewing" || value === "approved") {
66
- return value;
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 widget for planning workflows",
87
- usage: "/mu plan on|off|toggle|status|phase|root|check|uncheck|toggle-step|reset",
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
- if (tokens.length === 0 || tokens[0] === "status") {
94
- const done = state.steps.filter((step) => step.done).length;
95
- const root = state.rootIssueId ?? "(unset)";
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
- state.enabled = true;
103
- refresh(ctx);
104
- ctx.ui.notify("Planning HUD enabled.", "info");
105
- return;
645
+ params = { action: "on" };
646
+ break;
106
647
  case "off":
107
- state.enabled = false;
108
- refresh(ctx);
109
- ctx.ui.notify("Planning HUD disabled.", "info");
110
- return;
648
+ params = { action: "off" };
649
+ break;
111
650
  case "toggle":
112
- state.enabled = !state.enabled;
113
- refresh(ctx);
114
- ctx.ui.notify(`Planning HUD ${state.enabled ? "enabled" : "disabled"}.`, "info");
115
- return;
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
- state = createDefaultState();
118
- refresh(ctx);
119
- ctx.ui.notify("Planning HUD state reset.", "info");
120
- return;
121
- case "phase": {
122
- const phase = parsePlanningPhase(tokens[1] ?? "");
123
- if (!phase) {
124
- notify(ctx, "Invalid phase.", "error");
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
- case "toggle-step": {
148
- const indexRaw = tokens[1] ?? "";
149
- const parsed = Number.parseInt(indexRaw, 10);
150
- if (!Number.isFinite(parsed)) {
151
- notify(ctx, "Step index must be a number.", "error");
152
- return;
153
- }
154
- if (parsed < 1 || parsed > state.steps.length) {
155
- notify(ctx, `Step index out of range (1-${state.steps.length}).`, "error");
156
- return;
157
- }
158
- const index = parsed - 1;
159
- if (tokens[0] === "check") {
160
- state.steps[index].done = true;
161
- }
162
- else if (tokens[0] === "uncheck") {
163
- state.steps[index].done = false;
164
- }
165
- else {
166
- state.steps[index].done = !state.steps[index].done;
167
- }
168
- state.enabled = true;
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: ${tokens[0]}`, "error");
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
  }