@femtomc/mu-agent 26.2.55 → 26.2.57

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 CHANGED
@@ -54,16 +54,16 @@ Current stack:
54
54
 
55
55
  ## Slash commands (operator-facing)
56
56
 
57
- - `/mu-status` — concise server status
58
- - `/mu-control` — active control-plane adapters and webhook routes
59
- - `/mu-setup` — adapter preflight
60
- - `/mu-setup plan <adapter>` — actionable wiring plan
61
- - `/mu-setup apply <adapter>` — guided config apply + control-plane reload
62
- - `/mu-setup verify [adapter]` — runtime verification for mounted routes
63
- - `/mu-setup <adapter>` — sends adapter setup brief to mu agent (`--no-agent` prints local guide)
64
- - `/mu-events [n]` / `/mu-events tail [n]` — event log tail
65
- - `/mu-events watch on|off` — toggle event watch widget
66
- - `/mu-brand on|off|toggle` — enable/disable UI branding
57
+ - `/mu status` — concise server status
58
+ - `/mu control` — active control-plane adapters and webhook routes
59
+ - `/mu setup` — adapter preflight
60
+ - `/mu setup plan <adapter>` — actionable wiring plan
61
+ - `/mu setup apply <adapter>` — guided config apply + control-plane reload
62
+ - `/mu setup verify [adapter]` — runtime verification for mounted routes
63
+ - `/mu setup <adapter>` — sends adapter setup brief to mu agent (`--no-agent` prints local guide)
64
+ - `/mu events [n]` / `/mu events tail [n]` — event log tail
65
+ - `/mu events watch on|off` — toggle event watch widget
66
+ - `/mu brand on|off|toggle` — enable/disable UI branding
67
67
 
68
68
  ## Tools (agent/operator-facing)
69
69
 
@@ -1 +1 @@
1
- {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA+CpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QA4NjD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/extensions/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAgDpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QA8NjD;AAED,eAAe,iBAAiB,CAAC"}
@@ -8,6 +8,7 @@
8
8
  * - Lightweight periodic status refresh (open/ready/control-plane)
9
9
  */
10
10
  import { basename } from "node:path";
11
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
11
12
  import { fetchMuStatus, muServerUrl } from "./shared.js";
12
13
  const EMPTY_SNAPSHOT = {
13
14
  openCount: 0,
@@ -61,7 +62,7 @@ export function brandingExtension(pi) {
61
62
  : snapshot.controlPlaneActive
62
63
  ? theme.fg("success", `cp ${snapshot.adapters.join(",") || "on"}`)
63
64
  : theme.fg("muted", "cp off");
64
- const line1 = `${theme.fg("accent", "μ")}${theme.fg("dim", " quick actions")}: ${theme.fg("muted", "/mu-status /mu-control /mu-setup /mu-events")}`;
65
+ const line1 = `${theme.fg("accent", "μ")}${theme.fg("dim", " quick actions")}: ${theme.fg("muted", "/mu status /mu control /mu setup /mu events")}`;
65
66
  const line2 = `${theme.fg("dim", `open ${snapshot.openCount} · ready ${snapshot.readyCount}`)} · ${cpState}`;
66
67
  return [truncateToWidth(line1, width), truncateToWidth(line2, width)];
67
68
  },
@@ -220,8 +221,10 @@ export function brandingExtension(pi) {
220
221
  footerRequestRender = null;
221
222
  activeCtx = null;
222
223
  });
223
- pi.registerCommand("mu-brand", {
224
- description: "Toggle mu TUI branding (`/mu-brand on|off|toggle`)",
224
+ registerMuSubcommand(pi, {
225
+ subcommand: "brand",
226
+ summary: "Toggle mu TUI branding",
227
+ usage: "/mu brand [on|off|toggle]",
225
228
  handler: async (args, ctx) => {
226
229
  const mode = args.trim().toLowerCase();
227
230
  if (mode === "on") {
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function cronExtension(pi: ExtensionAPI): void;
3
+ export default cronExtension;
4
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../src/extensions/cron.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAiBlE,wBAAgB,aAAa,CAAC,EAAE,EAAE,YAAY,QAoK7C;AAED,eAAe,aAAa,CAAC"}
@@ -0,0 +1,187 @@
1
+ import { StringEnum } from "@mariozechner/pi-ai";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { clampInt, fetchMuJson, textResult, toJsonText } from "./shared.js";
4
+ function trimOrNull(value) {
5
+ if (value == null)
6
+ return null;
7
+ const trimmed = value.trim();
8
+ return trimmed.length > 0 ? trimmed : null;
9
+ }
10
+ function normalizedNumber(value) {
11
+ if (value == null || !Number.isFinite(value)) {
12
+ return undefined;
13
+ }
14
+ return Math.trunc(value);
15
+ }
16
+ export function cronExtension(pi) {
17
+ const Params = Type.Object({
18
+ action: StringEnum(["status", "list", "get", "create", "update", "delete", "trigger", "enable", "disable"]),
19
+ program_id: Type.Optional(Type.String({ description: "Cron program ID" })),
20
+ title: Type.Optional(Type.String({ description: "Program title" })),
21
+ target_kind: Type.Optional(Type.String({ description: "Target kind (run|activity)" })),
22
+ run_job_id: Type.Optional(Type.String({ description: "Run job ID target" })),
23
+ run_root_issue_id: Type.Optional(Type.String({ description: "Run root issue ID target" })),
24
+ activity_id: Type.Optional(Type.String({ description: "Activity ID target" })),
25
+ schedule_kind: Type.Optional(Type.String({ description: "Schedule kind (at|every|cron)" })),
26
+ at_ms: Type.Optional(Type.Number({ description: "One-shot timestamp in epoch ms" })),
27
+ at: Type.Optional(Type.String({ description: "One-shot timestamp (ISO-8601)" })),
28
+ every_ms: Type.Optional(Type.Number({ description: "Fixed interval in ms" })),
29
+ anchor_ms: Type.Optional(Type.Number({ description: "Anchor timestamp for every schedules" })),
30
+ expr: Type.Optional(Type.String({ description: "Cron expression (5-field)" })),
31
+ tz: Type.Optional(Type.String({ description: "Optional IANA timezone for cron expressions" })),
32
+ reason: Type.Optional(Type.String({ description: "Execution reason" })),
33
+ enabled: Type.Optional(Type.Boolean({ description: "Enabled state" })),
34
+ schedule_filter: Type.Optional(Type.String({ description: "Filter list by schedule kind" })),
35
+ limit: Type.Optional(Type.Number({ description: "Max returned items for list" })),
36
+ });
37
+ pi.registerTool({
38
+ name: "mu_cron",
39
+ label: "Cron",
40
+ description: "Manage persistent cron programs. Actions: status, list, get, create, update, delete, trigger, enable, disable.",
41
+ parameters: Params,
42
+ async execute(_toolCallId, params) {
43
+ switch (params.action) {
44
+ case "status": {
45
+ const payload = await fetchMuJson("/api/cron/status");
46
+ return textResult(toJsonText(payload), { action: "status" });
47
+ }
48
+ case "list": {
49
+ const query = new URLSearchParams();
50
+ const targetKind = trimOrNull(params.target_kind);
51
+ if (targetKind) {
52
+ query.set("target_kind", targetKind);
53
+ }
54
+ if (typeof params.enabled === "boolean") {
55
+ query.set("enabled", params.enabled ? "true" : "false");
56
+ }
57
+ const scheduleFilter = trimOrNull(params.schedule_filter) ?? trimOrNull(params.schedule_kind);
58
+ if (scheduleFilter) {
59
+ query.set("schedule_kind", scheduleFilter);
60
+ }
61
+ query.set("limit", String(clampInt(params.limit, 50, 1, 500)));
62
+ const payload = await fetchMuJson(`/api/cron?${query.toString()}`);
63
+ return textResult(toJsonText(payload), {
64
+ action: "list",
65
+ targetKind,
66
+ enabled: params.enabled,
67
+ scheduleFilter,
68
+ });
69
+ }
70
+ case "get": {
71
+ const programId = trimOrNull(params.program_id);
72
+ if (!programId)
73
+ return textResult("get requires program_id");
74
+ const payload = await fetchMuJson(`/api/cron/${encodeURIComponent(programId)}`);
75
+ return textResult(toJsonText(payload), { action: "get", programId });
76
+ }
77
+ case "create": {
78
+ const title = trimOrNull(params.title);
79
+ const targetKind = trimOrNull(params.target_kind);
80
+ const scheduleKind = trimOrNull(params.schedule_kind);
81
+ if (!title)
82
+ return textResult("create requires title");
83
+ if (!targetKind)
84
+ return textResult("create requires target_kind (run|activity)");
85
+ if (!scheduleKind)
86
+ return textResult("create requires schedule_kind (at|every|cron)");
87
+ const payload = await fetchMuJson("/api/cron/create", {
88
+ method: "POST",
89
+ body: {
90
+ title,
91
+ target_kind: targetKind,
92
+ run_job_id: trimOrNull(params.run_job_id),
93
+ run_root_issue_id: trimOrNull(params.run_root_issue_id),
94
+ activity_id: trimOrNull(params.activity_id),
95
+ schedule_kind: scheduleKind,
96
+ at_ms: normalizedNumber(params.at_ms),
97
+ at: trimOrNull(params.at),
98
+ every_ms: normalizedNumber(params.every_ms),
99
+ anchor_ms: normalizedNumber(params.anchor_ms),
100
+ expr: trimOrNull(params.expr),
101
+ tz: trimOrNull(params.tz),
102
+ reason: trimOrNull(params.reason),
103
+ enabled: typeof params.enabled === "boolean" ? params.enabled : undefined,
104
+ },
105
+ });
106
+ return textResult(toJsonText(payload), {
107
+ action: "create",
108
+ title,
109
+ targetKind,
110
+ scheduleKind,
111
+ });
112
+ }
113
+ case "update": {
114
+ const programId = trimOrNull(params.program_id);
115
+ if (!programId)
116
+ return textResult("update requires program_id");
117
+ const payload = await fetchMuJson("/api/cron/update", {
118
+ method: "POST",
119
+ body: {
120
+ program_id: programId,
121
+ title: trimOrNull(params.title),
122
+ target_kind: trimOrNull(params.target_kind),
123
+ run_job_id: trimOrNull(params.run_job_id),
124
+ run_root_issue_id: trimOrNull(params.run_root_issue_id),
125
+ activity_id: trimOrNull(params.activity_id),
126
+ schedule_kind: trimOrNull(params.schedule_kind),
127
+ at_ms: normalizedNumber(params.at_ms),
128
+ at: trimOrNull(params.at),
129
+ every_ms: normalizedNumber(params.every_ms),
130
+ anchor_ms: normalizedNumber(params.anchor_ms),
131
+ expr: trimOrNull(params.expr),
132
+ tz: trimOrNull(params.tz),
133
+ reason: trimOrNull(params.reason),
134
+ enabled: typeof params.enabled === "boolean" ? params.enabled : undefined,
135
+ },
136
+ });
137
+ return textResult(toJsonText(payload), { action: "update", programId });
138
+ }
139
+ case "delete": {
140
+ const programId = trimOrNull(params.program_id);
141
+ if (!programId)
142
+ return textResult("delete requires program_id");
143
+ const payload = await fetchMuJson("/api/cron/delete", {
144
+ method: "POST",
145
+ body: {
146
+ program_id: programId,
147
+ },
148
+ });
149
+ return textResult(toJsonText(payload), { action: "delete", programId });
150
+ }
151
+ case "trigger": {
152
+ const programId = trimOrNull(params.program_id);
153
+ if (!programId)
154
+ return textResult("trigger requires program_id");
155
+ const payload = await fetchMuJson("/api/cron/trigger", {
156
+ method: "POST",
157
+ body: {
158
+ program_id: programId,
159
+ reason: trimOrNull(params.reason),
160
+ },
161
+ });
162
+ return textResult(toJsonText(payload), { action: "trigger", programId });
163
+ }
164
+ case "enable":
165
+ case "disable": {
166
+ const programId = trimOrNull(params.program_id);
167
+ if (!programId)
168
+ return textResult(`${params.action} requires program_id`);
169
+ const payload = await fetchMuJson("/api/cron/update", {
170
+ method: "POST",
171
+ body: {
172
+ program_id: programId,
173
+ enabled: params.action === "enable",
174
+ },
175
+ });
176
+ return textResult(toJsonText(payload), {
177
+ action: params.action,
178
+ programId,
179
+ });
180
+ }
181
+ default:
182
+ return textResult(`unknown action: ${params.action}`);
183
+ }
184
+ },
185
+ });
186
+ }
187
+ export default cronExtension;
@@ -2,8 +2,8 @@
2
2
  * mu-event-log — Event stream helper for mu serve.
3
3
  *
4
4
  * - Status line with last event type and tail count
5
- * - Optional watch widget below editor (`/mu-events watch on|off`)
6
- * - Command for quick tail inspection (`/mu-events [n]` or `/mu-events tail [n]`)
5
+ * - Optional watch widget below editor (`/mu events watch on|off`)
6
+ * - Command for quick tail inspection (`/mu events [n]` or `/mu events tail [n]`)
7
7
  */
8
8
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
9
  export declare function eventLogExtension(pi: ExtensionAPI): void;
@@ -1 +1 @@
1
- {"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../../src/extensions/event-log.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAgCpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAgHjD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../../src/extensions/event-log.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAiCpF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAkHjD;AAED,eAAe,iBAAiB,CAAC"}
@@ -2,9 +2,10 @@
2
2
  * mu-event-log — Event stream helper for mu serve.
3
3
  *
4
4
  * - Status line with last event type and tail count
5
- * - Optional watch widget below editor (`/mu-events watch on|off`)
6
- * - Command for quick tail inspection (`/mu-events [n]` or `/mu-events tail [n]`)
5
+ * - Optional watch widget below editor (`/mu events watch on|off`)
6
+ * - Command for quick tail inspection (`/mu events [n]` or `/mu events tail [n]`)
7
7
  */
8
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
8
9
  import { clampInt, fetchMuJson, muServerUrl } from "./shared.js";
9
10
  function eventTime(tsMs) {
10
11
  return new Date(tsMs).toLocaleTimeString();
@@ -96,8 +97,10 @@ export function eventLogExtension(pi) {
96
97
  stopPolling();
97
98
  activeCtx = null;
98
99
  });
99
- pi.registerCommand("mu-events", {
100
- description: "Inspect events (`/mu-events [n]`, `/mu-events tail [n]`, `/mu-events watch on|off`)",
100
+ registerMuSubcommand(pi, {
101
+ subcommand: "events",
102
+ summary: "Inspect event tails and toggle the watch widget",
103
+ usage: "/mu events [n] | /mu events tail [n] | /mu events watch on|off",
101
104
  handler: async (args, ctx) => {
102
105
  const tokens = args
103
106
  .trim()
@@ -1,5 +1,6 @@
1
1
  export { activitiesExtension } from "./activities.js";
2
2
  export { brandingExtension } from "./branding.js";
3
+ export { cronExtension } from "./cron.js";
3
4
  export { eventLogExtension } from "./event-log.js";
4
5
  export { heartbeatsExtension } from "./heartbeats.js";
5
6
  export { messagingSetupExtension } from "./messaging-setup.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,kCAAkC,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AA0B1E;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAE/B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,UAElC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,kCAAkC,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AA2B1E;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAE/B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,UAElC,CAAC"}
@@ -1,5 +1,6 @@
1
1
  export { activitiesExtension } from "./activities.js";
2
2
  export { brandingExtension } from "./branding.js";
3
+ export { cronExtension } from "./cron.js";
3
4
  export { eventLogExtension } from "./event-log.js";
4
5
  export { heartbeatsExtension } from "./heartbeats.js";
5
6
  export { messagingSetupExtension } from "./messaging-setup.js";
@@ -16,6 +17,7 @@ const SERVE_EXTENSION_MODULE_BASENAMES = [
16
17
  "orchestration-runs",
17
18
  "activities",
18
19
  "heartbeats",
20
+ "cron",
19
21
  ];
20
22
  const OPERATOR_EXTENSION_MODULE_BASENAMES = [
21
23
  "branding",
@@ -2,7 +2,7 @@
2
2
  * mu-messaging-setup — Adapter configuration diagnostics + guided setup.
3
3
  *
4
4
  * Goals:
5
- * - Make `/mu-setup <adapter>` hand setup context to the active mu agent.
5
+ * - Make `/mu setup <adapter>` hand setup context to the active mu agent.
6
6
  * - Keep configuration in `.mu/config.json` (no process.env mutations).
7
7
  * - Support plan/apply/verify workflow with in-process control-plane reload.
8
8
  */
@@ -1 +1 @@
1
- {"version":3,"file":"messaging-setup.d.ts","sourceRoot":"","sources":["../../src/extensions/messaging-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAA6C,MAAM,+BAA+B,CAAC;AAyiC7G,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,YAAY,QA0PvD;AAED,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"messaging-setup.d.ts","sourceRoot":"","sources":["../../src/extensions/messaging-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAA6C,MAAM,+BAA+B,CAAC;AAypC7G,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,YAAY,QAyPvD;AAED,eAAe,uBAAuB,CAAC"}
@@ -2,13 +2,14 @@
2
2
  * mu-messaging-setup — Adapter configuration diagnostics + guided setup.
3
3
  *
4
4
  * Goals:
5
- * - Make `/mu-setup <adapter>` hand setup context to the active mu agent.
5
+ * - Make `/mu setup <adapter>` hand setup context to the active mu agent.
6
6
  * - Keep configuration in `.mu/config.json` (no process.env mutations).
7
7
  * - Support plan/apply/verify workflow with in-process control-plane reload.
8
8
  */
9
9
  import { StringEnum } from "@mariozechner/pi-ai";
10
10
  import { Type } from "@sinclair/typebox";
11
11
  import { loadBundledPrompt } from "../default_prompts.js";
12
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
12
13
  import { fetchMuJson, fetchMuStatus, muServerUrl, textResult, toJsonText } from "./shared.js";
13
14
  const MESSAGING_SETUP_BRIEF_TEMPLATE = loadBundledPrompt("skills/messaging-setup-brief.md");
14
15
  function interpolateTemplate(template, vars) {
@@ -31,7 +32,7 @@ const ADAPTERS = [
31
32
  "Copy Signing Secret into .mu/config.json → control_plane.adapters.slack.signing_secret.",
32
33
  "Create a Slash Command (e.g. /mu) with Request URL <public-base-url>/webhooks/slack.",
33
34
  "Install/reinstall app after command changes.",
34
- "Run /mu in Slack, then /mu-setup verify slack.",
35
+ "Run /mu in Slack, then /mu setup verify slack.",
35
36
  ],
36
37
  },
37
38
  {
@@ -49,7 +50,7 @@ const ADAPTERS = [
49
50
  "Create/open app in Discord Developer Portal.",
50
51
  "Copy Interaction Public Key into .mu/config.json → control_plane.adapters.discord.signing_secret.",
51
52
  "Set Interactions Endpoint URL to <public-base-url>/webhooks/discord.",
52
- "Run a Discord command interaction, then /mu-setup verify discord.",
53
+ "Run a Discord command interaction, then /mu setup verify discord.",
53
54
  ],
54
55
  },
55
56
  {
@@ -79,7 +80,7 @@ const ADAPTERS = [
79
80
  "Call Telegram setWebhook using URL <public-base-url>/webhooks/telegram and matching secret_token.",
80
81
  "Link your Telegram identity to control-plane policy (mu control link --channel telegram --actor-id <telegram-user-id> --tenant-id telegram-bot --role <viewer|contributor|operator>).",
81
82
  "Optionally set control_plane.adapters.telegram.bot_username.",
82
- "Send /mu in Telegram chat, then /mu-setup verify telegram.",
83
+ "Send /mu in Telegram chat, then /mu setup verify telegram.",
83
84
  ],
84
85
  },
85
86
  {
@@ -251,7 +252,7 @@ function nextStepForState(opts) {
251
252
  case "active":
252
253
  return "No action needed. Adapter is mounted and receiving webhooks.";
253
254
  case "configured_not_active":
254
- return "Run `/mu-setup apply <adapter>` to trigger in-process control-plane reload.";
255
+ return "Run `/mu setup apply <adapter>` to trigger in-process control-plane reload.";
255
256
  case "missing_config":
256
257
  return `Set required config fields: ${opts.missing.join(", ")}.`;
257
258
  case "planned":
@@ -383,7 +384,7 @@ function setupGuide(checks, adapterId) {
383
384
  return [
384
385
  "# Messaging Integration Setup",
385
386
  "",
386
- "Use `/mu-setup <adapter>` to hand setup context to mu agent.",
387
+ "Use `/mu setup <adapter>` to hand setup context to mu agent.",
387
388
  "Config source of truth is `.mu/config.json`.",
388
389
  "",
389
390
  ...sections,
@@ -402,15 +403,15 @@ function buildPlan(check, publicBaseUrl) {
402
403
  else {
403
404
  if (check.missing.length > 0) {
404
405
  steps.push(`Set required config fields: ${check.missing.join(", ")}.`);
405
- steps.push(`Run /mu-setup apply ${check.id} to write config and reload control-plane.`);
406
+ steps.push(`Run /mu setup apply ${check.id} to write config and reload control-plane.`);
406
407
  }
407
408
  if (check.state === "configured_not_active") {
408
- steps.push(`Run /mu-setup apply ${check.id} to trigger control-plane reload.`);
409
+ steps.push(`Run /mu setup apply ${check.id} to trigger control-plane reload.`);
409
410
  }
410
411
  if (webhookUrl) {
411
412
  steps.push(`Configure provider webhook/inbound URL to: ${webhookUrl}`);
412
413
  }
413
- steps.push(`Run verification: /mu-setup verify ${check.id}${normalizedBase ? ` --public-base-url ${normalizedBase}` : ""}`);
414
+ steps.push(`Run verification: /mu setup verify ${check.id}${normalizedBase ? ` --public-base-url ${normalizedBase}` : ""}`);
414
415
  }
415
416
  return {
416
417
  id: check.id,
@@ -423,8 +424,8 @@ function buildPlan(check, publicBaseUrl) {
423
424
  missing_required_fields: check.missing,
424
425
  steps,
425
426
  commands: {
426
- apply: `/mu-setup apply ${check.id}`,
427
- verify: `/mu-setup verify ${check.id}`,
427
+ apply: `/mu setup apply ${check.id}`,
428
+ verify: `/mu setup verify ${check.id}`,
428
429
  },
429
430
  };
430
431
  }
@@ -452,6 +453,62 @@ function planText(plan) {
452
453
  function planSummary(plans) {
453
454
  return plans.map((plan) => planText(plan)).join("\n\n");
454
455
  }
456
+ function isRecord(value) {
457
+ return typeof value === "object" && value !== null;
458
+ }
459
+ function isControlPlaneGenerationIdentity(value) {
460
+ if (!isRecord(value))
461
+ return false;
462
+ return typeof value.generation_id === "string" && typeof value.generation_seq === "number";
463
+ }
464
+ function isControlPlaneReloadGenerationSummary(value) {
465
+ if (!isRecord(value))
466
+ return false;
467
+ if (typeof value.attempt_id !== "string")
468
+ return false;
469
+ if (typeof value.coalesced !== "boolean")
470
+ return false;
471
+ if (value.from_generation !== null && !isControlPlaneGenerationIdentity(value.from_generation))
472
+ return false;
473
+ if (!isControlPlaneGenerationIdentity(value.to_generation))
474
+ return false;
475
+ if (value.active_generation !== null && !isControlPlaneGenerationIdentity(value.active_generation))
476
+ return false;
477
+ return value.outcome === "success" || value.outcome === "failure";
478
+ }
479
+ function parseControlPlaneReloadApiResponse(raw) {
480
+ let parsed;
481
+ try {
482
+ parsed = JSON.parse(raw);
483
+ }
484
+ catch {
485
+ return {
486
+ response: null,
487
+ error: "control-plane reload returned invalid JSON response",
488
+ };
489
+ }
490
+ if (!isRecord(parsed)) {
491
+ return {
492
+ response: null,
493
+ error: "control-plane reload returned non-object payload",
494
+ };
495
+ }
496
+ if (!isControlPlaneReloadGenerationSummary(parsed.generation)) {
497
+ return {
498
+ response: null,
499
+ error: "control-plane reload response missing generation metadata (expected generation-scoped contract)",
500
+ };
501
+ }
502
+ const parsedRecord = parsed;
503
+ const response = {
504
+ ...parsed,
505
+ telegram_generation: parsedRecord.telegram_generation ?? null,
506
+ };
507
+ return {
508
+ response,
509
+ error: null,
510
+ };
511
+ }
455
512
  async function reloadControlPlaneInProcess(reason) {
456
513
  const base = muServerUrl();
457
514
  if (!base) {
@@ -468,18 +525,27 @@ async function reloadControlPlaneInProcess(reason) {
468
525
  body: JSON.stringify({ reason }),
469
526
  });
470
527
  const raw = await response.text();
471
- let parsed = null;
472
- try {
473
- parsed = JSON.parse(raw);
528
+ const parsedResult = parseControlPlaneReloadApiResponse(raw);
529
+ const parsed = parsedResult.response;
530
+ if (parsedResult.error) {
531
+ return {
532
+ ok: false,
533
+ response: null,
534
+ error: parsedResult.error,
535
+ };
474
536
  }
475
- catch {
476
- parsed = null;
537
+ if (!parsed) {
538
+ return {
539
+ ok: false,
540
+ response: null,
541
+ error: "control-plane reload response missing payload",
542
+ };
477
543
  }
478
- if (!response.ok || !parsed?.ok) {
544
+ if (!response.ok || !parsed.ok) {
479
545
  return {
480
546
  ok: false,
481
547
  response: parsed,
482
- error: parsed?.error ?? `control-plane reload failed (${response.status})`,
548
+ error: parsed.error ?? `control-plane reload failed (${response.status})`,
483
549
  };
484
550
  }
485
551
  return {
@@ -496,6 +562,22 @@ async function reloadControlPlaneInProcess(reason) {
496
562
  };
497
563
  }
498
564
  }
565
+ function reloadOutcomeSummary(reload) {
566
+ if (!reload.ok) {
567
+ return `Control-plane reload failed: ${reload.error ?? "unknown error"}.`;
568
+ }
569
+ const response = reload.response;
570
+ if (!response) {
571
+ return "Control-plane reload failed: missing reload response payload.";
572
+ }
573
+ const adapters = response.control_plane?.adapters.join(", ") || "(none)";
574
+ const generationSummary = `${response.generation.outcome} (${response.generation.active_generation?.generation_id ?? response.generation.to_generation.generation_id})`;
575
+ const telegramRollbackTrigger = response.telegram_generation?.rollback.trigger;
576
+ const telegramNote = response.telegram_generation?.handled && telegramRollbackTrigger
577
+ ? ` rollback_trigger=${telegramRollbackTrigger}`
578
+ : "";
579
+ return `Control-plane reloaded in-process. Active adapters: ${adapters}. Generation: ${generationSummary}.${telegramNote}`;
580
+ }
499
581
  function patchForAdapterValues(adapterId, values) {
500
582
  switch (adapterId) {
501
583
  case "slack":
@@ -621,7 +703,7 @@ function verifyText(result) {
621
703
  lines.push(` next: ${check.next_step}`);
622
704
  }
623
705
  if (!result.ok) {
624
- lines.push("", "Tip: run `/mu-setup plan <adapter>` for exact remediation steps.");
706
+ lines.push("", "Tip: run `/mu setup plan <adapter>` for exact remediation steps.");
625
707
  }
626
708
  return lines.join("\n");
627
709
  }
@@ -772,7 +854,7 @@ function buildAgentSetupPrompt(opts) {
772
854
  missing_fields: opts.check.missing.join(", ") || "(none)",
773
855
  provider_steps: adapter.providerSetupSteps.map((step, index) => `${index + 1}. ${step}`).join("\n"),
774
856
  field_status: adapterFieldStatusLines(adapter, opts.check).join("\n"),
775
- verify_command: `/mu-setup verify ${adapter.id}${verifyFlag}`,
857
+ verify_command: `/mu setup verify ${adapter.id}${verifyFlag}`,
776
858
  });
777
859
  }
778
860
  function dispatchSetupPromptToAgent(pi, ctx, prompt) {
@@ -846,9 +928,7 @@ async function runInteractiveApply(ctx, adapterId) {
846
928
  const lines = [
847
929
  `Updated config fields: ${outcome.updated_fields.join(", ") || "(none)"}`,
848
930
  `Config path: ${outcome.config_path ?? runtime.configPath ?? "(unknown)"}`,
849
- outcome.reload.ok
850
- ? `Control-plane reloaded in-process. Active adapters: ${outcome.reload.response?.control_plane?.adapters.join(", ") || "(none)"}.`
851
- : `Control-plane reload failed: ${outcome.reload.error ?? "unknown error"}.`,
931
+ reloadOutcomeSummary(outcome.reload),
852
932
  "",
853
933
  verifyText(verify),
854
934
  ];
@@ -944,7 +1024,7 @@ export function messagingSetupExtension(pi) {
944
1024
  const overrides = params.fields ?? {};
945
1025
  const stillMissing = check.missing.filter((field) => !(field in overrides));
946
1026
  if (stillMissing.length > 0) {
947
- return textResult(`Cannot apply ${adapterId}: missing required config fields (${stillMissing.join(", ")}). Pass them via the fields parameter or use /mu-setup apply ${adapterId} for guided input.`, { adapter: adapterId, missing_required_fields: stillMissing });
1027
+ return textResult(`Cannot apply ${adapterId}: missing required config fields (${stillMissing.join(", ")}). Pass them via the fields parameter or use /mu setup apply ${adapterId} for guided input.`, { adapter: adapterId, missing_required_fields: stillMissing });
948
1028
  }
949
1029
  const outcome = await applyAdapterConfig({
950
1030
  adapterId,
@@ -959,9 +1039,7 @@ export function messagingSetupExtension(pi) {
959
1039
  const lines = [
960
1040
  `Updated config fields: ${outcome.updated_fields.join(", ") || "(none)"}`,
961
1041
  `Config path: ${outcome.config_path ?? runtime.configPath ?? "(unknown)"}`,
962
- outcome.reload.ok
963
- ? `Control-plane reloaded in-process. Active adapters: ${outcome.reload.response?.control_plane?.adapters.join(", ") || "(none)"}.`
964
- : `Control-plane reload failed: ${outcome.reload.error ?? "unknown error"}.`,
1042
+ reloadOutcomeSummary(outcome.reload),
965
1043
  "",
966
1044
  verifyText(verify),
967
1045
  ];
@@ -980,12 +1058,14 @@ export function messagingSetupExtension(pi) {
980
1058
  }
981
1059
  },
982
1060
  });
983
- pi.registerCommand("mu-setup", {
984
- description: "Messaging setup workflow (`/mu-setup slack`, `/mu-setup plan <adapter>`, `/mu-setup apply <adapter>`, `/mu-setup verify [adapter]`)",
1061
+ registerMuSubcommand(pi, {
1062
+ subcommand: "setup",
1063
+ summary: "Messaging adapter setup workflow (preflight/guide/plan/apply/verify)",
1064
+ usage: "/mu setup [preflight|guide|plan|apply|verify] [adapter] [--public-base-url URL] [--agent|--no-agent]",
985
1065
  handler: async (args, ctx) => {
986
1066
  const parsed = parseSetupCommandArgs(args);
987
1067
  if (parsed.error) {
988
- ctx.ui.notify(`${parsed.error}. Usage: /mu-setup [preflight|guide|plan|apply|verify] [adapter] [--public-base-url URL] [--agent|--no-agent]`, "error");
1068
+ ctx.ui.notify(`${parsed.error}. Usage: /mu setup [preflight|guide|plan|apply|verify] [adapter] [--public-base-url URL] [--agent|--no-agent]`, "error");
989
1069
  return;
990
1070
  }
991
1071
  switch (parsed.action) {
@@ -1044,7 +1124,7 @@ export function messagingSetupExtension(pi) {
1044
1124
  }
1045
1125
  case "apply": {
1046
1126
  if (!parsed.adapterId) {
1047
- ctx.ui.notify("apply requires adapter. Example: /mu-setup apply slack", "error");
1127
+ ctx.ui.notify("apply requires adapter. Example: /mu setup apply slack", "error");
1048
1128
  return;
1049
1129
  }
1050
1130
  const text = await runInteractiveApply(ctx, parsed.adapterId);
@@ -0,0 +1,11 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ export type MuSubcommandHandler = (args: string, ctx: ExtensionCommandContext) => Promise<void> | void;
3
+ export type MuSubcommandRegistration = {
4
+ subcommand: string;
5
+ summary: string;
6
+ usage: string;
7
+ aliases?: string[];
8
+ handler: MuSubcommandHandler;
9
+ };
10
+ export declare function registerMuSubcommand(pi: ExtensionAPI, registration: MuSubcommandRegistration): void;
11
+ //# sourceMappingURL=mu-command-dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mu-command-dispatcher.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-command-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE3F,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEvG,MAAM,MAAM,wBAAwB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAiIF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAsDnG"}
@@ -0,0 +1,143 @@
1
+ const DISPATCHER_STATES = new WeakMap();
2
+ const RESERVED_SUBCOMMANDS = new Set(["help", "?"]);
3
+ function normalizeSubcommand(value) {
4
+ return value.trim().toLowerCase();
5
+ }
6
+ function isValidSubcommandToken(value) {
7
+ return /^[a-z][a-z0-9_-]*$/.test(value);
8
+ }
9
+ function subcommandUsageSummary(entry) {
10
+ const aliasSuffix = entry.aliases && entry.aliases.length > 0 ? ` (aliases: ${entry.aliases.join(", ")})` : "";
11
+ return `- ${entry.usage} — ${entry.summary}${aliasSuffix}`;
12
+ }
13
+ function renderSubcommandCatalog(state) {
14
+ if (state.entries.size === 0) {
15
+ return ["No /mu subcommands are currently registered.", "", "Try again after extensions finish loading."].join("\n");
16
+ }
17
+ const entries = [...state.entries.values()].sort((left, right) => left.normalizedSubcommand.localeCompare(right.normalizedSubcommand));
18
+ const lines = ["Usage: /mu <subcommand> [args]", "", "Subcommands:"];
19
+ for (const entry of entries) {
20
+ lines.push(subcommandUsageSummary(entry));
21
+ }
22
+ lines.push("", "Run `/mu help <subcommand>` for focused usage.");
23
+ return lines.join("\n");
24
+ }
25
+ function renderSubcommandHelp(entry) {
26
+ const lines = [entry.summary, "", `Usage: ${entry.usage}`];
27
+ if (entry.aliases && entry.aliases.length > 0) {
28
+ lines.push(`Aliases: ${entry.aliases.map((alias) => `/mu ${alias}`).join(", ")}`);
29
+ }
30
+ return lines.join("\n");
31
+ }
32
+ function parseInvocation(args) {
33
+ const trimmed = args.trim();
34
+ if (trimmed.length === 0) {
35
+ return { subcommand: "", remainder: "" };
36
+ }
37
+ const boundary = trimmed.search(/\s/);
38
+ if (boundary === -1) {
39
+ return { subcommand: trimmed, remainder: "" };
40
+ }
41
+ return {
42
+ subcommand: trimmed.slice(0, boundary),
43
+ remainder: trimmed.slice(boundary + 1).trim(),
44
+ };
45
+ }
46
+ function resolveEntry(state, token) {
47
+ const normalized = normalizeSubcommand(token);
48
+ if (!normalized)
49
+ return null;
50
+ const canonical = state.aliases.get(normalized) ?? normalized;
51
+ return state.entries.get(canonical) ?? null;
52
+ }
53
+ function ensureDispatcher(pi) {
54
+ const existing = DISPATCHER_STATES.get(pi);
55
+ if (existing) {
56
+ return existing;
57
+ }
58
+ const state = {
59
+ entries: new Map(),
60
+ aliases: new Map(),
61
+ };
62
+ DISPATCHER_STATES.set(pi, state);
63
+ pi.registerCommand("mu", {
64
+ description: "mu command dispatcher (`/mu <subcommand> ...`)",
65
+ handler: async (args, ctx) => {
66
+ const parsed = parseInvocation(args);
67
+ if (!parsed.subcommand) {
68
+ ctx.ui.notify(renderSubcommandCatalog(state), "info");
69
+ return;
70
+ }
71
+ const normalized = normalizeSubcommand(parsed.subcommand);
72
+ if (normalized === "help" || normalized === "?") {
73
+ if (!parsed.remainder) {
74
+ ctx.ui.notify(renderSubcommandCatalog(state), "info");
75
+ return;
76
+ }
77
+ const detail = resolveEntry(state, parsed.remainder.split(/\s+/)[0] ?? "");
78
+ if (!detail) {
79
+ ctx.ui.notify(`Unknown mu subcommand: ${parsed.remainder}\n\n${renderSubcommandCatalog(state)}`, "error");
80
+ return;
81
+ }
82
+ ctx.ui.notify(renderSubcommandHelp(detail), "info");
83
+ return;
84
+ }
85
+ const entry = resolveEntry(state, parsed.subcommand);
86
+ if (!entry) {
87
+ ctx.ui.notify(`Unknown mu subcommand: ${parsed.subcommand}\n\n${renderSubcommandCatalog(state)}`, "error");
88
+ return;
89
+ }
90
+ await entry.handler(parsed.remainder, ctx);
91
+ },
92
+ });
93
+ return state;
94
+ }
95
+ export function registerMuSubcommand(pi, registration) {
96
+ const state = ensureDispatcher(pi);
97
+ const normalizedSubcommand = normalizeSubcommand(registration.subcommand);
98
+ if (!isValidSubcommandToken(normalizedSubcommand)) {
99
+ throw new Error(`Invalid mu subcommand: ${registration.subcommand}`);
100
+ }
101
+ if (RESERVED_SUBCOMMANDS.has(normalizedSubcommand)) {
102
+ throw new Error(`Reserved mu subcommand: ${registration.subcommand}`);
103
+ }
104
+ if (!registration.usage.startsWith("/mu ")) {
105
+ throw new Error(`mu subcommand usage must start with '/mu ': ${registration.usage}`);
106
+ }
107
+ const normalizedAliases = (registration.aliases ?? [])
108
+ .map((alias) => normalizeSubcommand(alias))
109
+ .filter((alias) => alias.length > 0 && alias !== normalizedSubcommand);
110
+ for (const alias of normalizedAliases) {
111
+ if (!isValidSubcommandToken(alias)) {
112
+ throw new Error(`Invalid mu subcommand alias: ${alias}`);
113
+ }
114
+ if (RESERVED_SUBCOMMANDS.has(alias)) {
115
+ throw new Error(`Reserved mu subcommand alias: ${alias}`);
116
+ }
117
+ }
118
+ const existing = state.entries.get(normalizedSubcommand);
119
+ if (existing) {
120
+ for (const alias of existing.normalizedAliases) {
121
+ state.aliases.delete(alias);
122
+ }
123
+ }
124
+ for (const alias of normalizedAliases) {
125
+ const occupiedBy = state.aliases.get(alias);
126
+ if (occupiedBy && occupiedBy !== normalizedSubcommand) {
127
+ throw new Error(`mu subcommand alias '${alias}' is already registered by '${occupiedBy}'`);
128
+ }
129
+ if (state.entries.has(alias) && alias !== normalizedSubcommand) {
130
+ throw new Error(`mu subcommand alias '${alias}' conflicts with existing subcommand`);
131
+ }
132
+ }
133
+ const entry = {
134
+ ...registration,
135
+ normalizedSubcommand,
136
+ normalizedAliases,
137
+ };
138
+ state.entries.set(normalizedSubcommand, entry);
139
+ state.aliases.set(normalizedSubcommand, normalizedSubcommand);
140
+ for (const alias of normalizedAliases) {
141
+ state.aliases.set(alias, normalizedSubcommand);
142
+ }
143
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server-tools.d.ts","sourceRoot":"","sources":["../../src/extensions/server-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAgElE,MAAM,MAAM,wBAAwB,GAAG;IACtC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CAClC,CAAC;AAoXF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,GAAE,wBAA6B,QAQzF;AAED,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,YAAY,QAS5D;AAED,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"server-tools.d.ts","sourceRoot":"","sources":["../../src/extensions/server-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAqFlE,MAAM,MAAM,wBAAwB,GAAG;IACtC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CAClC,CAAC;AA2XF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,GAAE,wBAA6B,QAQzF;AAED,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,YAAY,QAS5D;AAED,eAAe,oBAAoB,CAAC"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { StringEnum } from "@mariozechner/pi-ai";
7
7
  import { Type } from "@sinclair/typebox";
8
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
8
9
  import { clampInt, fetchMuJson, fetchMuStatus, muServerUrl, textResult, toJsonText, } from "./shared.js";
9
10
  function trimOrNull(value) {
10
11
  if (value == null)
@@ -21,17 +22,33 @@ function cpRoutesFromStatus(routes, adapters) {
21
22
  route: `/webhooks/${name}`,
22
23
  }));
23
24
  }
25
+ function generationSummary(generation) {
26
+ const active = generation.active_generation?.generation_id ?? "(none)";
27
+ const pending = generation.pending_reload
28
+ ? `${generation.pending_reload.attempt_id}:${generation.pending_reload.state}`
29
+ : "(none)";
30
+ const last = generation.last_reload
31
+ ? `${generation.last_reload.attempt_id}:${generation.last_reload.state}`
32
+ : "(none)";
33
+ return `generation: active=${active} pending=${pending} last=${last}`;
34
+ }
35
+ function observabilitySummary(counters) {
36
+ return `observability: reload_success=${counters.reload_success_total} reload_failure=${counters.reload_failure_total} duplicate=${counters.duplicate_signal_total} drop=${counters.drop_signal_total}`;
37
+ }
24
38
  function summarizeStatus(status) {
25
- const cp = status.control_plane ?? { active: false, adapters: [], routes: [] };
39
+ const cp = status.control_plane;
26
40
  const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
27
41
  const routeText = routes.length > 0 ? routes.map((entry) => `${entry.name}:${entry.route}`).join(", ") : "(none)";
28
- return [
42
+ const lines = [
29
43
  `repo: ${status.repo_root}`,
30
44
  `issues: open=${status.open_count} ready=${status.ready_count}`,
31
45
  `control_plane: ${cp.active ? "active" : "inactive"}`,
32
46
  `adapters: ${cp.adapters.length > 0 ? cp.adapters.join(", ") : "(none)"}`,
33
47
  `routes: ${routeText}`,
34
- ].join("\n");
48
+ generationSummary(cp.generation),
49
+ observabilitySummary(cp.observability.counters),
50
+ ];
51
+ return lines.join("\n");
35
52
  }
36
53
  function sliceWithLimit(items, limitRaw, fallback = 50) {
37
54
  const limit = clampInt(limitRaw, fallback, 1, 200);
@@ -68,7 +85,7 @@ function registerServerTools(pi, opts) {
68
85
  ctx.ui.setStatus("mu-server", ctx.ui.theme.fg("dim", `μ server ${url}`));
69
86
  try {
70
87
  const status = await fetchMuStatus(4_000);
71
- ctx.ui.setStatus("mu-status", ctx.ui.theme.fg("dim", `open ${status.open_count} · ready ${status.ready_count} · cp ${status.control_plane?.active ? "on" : "off"}`));
88
+ ctx.ui.setStatus("mu-status", ctx.ui.theme.fg("dim", `open ${status.open_count} · ready ${status.ready_count} · cp ${status.control_plane.active ? "on" : "off"}`));
72
89
  }
73
90
  catch {
74
91
  ctx.ui.setStatus("mu-status", ctx.ui.theme.fg("warning", "μ status unavailable"));
@@ -96,19 +113,19 @@ function registerServerTools(pi, opts) {
96
113
  parameters: ControlPlaneParams,
97
114
  async execute(_toolCallId, params) {
98
115
  const status = await fetchMuStatus();
99
- const cp = status.control_plane ?? {
100
- active: false,
101
- adapters: [],
102
- routes: [],
103
- };
116
+ const cp = status.control_plane;
104
117
  const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
118
+ const generation = cp.generation;
119
+ const observability = cp.observability.counters;
105
120
  switch (params.action) {
106
121
  case "status":
107
122
  return textResult(toJsonText({
108
123
  active: cp.active,
109
124
  adapters: cp.adapters,
110
125
  routes,
111
- }), { control_plane: cp, routes });
126
+ generation,
127
+ observability,
128
+ }), { control_plane: cp, routes, generation, observability });
112
129
  case "adapters":
113
130
  return textResult(toJsonText(cp.adapters), { adapters: cp.adapters });
114
131
  case "routes":
@@ -360,8 +377,10 @@ function registerServerTools(pi, opts) {
360
377
  }
361
378
  },
362
379
  });
363
- pi.registerCommand("mu-status", {
364
- description: "Show concise mu server status",
380
+ registerMuSubcommand(pi, {
381
+ subcommand: "status",
382
+ summary: "Show concise mu server status",
383
+ usage: "/mu status",
365
384
  handler: async (_args, ctx) => {
366
385
  try {
367
386
  const status = await fetchMuStatus();
@@ -372,21 +391,21 @@ function registerServerTools(pi, opts) {
372
391
  }
373
392
  },
374
393
  });
375
- pi.registerCommand("mu-control", {
376
- description: "Show control-plane adapter/runtime status",
394
+ registerMuSubcommand(pi, {
395
+ subcommand: "control",
396
+ summary: "Show control-plane adapter/runtime status",
397
+ usage: "/mu control",
377
398
  handler: async (_args, ctx) => {
378
399
  try {
379
400
  const status = await fetchMuStatus();
380
- const cp = status.control_plane ?? {
381
- active: false,
382
- adapters: [],
383
- routes: [],
384
- };
401
+ const cp = status.control_plane;
385
402
  const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
386
403
  const lines = [
387
404
  `control_plane: ${cp.active ? "active" : "inactive"}`,
388
405
  `adapters: ${cp.adapters.length > 0 ? cp.adapters.join(", ") : "(none)"}`,
389
406
  `routes: ${routes.length > 0 ? routes.map((entry) => `${entry.name}:${entry.route}`).join(", ") : "(none)"}`,
407
+ generationSummary(cp.generation),
408
+ observabilitySummary(cp.observability.counters),
390
409
  ];
391
410
  ctx.ui.notify(lines.join("\n"), "info");
392
411
  }
@@ -400,7 +419,7 @@ export function serverToolsExtension(pi, opts = {}) {
400
419
  registerServerTools(pi, {
401
420
  allowForumPost: opts.allowForumPost ?? true,
402
421
  toolIntroLine: opts.toolIntroLine ??
403
- "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events, mu_runs, mu_activities, mu_heartbeats, mu_identity.",
422
+ "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events, mu_runs, mu_activities, mu_heartbeats, mu_cron, mu_identity.",
404
423
  extraSystemPromptLines: opts.extraSystemPromptLines ?? [],
405
424
  });
406
425
  }
@@ -2,15 +2,48 @@ export type MuControlPlaneRoute = {
2
2
  name: string;
3
3
  route: string;
4
4
  };
5
+ export type MuGenerationIdentity = {
6
+ generation_id: string;
7
+ generation_seq: number;
8
+ };
9
+ export type MuGenerationReloadAttempt = {
10
+ attempt_id: string;
11
+ reason: string;
12
+ state: "planned" | "swapped" | "completed" | "failed";
13
+ requested_at_ms: number;
14
+ swapped_at_ms: number | null;
15
+ finished_at_ms: number | null;
16
+ from_generation: MuGenerationIdentity | null;
17
+ to_generation: MuGenerationIdentity;
18
+ };
19
+ export type MuGenerationSupervisorSnapshot = {
20
+ supervisor_id: string;
21
+ active_generation: MuGenerationIdentity | null;
22
+ pending_reload: MuGenerationReloadAttempt | null;
23
+ last_reload: MuGenerationReloadAttempt | null;
24
+ };
25
+ export type MuGenerationObservabilityCounters = {
26
+ reload_success_total: number;
27
+ reload_failure_total: number;
28
+ reload_drain_duration_ms_total: number;
29
+ reload_drain_duration_samples_total: number;
30
+ duplicate_signal_total: number;
31
+ drop_signal_total: number;
32
+ };
33
+ export type MuControlPlaneStatus = {
34
+ active: boolean;
35
+ adapters: string[];
36
+ routes?: MuControlPlaneRoute[];
37
+ generation: MuGenerationSupervisorSnapshot;
38
+ observability: {
39
+ counters: MuGenerationObservabilityCounters;
40
+ };
41
+ };
5
42
  export type MuStatusResponse = {
6
43
  repo_root: string;
7
44
  open_count: number;
8
45
  ready_count: number;
9
- control_plane?: {
10
- active: boolean;
11
- adapters: string[];
12
- routes?: MuControlPlaneRoute[];
13
- };
46
+ control_plane: MuControlPlaneStatus;
14
47
  };
15
48
  export declare function muServerUrl(): string | null;
16
49
  export declare function clampInt(value: number | undefined, fallback: number, min: number, max: number): number;
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/extensions/shared.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE;QACf,MAAM,EAAE,OAAO,CAAC;QAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,MAAM,CAAC,EAAE,mBAAmB,EAAE,CAAC;KAC/B,CAAC;CACF,CAAC;AAEF,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAG3C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKtG;AAED,wBAAsB,WAAW,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAChE,OAAO,CAAC,CAAC,CAAC,CA4BZ;AAED,wBAAsB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEjF;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;;;;;;EAK7E;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/extensions/shared.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtD,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC7C,aAAa,EAAE,oBAAoB,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC5C,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC/C,cAAc,EAAE,yBAAyB,GAAG,IAAI,CAAC;IACjD,WAAW,EAAE,yBAAyB,GAAG,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC/C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,8BAA8B,EAAE,MAAM,CAAC;IACvC,mCAAmC,EAAE,MAAM,CAAC;IAC5C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAClC,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC/B,UAAU,EAAE,8BAA8B,CAAC;IAC3C,aAAa,EAAE;QACd,QAAQ,EAAE,iCAAiC,CAAC;KAC5C,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,oBAAoB,CAAC;CACpC,CAAC;AAEF,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAG3C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKtG;AAED,wBAAsB,WAAW,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAChE,OAAO,CAAC,CAAC,CAAC,CA4BZ;AAiCD,wBAAsB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAGjF;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;;;;;;EAK7E;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD"}
@@ -39,8 +39,30 @@ export async function fetchMuJson(path, opts = {}) {
39
39
  clearTimeout(timeout);
40
40
  }
41
41
  }
42
+ function ensureGenerationScopedStatus(status) {
43
+ const controlPlane = status.control_plane;
44
+ if (!controlPlane || typeof controlPlane !== "object") {
45
+ throw new Error("mu server /api/status missing control_plane payload (expected generation-scoped contract)");
46
+ }
47
+ const controlPlaneRecord = controlPlane;
48
+ if (!("generation" in controlPlaneRecord) || !controlPlaneRecord.generation) {
49
+ throw new Error("mu server /api/status missing control_plane.generation (expected generation-scoped contract)");
50
+ }
51
+ if (!("observability" in controlPlaneRecord) || !controlPlaneRecord.observability) {
52
+ throw new Error("mu server /api/status missing control_plane.observability (expected generation-scoped contract)");
53
+ }
54
+ const observability = controlPlaneRecord.observability;
55
+ if (typeof observability !== "object" ||
56
+ observability == null ||
57
+ !("counters" in observability) ||
58
+ !observability.counters) {
59
+ throw new Error("mu server /api/status missing control_plane.observability.counters (expected generation-scoped contract)");
60
+ }
61
+ return status;
62
+ }
42
63
  export async function fetchMuStatus(timeoutMs) {
43
- return await fetchMuJson("/api/status", { timeoutMs });
64
+ const status = await fetchMuJson("/api/status", { timeoutMs });
65
+ return ensureGenerationScopedStatus(status);
44
66
  }
45
67
  export function textResult(text, details = {}) {
46
68
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.55",
3
+ "version": "26.2.57",
4
4
  "description": "Shared agent runtime for mu chat, orchestration roles, and serve extensions.",
5
5
  "keywords": [
6
6
  "mu",
@@ -23,7 +23,7 @@
23
23
  "prompts/**"
24
24
  ],
25
25
  "dependencies": {
26
- "@femtomc/mu-core": "26.2.54",
26
+ "@femtomc/mu-core": "workspace:*",
27
27
  "@mariozechner/pi-agent-core": "^0.52.12",
28
28
  "@mariozechner/pi-ai": "^0.52.12",
29
29
  "@mariozechner/pi-coding-agent": "^0.52.12",
@@ -22,6 +22,7 @@ You also have access to specialized read/diagnostic tools:
22
22
  - `mu_runs`
23
23
  - `mu_activities`
24
24
  - `mu_heartbeats`
25
+ - `mu_cron`
25
26
  - `mu_messaging_setup`
26
27
 
27
28
  Hard Constraints: