@femtomc/mu-agent 26.2.84 → 26.2.86

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
@@ -48,6 +48,8 @@ Current stack:
48
48
 
49
49
  - `brandingExtension` — mu compact header/footer branding + default theme
50
50
  - `eventLogExtension` — event tail + watch widget
51
+ - `planningUiExtension` — planning phase/checklist HUD widget (`/mu plan ...`)
52
+ - `subagentsUiExtension` — tmux subagent monitor/spawner widget (`/mu subagents ...`)
51
53
 
52
54
  Default operator UI theme is `mu-gruvbox-dark`.
53
55
 
@@ -56,6 +58,8 @@ Default operator UI theme is `mu-gruvbox-dark`.
56
58
  - `/mu events [n]` / `/mu events tail [n]` — event log tail
57
59
  - `/mu events watch on|off` — toggle event watch widget
58
60
  - `/mu brand on|off|toggle` — enable/disable UI branding
61
+ - `/mu plan on|off|status|phase|root|check|uncheck|toggle-step|reset` — planning HUD
62
+ - `/mu subagents on|off|status|refresh|prefix|root|role|spawn` — tmux + issue queue monitor/spawner
59
63
  - `/mu help` — dispatcher catalog of registered `/mu` subcommands
60
64
 
61
65
  ## Tooling model (CLI-first)
@@ -2,6 +2,8 @@ export { brandingExtension } from "./branding.js";
2
2
  export { eventLogExtension } from "./event-log.js";
3
3
  export { muOperatorExtension } from "./mu-operator.js";
4
4
  export { muServeExtension } from "./mu-serve.js";
5
+ export { planningUiExtension } from "./planning-ui.js";
6
+ export { subagentsUiExtension } from "./subagents-ui.js";
5
7
  /**
6
8
  * Serve-mode extension — single facade that bundles all serve extensions.
7
9
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAA4C,CAAC;AAE7E;;GAEG;AACH,eAAO,MAAM,sBAAsB,UAA+C,CAAC;AAEnF;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,MAAM,EAAO,CAAC;AAC3D,eAAO,MAAM,wBAAwB,EAAE,MAAM,EAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAQzD;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAA4C,CAAC;AAE7E;;GAEG;AACH,eAAO,MAAM,sBAAsB,UAA+C,CAAC;AAEnF;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,MAAM,EAAO,CAAC;AAC3D,eAAO,MAAM,wBAAwB,EAAE,MAAM,EAAO,CAAC"}
@@ -2,6 +2,8 @@ export { brandingExtension } from "./branding.js";
2
2
  export { eventLogExtension } from "./event-log.js";
3
3
  export { muOperatorExtension } from "./mu-operator.js";
4
4
  export { muServeExtension } from "./mu-serve.js";
5
+ export { planningUiExtension } from "./planning-ui.js";
6
+ export { subagentsUiExtension } from "./subagents-ui.js";
5
7
  const RUNTIME_EXTENSION = import.meta.url.endsWith(".ts") ? "ts" : "js";
6
8
  function resolveBundledExtensionPath(moduleBasename) {
7
9
  return new URL(`./${moduleBasename}.${RUNTIME_EXTENSION}`, import.meta.url).pathname;
@@ -1 +1 @@
1
- {"version":3,"file":"mu-operator.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-operator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAIlE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAGnD;AAED,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"mu-operator.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-operator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAMlE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAKnD;AAED,eAAe,mBAAmB,CAAC"}
@@ -6,8 +6,12 @@
6
6
  */
7
7
  import { brandingExtension } from "./branding.js";
8
8
  import { eventLogExtension } from "./event-log.js";
9
+ import { planningUiExtension } from "./planning-ui.js";
10
+ import { subagentsUiExtension } from "./subagents-ui.js";
9
11
  export function muOperatorExtension(pi) {
10
12
  brandingExtension(pi);
11
13
  eventLogExtension(pi);
14
+ planningUiExtension(pi);
15
+ subagentsUiExtension(pi);
12
16
  }
13
17
  export default muOperatorExtension;
@@ -1 +1 @@
1
- {"version":3,"file":"mu-serve.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-serve.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAIlE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,QAGhD;AAED,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"mu-serve.d.ts","sourceRoot":"","sources":["../../src/extensions/mu-serve.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAMlE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,QAKhD;AAED,eAAe,gBAAgB,CAAC"}
@@ -6,8 +6,12 @@
6
6
  */
7
7
  import { brandingExtension } from "./branding.js";
8
8
  import { eventLogExtension } from "./event-log.js";
9
+ import { planningUiExtension } from "./planning-ui.js";
10
+ import { subagentsUiExtension } from "./subagents-ui.js";
9
11
  export function muServeExtension(pi) {
10
12
  brandingExtension(pi);
11
13
  eventLogExtension(pi);
14
+ planningUiExtension(pi);
15
+ subagentsUiExtension(pi);
12
16
  }
13
17
  export default muServeExtension;
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function planningUiExtension(pi: ExtensionAPI): void;
3
+ export default planningUiExtension;
4
+ //# sourceMappingURL=planning-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planning-ui.d.ts","sourceRoot":"","sources":["../../src/extensions/planning-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AA4FpF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAoHnD;AAED,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,179 @@
1
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
2
+ const DEFAULT_STEPS = [
3
+ "Investigate relevant code/docs/state",
4
+ "Create root issue + decomposed child issues",
5
+ "Present plan with IDs, ordering, risks",
6
+ "Refine with user feedback until approved",
7
+ ];
8
+ function createDefaultState() {
9
+ return {
10
+ enabled: false,
11
+ phase: "investigating",
12
+ rootIssueId: null,
13
+ steps: DEFAULT_STEPS.map((label) => ({ label, done: false })),
14
+ };
15
+ }
16
+ function summarizePhase(phase) {
17
+ switch (phase) {
18
+ case "investigating":
19
+ return "investigating";
20
+ case "drafting":
21
+ return "drafting";
22
+ case "reviewing":
23
+ return "reviewing";
24
+ case "approved":
25
+ return "approved";
26
+ }
27
+ }
28
+ function renderPlanningUi(ctx, state) {
29
+ if (!ctx.hasUI) {
30
+ return;
31
+ }
32
+ if (!state.enabled) {
33
+ ctx.ui.setStatus("mu-planning", undefined);
34
+ ctx.ui.setWidget("mu-planning", undefined);
35
+ return;
36
+ }
37
+ const done = state.steps.filter((step) => step.done).length;
38
+ const total = state.steps.length;
39
+ 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}`));
42
+ 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
+ }),
51
+ ];
52
+ ctx.ui.setWidget("mu-planning", lines, { placement: "belowEditor" });
53
+ }
54
+ function planningUsageText() {
55
+ return [
56
+ "Usage:",
57
+ " /mu plan on|off|toggle|status|reset",
58
+ " /mu plan phase <investigating|drafting|reviewing|approved>",
59
+ " /mu plan root <issue-id|clear>",
60
+ " /mu plan check <n> | /mu plan uncheck <n> | /mu plan toggle-step <n>",
61
+ ].join("\n");
62
+ }
63
+ function parsePlanningPhase(raw) {
64
+ const value = raw.trim().toLowerCase();
65
+ if (value === "investigating" || value === "drafting" || value === "reviewing" || value === "approved") {
66
+ return value;
67
+ }
68
+ return null;
69
+ }
70
+ export function planningUiExtension(pi) {
71
+ let state = createDefaultState();
72
+ const notify = (ctx, message, level = "info") => {
73
+ ctx.ui.notify(`${message}\n\n${planningUsageText()}`, level);
74
+ };
75
+ const refresh = (ctx) => {
76
+ renderPlanningUi(ctx, state);
77
+ };
78
+ pi.on("session_start", async (_event, ctx) => {
79
+ refresh(ctx);
80
+ });
81
+ pi.on("session_switch", async (_event, ctx) => {
82
+ refresh(ctx);
83
+ });
84
+ registerMuSubcommand(pi, {
85
+ 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",
88
+ handler: async (args, ctx) => {
89
+ const tokens = args
90
+ .trim()
91
+ .split(/\s+/)
92
+ .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]) {
101
+ case "on":
102
+ state.enabled = true;
103
+ refresh(ctx);
104
+ ctx.ui.notify("Planning HUD enabled.", "info");
105
+ return;
106
+ case "off":
107
+ state.enabled = false;
108
+ refresh(ctx);
109
+ ctx.ui.notify("Planning HUD disabled.", "info");
110
+ return;
111
+ case "toggle":
112
+ state.enabled = !state.enabled;
113
+ refresh(ctx);
114
+ ctx.ui.notify(`Planning HUD ${state.enabled ? "enabled" : "disabled"}.`, "info");
115
+ return;
116
+ 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
+ }
145
+ case "check":
146
+ 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;
172
+ }
173
+ default:
174
+ notify(ctx, `Unknown plan command: ${tokens[0]}`, "error");
175
+ }
176
+ },
177
+ });
178
+ }
179
+ export default planningUiExtension;
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function subagentsUiExtension(pi: ExtensionAPI): void;
3
+ export default subagentsUiExtension;
4
+ //# sourceMappingURL=subagents-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subagents-ui.d.ts","sourceRoot":"","sources":["../../src/extensions/subagents-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAuapF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,QAmRpD;AAED,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,602 @@
1
+ import { registerMuSubcommand } from "./mu-command-dispatcher.js";
2
+ const DEFAULT_PREFIX = "mu-sub-";
3
+ const DEFAULT_ROLE_TAG = "role:worker";
4
+ const ISSUE_LIST_LIMIT = 40;
5
+ const MU_CLI_TIMEOUT_MS = 12_000;
6
+ function shellQuote(value) {
7
+ return `'${value.replaceAll("'", "'\"'\"'")}'`;
8
+ }
9
+ function spawnRunId(now = new Date()) {
10
+ return now.toISOString().replaceAll(/[-:TZ.]/g, "").slice(0, 14);
11
+ }
12
+ function issueHasSession(sessions, issueId) {
13
+ return sessions.some((session) => session === issueId ||
14
+ session.endsWith(`-${issueId}`) ||
15
+ session.includes(`-${issueId}-`) ||
16
+ session.includes(`_${issueId}`));
17
+ }
18
+ function buildSubagentPrompt(issue) {
19
+ return [
20
+ `Work issue ${issue.id} (${truncateOneLine(issue.title, 80)}).`,
21
+ `First run: mu issues claim ${issue.id}.`,
22
+ `Keep forum updates in topic issue:${issue.id}.`,
23
+ "When done, close with an explicit outcome and summary.",
24
+ ].join(" ");
25
+ }
26
+ async function spawnIssueTmuxSession(opts) {
27
+ const shellCommand = `cd ${shellQuote(opts.cwd)} && mu exec ${shellQuote(buildSubagentPrompt(opts.issue))} ; rc=$?; echo __MU_DONE__:$rc`;
28
+ let proc = null;
29
+ try {
30
+ proc = Bun.spawn({
31
+ cmd: ["tmux", "new-session", "-d", "-s", opts.sessionName, shellCommand],
32
+ stdin: "ignore",
33
+ stdout: "pipe",
34
+ stderr: "pipe",
35
+ });
36
+ }
37
+ catch (err) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ return { ok: false, error: `failed to launch tmux session: ${message}` };
40
+ }
41
+ const [exitCode, stderr] = await Promise.all([proc.exited, readableText(proc.stderr)]);
42
+ if (exitCode !== 0) {
43
+ const detail = stderr.trim();
44
+ return { ok: false, error: detail.length > 0 ? detail : `tmux exited ${exitCode}` };
45
+ }
46
+ return { ok: true, error: null };
47
+ }
48
+ function readableText(stream) {
49
+ if (stream && typeof stream === "object" && "getReader" in stream) {
50
+ return new Response(stream).text().catch(() => "");
51
+ }
52
+ return Promise.resolve("");
53
+ }
54
+ function createDefaultState() {
55
+ return {
56
+ enabled: false,
57
+ prefix: DEFAULT_PREFIX,
58
+ sessions: [],
59
+ sessionError: null,
60
+ issueRootId: null,
61
+ issueRoleTag: DEFAULT_ROLE_TAG,
62
+ readyIssues: [],
63
+ activeIssues: [],
64
+ issueError: null,
65
+ lastUpdatedMs: null,
66
+ };
67
+ }
68
+ function truncateOneLine(input, maxLen = 68) {
69
+ const compact = input.replace(/\s+/g, " ").trim();
70
+ if (compact.length <= maxLen) {
71
+ return compact;
72
+ }
73
+ return `${compact.slice(0, Math.max(0, maxLen - 1))}…`;
74
+ }
75
+ function summarizeFailure(label, outcome) {
76
+ if (outcome.error) {
77
+ return `${label}: ${outcome.error}`;
78
+ }
79
+ if (outcome.timedOut) {
80
+ return `${label}: timed out after ${MU_CLI_TIMEOUT_MS}ms`;
81
+ }
82
+ const stderr = outcome.stderr.trim();
83
+ if (stderr.length > 0) {
84
+ return `${label}: ${stderr}`;
85
+ }
86
+ const stdout = outcome.stdout.trim();
87
+ if (stdout.length > 0) {
88
+ return `${label}: ${truncateOneLine(stdout, 120)}`;
89
+ }
90
+ return `${label}: exit ${outcome.exitCode}`;
91
+ }
92
+ function normalizeIssueDigest(row) {
93
+ if (!row || typeof row !== "object") {
94
+ return null;
95
+ }
96
+ const value = row;
97
+ const id = typeof value.id === "string" ? value.id.trim() : "";
98
+ const title = typeof value.title === "string" ? value.title : "";
99
+ const status = value.status;
100
+ const priorityRaw = value.priority;
101
+ const tagsRaw = value.tags;
102
+ if (!id || !title) {
103
+ return null;
104
+ }
105
+ if (status !== "open" && status !== "in_progress" && status !== "closed") {
106
+ return null;
107
+ }
108
+ const priority = typeof priorityRaw === "number" && Number.isFinite(priorityRaw) ? Math.trunc(priorityRaw) : 3;
109
+ const tags = Array.isArray(tagsRaw) ? tagsRaw.filter((tag) => typeof tag === "string") : [];
110
+ return {
111
+ id,
112
+ title,
113
+ status,
114
+ priority,
115
+ tags,
116
+ };
117
+ }
118
+ function parseIssueArray(label, jsonText) {
119
+ const trimmed = jsonText.trim();
120
+ if (trimmed.length === 0) {
121
+ return { issues: [], error: null };
122
+ }
123
+ let parsed = null;
124
+ try {
125
+ parsed = JSON.parse(trimmed);
126
+ }
127
+ catch {
128
+ return { issues: [], error: `${label}: invalid JSON output from mu issues command` };
129
+ }
130
+ if (!Array.isArray(parsed)) {
131
+ return { issues: [], error: `${label}: expected JSON array output from mu issues command` };
132
+ }
133
+ const issues = parsed.map(normalizeIssueDigest).filter((issue) => issue !== null);
134
+ issues.sort((left, right) => {
135
+ if (left.priority !== right.priority) {
136
+ return left.priority - right.priority;
137
+ }
138
+ return left.id.localeCompare(right.id);
139
+ });
140
+ return { issues, error: null };
141
+ }
142
+ async function runMuCli(args) {
143
+ let proc = null;
144
+ try {
145
+ proc = Bun.spawn({
146
+ cmd: ["mu", ...args],
147
+ stdin: "ignore",
148
+ stdout: "pipe",
149
+ stderr: "pipe",
150
+ });
151
+ }
152
+ catch (err) {
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ return {
155
+ exitCode: 1,
156
+ stdout: "",
157
+ stderr: "",
158
+ timedOut: false,
159
+ error: `failed to launch mu CLI (${message})`,
160
+ };
161
+ }
162
+ let timedOut = false;
163
+ const timeout = setTimeout(() => {
164
+ timedOut = true;
165
+ proc?.kill();
166
+ }, MU_CLI_TIMEOUT_MS);
167
+ const [exitCode, stdout, stderr] = await Promise.all([proc.exited, readableText(proc.stdout), readableText(proc.stderr)]);
168
+ clearTimeout(timeout);
169
+ return {
170
+ exitCode,
171
+ stdout,
172
+ stderr,
173
+ timedOut,
174
+ error: null,
175
+ };
176
+ }
177
+ async function listTmuxSessions(prefix) {
178
+ let proc = null;
179
+ try {
180
+ proc = Bun.spawn({
181
+ cmd: ["tmux", "ls"],
182
+ stdin: "ignore",
183
+ stdout: "pipe",
184
+ stderr: "pipe",
185
+ });
186
+ }
187
+ catch (err) {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ return { sessions: [], error: `failed to launch tmux: ${message}` };
190
+ }
191
+ const [exitCode, stdout, stderr] = await Promise.all([proc.exited, readableText(proc.stdout), readableText(proc.stderr)]);
192
+ const stderrTrimmed = stderr.trim();
193
+ if (exitCode !== 0) {
194
+ const lowered = stderrTrimmed.toLowerCase();
195
+ if (lowered.includes("no server running") || lowered.includes("failed to connect to server")) {
196
+ return { sessions: [], error: null };
197
+ }
198
+ const detail = stderrTrimmed.length > 0 ? stderrTrimmed : `tmux exited ${exitCode}`;
199
+ return { sessions: [], error: detail };
200
+ }
201
+ const sessions = stdout
202
+ .split("\n")
203
+ .map((line) => line.trim())
204
+ .filter((line) => line.length > 0)
205
+ .map((line) => {
206
+ const colon = line.indexOf(":");
207
+ return colon >= 0 ? line.slice(0, colon).trim() : line;
208
+ })
209
+ .filter((name) => name.length > 0)
210
+ .filter((name) => (prefix.length > 0 ? name.startsWith(prefix) : true))
211
+ .sort((left, right) => left.localeCompare(right));
212
+ return { sessions, error: null };
213
+ }
214
+ async function listIssueSlices(rootId, roleTag) {
215
+ const readyArgs = ["issues", "ready", "--json", "--limit", String(ISSUE_LIST_LIMIT)];
216
+ const activeArgs = ["issues", "list", "--status", "in_progress", "--json", "--limit", String(ISSUE_LIST_LIMIT)];
217
+ if (rootId) {
218
+ readyArgs.push("--root", rootId);
219
+ activeArgs.push("--root", rootId);
220
+ }
221
+ if (roleTag) {
222
+ readyArgs.push("--tag", roleTag);
223
+ activeArgs.push("--tag", roleTag);
224
+ }
225
+ const [readyOutcome, activeOutcome] = await Promise.all([runMuCli(readyArgs), runMuCli(activeArgs)]);
226
+ if (readyOutcome.exitCode !== 0 || readyOutcome.error || readyOutcome.timedOut) {
227
+ return {
228
+ ready: [],
229
+ active: [],
230
+ error: summarizeFailure("ready", readyOutcome),
231
+ };
232
+ }
233
+ if (activeOutcome.exitCode !== 0 || activeOutcome.error || activeOutcome.timedOut) {
234
+ return {
235
+ ready: [],
236
+ active: [],
237
+ error: summarizeFailure("in-progress", activeOutcome),
238
+ };
239
+ }
240
+ const readyParsed = parseIssueArray("ready", readyOutcome.stdout);
241
+ if (readyParsed.error) {
242
+ return { ready: [], active: [], error: readyParsed.error };
243
+ }
244
+ const activeParsed = parseIssueArray("in-progress", activeOutcome.stdout);
245
+ if (activeParsed.error) {
246
+ return { ready: [], active: [], error: activeParsed.error };
247
+ }
248
+ return {
249
+ ready: readyParsed.issues,
250
+ active: activeParsed.issues,
251
+ error: null,
252
+ };
253
+ }
254
+ function formatIssueLine(ctx, issue) {
255
+ const id = ctx.ui.theme.fg("dim", issue.id);
256
+ const priority = ctx.ui.theme.fg("muted", `p${issue.priority}`);
257
+ return ` ${ctx.ui.theme.fg("success", "•")} ${id} ${priority} ${truncateOneLine(issue.title)}`;
258
+ }
259
+ function renderSubagentsUi(ctx, state) {
260
+ if (!ctx.hasUI) {
261
+ return;
262
+ }
263
+ if (!state.enabled) {
264
+ ctx.ui.setStatus("mu-subagents", undefined);
265
+ ctx.ui.setWidget("mu-subagents", undefined);
266
+ return;
267
+ }
268
+ const issueScope = state.issueRootId ? `root ${state.issueRootId}` : "all roots";
269
+ const roleScope = state.issueRoleTag ? state.issueRoleTag : "(all roles)";
270
+ const issueStatus = `${state.readyIssues.length} ready / ${state.activeIssues.length} active`;
271
+ if (state.sessionError || state.issueError) {
272
+ ctx.ui.setStatus("mu-subagents", ctx.ui.theme.fg("warning", `subagents ${state.sessions.length} · issues ${issueStatus} · degraded`));
273
+ }
274
+ else {
275
+ ctx.ui.setStatus("mu-subagents", ctx.ui.theme.fg("dim", `subagents ${state.sessions.length} · issues ${issueStatus} · ${issueScope}`));
276
+ }
277
+ const lines = [
278
+ ctx.ui.theme.fg("accent", "Subagents monitor"),
279
+ ctx.ui.theme.fg("dim", ` tmux prefix: ${state.prefix || "(all sessions)"}`),
280
+ ctx.ui.theme.fg("dim", ` issue scope: ${issueScope} · tag ${roleScope}`),
281
+ ];
282
+ if (state.sessionError) {
283
+ lines.push(ctx.ui.theme.fg("warning", ` tmux error: ${state.sessionError}`));
284
+ }
285
+ else if (state.sessions.length === 0) {
286
+ lines.push(ctx.ui.theme.fg("muted", " tmux: (no matching sessions)"));
287
+ }
288
+ else {
289
+ for (const name of state.sessions.slice(0, 8)) {
290
+ lines.push(` ${ctx.ui.theme.fg("success", "●")} ${name}`);
291
+ }
292
+ if (state.sessions.length > 8) {
293
+ lines.push(ctx.ui.theme.fg("muted", ` ... +${state.sessions.length - 8} more tmux sessions`));
294
+ }
295
+ }
296
+ if (state.issueError) {
297
+ lines.push(ctx.ui.theme.fg("warning", ` issue error: ${state.issueError}`));
298
+ }
299
+ else {
300
+ lines.push(ctx.ui.theme.fg("accent", "Ready issues"));
301
+ if (state.readyIssues.length === 0) {
302
+ lines.push(ctx.ui.theme.fg("muted", " (no ready issues)"));
303
+ }
304
+ else {
305
+ for (const issue of state.readyIssues.slice(0, 6)) {
306
+ lines.push(formatIssueLine(ctx, issue));
307
+ }
308
+ if (state.readyIssues.length > 6) {
309
+ lines.push(ctx.ui.theme.fg("muted", ` ... +${state.readyIssues.length - 6} more ready issues`));
310
+ }
311
+ }
312
+ lines.push(ctx.ui.theme.fg("accent", "Active issues"));
313
+ if (state.activeIssues.length === 0) {
314
+ lines.push(ctx.ui.theme.fg("muted", " (no in-progress issues)"));
315
+ }
316
+ else {
317
+ for (const issue of state.activeIssues.slice(0, 6)) {
318
+ lines.push(formatIssueLine(ctx, issue));
319
+ }
320
+ if (state.activeIssues.length > 6) {
321
+ lines.push(ctx.ui.theme.fg("muted", ` ... +${state.activeIssues.length - 6} more active issues`));
322
+ }
323
+ }
324
+ }
325
+ ctx.ui.setWidget("mu-subagents", lines, { placement: "belowEditor" });
326
+ }
327
+ function subagentsUsageText() {
328
+ return [
329
+ "Usage:",
330
+ " /mu subagents on|off|toggle|status|refresh",
331
+ " /mu subagents prefix <text|clear>",
332
+ " /mu subagents root <issue-id|clear>",
333
+ " /mu subagents role <tag|clear>",
334
+ " /mu subagents spawn [N|all]",
335
+ ].join("\n");
336
+ }
337
+ function normalizeRoleTag(raw) {
338
+ const trimmed = raw.trim();
339
+ if (!trimmed || trimmed.toLowerCase() === "clear") {
340
+ return null;
341
+ }
342
+ if (trimmed === "worker" || trimmed === "orchestrator") {
343
+ return `role:${trimmed}`;
344
+ }
345
+ return trimmed;
346
+ }
347
+ export function subagentsUiExtension(pi) {
348
+ let activeCtx = null;
349
+ let pollTimer = null;
350
+ let state = createDefaultState();
351
+ const refresh = async (ctx) => {
352
+ if (!state.enabled) {
353
+ renderSubagentsUi(ctx, state);
354
+ return;
355
+ }
356
+ const [tmux, issues] = await Promise.all([
357
+ listTmuxSessions(state.prefix),
358
+ listIssueSlices(state.issueRootId, state.issueRoleTag),
359
+ ]);
360
+ state.sessions = tmux.sessions;
361
+ state.sessionError = tmux.error;
362
+ state.readyIssues = issues.ready;
363
+ state.activeIssues = issues.active;
364
+ state.issueError = issues.error;
365
+ state.lastUpdatedMs = Date.now();
366
+ renderSubagentsUi(ctx, state);
367
+ };
368
+ const stopPolling = () => {
369
+ if (!pollTimer) {
370
+ return;
371
+ }
372
+ clearInterval(pollTimer);
373
+ pollTimer = null;
374
+ };
375
+ const ensurePolling = () => {
376
+ if (pollTimer) {
377
+ return;
378
+ }
379
+ pollTimer = setInterval(() => {
380
+ if (!activeCtx) {
381
+ return;
382
+ }
383
+ void refresh(activeCtx);
384
+ }, 8_000);
385
+ };
386
+ const notify = (ctx, message, level = "info") => {
387
+ ctx.ui.notify(`${message}\n\n${subagentsUsageText()}`, level);
388
+ };
389
+ pi.on("session_start", async (_event, ctx) => {
390
+ activeCtx = ctx;
391
+ if (state.enabled) {
392
+ ensurePolling();
393
+ }
394
+ await refresh(ctx);
395
+ });
396
+ pi.on("session_switch", async (_event, ctx) => {
397
+ activeCtx = ctx;
398
+ if (state.enabled) {
399
+ ensurePolling();
400
+ }
401
+ await refresh(ctx);
402
+ });
403
+ pi.on("session_shutdown", async () => {
404
+ stopPolling();
405
+ activeCtx = null;
406
+ });
407
+ registerMuSubcommand(pi, {
408
+ subcommand: "subagents",
409
+ summary: "Monitor tmux subagent sessions + issue queue, and spawn ready issue sessions",
410
+ usage: "/mu subagents on|off|toggle|status|refresh|prefix|root|role|spawn",
411
+ handler: async (args, ctx) => {
412
+ activeCtx = ctx;
413
+ const tokens = args
414
+ .trim()
415
+ .split(/\s+/)
416
+ .filter((token) => token.length > 0);
417
+ if (tokens.length === 0 || tokens[0] === "status") {
418
+ const when = state.lastUpdatedMs == null ? "never" : new Date(state.lastUpdatedMs).toLocaleTimeString();
419
+ const status = state.enabled ? "enabled" : "disabled";
420
+ const issueScope = state.issueRootId ?? "(all roots)";
421
+ const issueRole = state.issueRoleTag ?? "(all roles)";
422
+ const issueError = state.issueError ? `\nissue_error: ${state.issueError}` : "";
423
+ const tmuxError = state.sessionError ? `\ntmux_error: ${state.sessionError}` : "";
424
+ ctx.ui.notify([
425
+ `Subagents monitor ${status}`,
426
+ `prefix: ${state.prefix || "(all sessions)"}`,
427
+ `issue_root: ${issueScope}`,
428
+ `issue_role: ${issueRole}`,
429
+ `sessions: ${state.sessions.length}`,
430
+ `ready_issues: ${state.readyIssues.length}`,
431
+ `active_issues: ${state.activeIssues.length}`,
432
+ `last refresh: ${when}`,
433
+ ].join("\n") + issueError + tmuxError, state.issueError || state.sessionError ? "warning" : "info");
434
+ renderSubagentsUi(ctx, state);
435
+ return;
436
+ }
437
+ switch (tokens[0]) {
438
+ case "on":
439
+ state.enabled = true;
440
+ ensurePolling();
441
+ await refresh(ctx);
442
+ ctx.ui.notify("Subagents monitor enabled.", "info");
443
+ return;
444
+ case "off":
445
+ state.enabled = false;
446
+ stopPolling();
447
+ renderSubagentsUi(ctx, state);
448
+ ctx.ui.notify("Subagents monitor disabled.", "info");
449
+ return;
450
+ case "toggle":
451
+ state.enabled = !state.enabled;
452
+ if (state.enabled) {
453
+ ensurePolling();
454
+ await refresh(ctx);
455
+ }
456
+ else {
457
+ stopPolling();
458
+ renderSubagentsUi(ctx, state);
459
+ }
460
+ ctx.ui.notify(`Subagents monitor ${state.enabled ? "enabled" : "disabled"}.`, "info");
461
+ return;
462
+ case "refresh": {
463
+ await refresh(ctx);
464
+ ctx.ui.notify("Subagents monitor refreshed.", "info");
465
+ return;
466
+ }
467
+ case "prefix": {
468
+ const value = tokens.slice(1).join(" ").trim();
469
+ if (!value) {
470
+ notify(ctx, "Missing prefix value.", "error");
471
+ return;
472
+ }
473
+ state.prefix = value.toLowerCase() === "clear" ? "" : value;
474
+ state.enabled = true;
475
+ ensurePolling();
476
+ await refresh(ctx);
477
+ ctx.ui.notify(`Subagents prefix set to ${state.prefix || "(all sessions)"}.`, "info");
478
+ return;
479
+ }
480
+ case "root": {
481
+ const value = tokens.slice(1).join(" ").trim();
482
+ if (!value) {
483
+ notify(ctx, "Missing root issue id.", "error");
484
+ return;
485
+ }
486
+ state.issueRootId = value.toLowerCase() === "clear" ? null : value;
487
+ state.enabled = true;
488
+ ensurePolling();
489
+ await refresh(ctx);
490
+ ctx.ui.notify(`Subagents root set to ${state.issueRootId ?? "(all roots)"}.`, "info");
491
+ return;
492
+ }
493
+ case "role": {
494
+ const value = tokens.slice(1).join(" ").trim();
495
+ if (!value) {
496
+ notify(ctx, "Missing role/tag value.", "error");
497
+ return;
498
+ }
499
+ state.issueRoleTag = normalizeRoleTag(value);
500
+ state.enabled = true;
501
+ ensurePolling();
502
+ await refresh(ctx);
503
+ ctx.ui.notify(`Subagents issue tag filter set to ${state.issueRoleTag ?? "(all roles)"}.`, "info");
504
+ return;
505
+ }
506
+ case "spawn": {
507
+ if (!state.issueRootId) {
508
+ notify(ctx, "Set a root first (`/mu subagents root <root-id>`) before spawning.", "error");
509
+ return;
510
+ }
511
+ let spawnLimit = null;
512
+ const limitToken = tokens[1]?.trim();
513
+ if (limitToken && limitToken.toLowerCase() !== "all") {
514
+ const parsed = Number.parseInt(limitToken, 10);
515
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > ISSUE_LIST_LIMIT) {
516
+ notify(ctx, `Spawn count must be 1-${ISSUE_LIST_LIMIT} or 'all'.`, "error");
517
+ return;
518
+ }
519
+ spawnLimit = parsed;
520
+ }
521
+ const issueSlices = await listIssueSlices(state.issueRootId, state.issueRoleTag);
522
+ state.readyIssues = issueSlices.ready;
523
+ state.activeIssues = issueSlices.active;
524
+ state.issueError = issueSlices.error;
525
+ if (issueSlices.error) {
526
+ state.enabled = true;
527
+ ensurePolling();
528
+ renderSubagentsUi(ctx, state);
529
+ notify(ctx, `Cannot spawn: ${issueSlices.error}`, "error");
530
+ return;
531
+ }
532
+ const candidates = spawnLimit == null ? issueSlices.ready : issueSlices.ready.slice(0, spawnLimit);
533
+ if (candidates.length === 0) {
534
+ state.enabled = true;
535
+ ensurePolling();
536
+ await refresh(ctx);
537
+ ctx.ui.notify("No ready issues to spawn for current root/tag filter.", "info");
538
+ return;
539
+ }
540
+ const spawnPrefix = state.prefix.length > 0 ? state.prefix : DEFAULT_PREFIX;
541
+ const tmux = await listTmuxSessions(spawnPrefix);
542
+ if (tmux.error) {
543
+ state.sessionError = tmux.error;
544
+ state.enabled = true;
545
+ ensurePolling();
546
+ renderSubagentsUi(ctx, state);
547
+ notify(ctx, `Cannot spawn: ${tmux.error}`, "error");
548
+ return;
549
+ }
550
+ const existingSessions = [...tmux.sessions];
551
+ const runId = spawnRunId();
552
+ const launched = [];
553
+ const skipped = [];
554
+ const failed = [];
555
+ for (const issue of candidates) {
556
+ if (issueHasSession(existingSessions, issue.id)) {
557
+ skipped.push(`${issue.id} (session exists)`);
558
+ continue;
559
+ }
560
+ let sessionName = `${spawnPrefix}${runId}-${issue.id}`;
561
+ if (existingSessions.includes(sessionName)) {
562
+ let suffix = 1;
563
+ while (existingSessions.includes(`${sessionName}-${suffix}`)) {
564
+ suffix += 1;
565
+ }
566
+ sessionName = `${sessionName}-${suffix}`;
567
+ }
568
+ const spawned = await spawnIssueTmuxSession({
569
+ cwd: ctx.cwd,
570
+ sessionName,
571
+ issue,
572
+ });
573
+ if (spawned.ok) {
574
+ existingSessions.push(sessionName);
575
+ launched.push(`${issue.id} -> ${sessionName}`);
576
+ }
577
+ else {
578
+ failed.push(`${issue.id} (${spawned.error ?? "unknown error"})`);
579
+ }
580
+ }
581
+ state.enabled = true;
582
+ ensurePolling();
583
+ await refresh(ctx);
584
+ const summary = [
585
+ `Spawned ${launched.length}/${candidates.length} ready issue sessions.`,
586
+ launched.length > 0 ? `launched: ${launched.join(", ")}` : "launched: (none)",
587
+ `skipped: ${skipped.length}`,
588
+ `failed: ${failed.length}`,
589
+ ];
590
+ if (failed.length > 0) {
591
+ summary.push(`failures: ${failed.join("; ")}`);
592
+ }
593
+ ctx.ui.notify(summary.join("\n"), failed.length > 0 ? "warning" : "info");
594
+ return;
595
+ }
596
+ default:
597
+ notify(ctx, `Unknown subagents command: ${tokens[0]}`, "error");
598
+ }
599
+ },
600
+ });
601
+ }
602
+ export default subagentsUiExtension;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.84",
3
+ "version": "26.2.86",
4
4
  "description": "Shared agent runtime for mu assistant sessions, orchestration roles, and serve extensions.",
5
5
  "keywords": [
6
6
  "mu",
@@ -24,7 +24,7 @@
24
24
  "themes/**"
25
25
  ],
26
26
  "dependencies": {
27
- "@femtomc/mu-core": "26.2.84",
27
+ "@femtomc/mu-core": "26.2.86",
28
28
  "@mariozechner/pi-agent-core": "^0.53.0",
29
29
  "@mariozechner/pi-ai": "^0.53.0",
30
30
  "@mariozechner/pi-coding-agent": "^0.53.0",
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: planning
3
+ description: Investigate first, then propose a concrete issue plan and refine it with the user until approved.
4
+ ---
5
+
6
+ # Planning
7
+
8
+ Use this skill when the user asks for planning, decomposition, or a staged execution roadmap.
9
+
10
+ ## Core contract
11
+
12
+ 1. **Investigate first**
13
+ - Read relevant code/docs/state before proposing work.
14
+ - Avoid speculative plans when evidence is cheap to gather.
15
+
16
+ 2. **Materialize the plan in mu issues**
17
+ - Create a root planning issue and concrete child issues.
18
+ - Encode dependencies so the DAG reflects execution order.
19
+ - Add clear titles, scope, acceptance criteria, and role tags.
20
+
21
+ 3. **Present the plan to the user**
22
+ - Summarize goals, sequencing, risks, and tradeoffs.
23
+ - Include issue IDs so the user can reference exact nodes.
24
+
25
+ 4. **Iterate until user approval**
26
+ - Treat user feedback as first-class constraints.
27
+ - Update issues/dependencies and re-present deltas.
28
+ - Do not begin broad execution until the user signals satisfaction.
29
+
30
+ ## Suggested workflow
31
+
32
+ ### A) Investigation pass
33
+
34
+ ```bash
35
+ mu status --pretty
36
+ mu issues list --status open --limit 50 --pretty
37
+ mu forum read user:context --limit 50 --pretty
38
+ mu memory search --query "<topic>" --limit 30
39
+ ```
40
+
41
+ Optional planning HUD (interactive operator session):
42
+
43
+ ```text
44
+ /mu plan on
45
+ /mu plan phase investigating
46
+ ```
47
+
48
+ Also inspect repo files directly (read/bash) for implementation constraints.
49
+
50
+ ### B) Draft DAG in mu-issue
51
+
52
+ ```bash
53
+ # 1) Create root planning issue
54
+ mu issues create "<Goal>" --body "<scope + success criteria>" --tag node:root --role orchestrator --pretty
55
+
56
+ # 2) Create child work items
57
+ mu issues create "<Subtask A>" --parent <root-id> --role worker --priority 2 --pretty
58
+ mu issues create "<Subtask B>" --parent <root-id> --role worker --priority 2 --pretty
59
+
60
+ # 3) Add dependency edges where needed
61
+ mu issues dep <child-a-id> blocks <child-b-id>
62
+
63
+ # 4) Validate ready set
64
+ mu issues ready --root <root-id> --pretty
65
+ ```
66
+
67
+ ### C) Plan presentation template
68
+
69
+ - Objective
70
+ - Assumptions and constraints discovered in investigation
71
+ - Proposed issue DAG (IDs + titles + ordering)
72
+ - Risks and mitigations
73
+ - Open questions for user approval
74
+
75
+ ### D) Revision loop
76
+
77
+ - Apply feedback with `mu issues update` / `mu issues dep` / additional issues.
78
+ - Re-run `mu issues ready --root <root-id> --pretty`.
79
+ - Present a concise diff of what changed and why.
80
+
81
+ Optional HUD updates during the loop:
82
+
83
+ ```text
84
+ /mu plan root <root-id>
85
+ /mu plan phase drafting
86
+ /mu plan check 1
87
+ /mu plan phase reviewing
88
+ ```
89
+
90
+ ## Quality bar
91
+
92
+ - Every issue should be actionable and testable.
93
+ - Keep tasks small enough to complete in one focused pass.
94
+ - Explicitly call out uncertain assumptions for user confirmation.
95
+ - Prefer reversible plans and incremental checkpoints.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: reviewer
3
+ description: Run a dedicated reviewer pass in tmux and return a strict verdict with evidence.
4
+ ---
5
+
6
+ # Reviewer
7
+
8
+ Use this skill when you want a separate reviewer lane that audits work before finalizing.
9
+
10
+ ## Goals
11
+
12
+ - Isolate review from implementation context.
13
+ - Require explicit pass/fail criteria.
14
+ - Produce concrete evidence (tests, diffs, traces) for the verdict.
15
+
16
+ ## Reviewer lane pattern
17
+
18
+ ```bash
19
+ run_id="$(date +%Y%m%d-%H%M%S)"
20
+ review_session="mu-review-${run_id}"
21
+
22
+ # Start a dedicated reviewer session
23
+ # (Use the same repo and server port as the main workflow.)
24
+ tmux new-session -d -s "$review_session" \
25
+ "cd '$PWD' && mu session --new --port 3000 ; rc=\$?; echo __MU_DONE__:\$rc"
26
+ ```
27
+
28
+ Then inject a strict reviewer prompt in that tmux pane (attach or send keys) with:
29
+
30
+ - Scope under review
31
+ - Acceptance criteria
32
+ - Required checks (build/test/lint, edge cases, regressions)
33
+ - Required output format: `PASS` or `FAIL`, plus blockers and fixes
34
+
35
+ ## Suggested reviewer prompt shape
36
+
37
+ - "Act as a strict reviewer. Validate only against these acceptance criteria..."
38
+ - "Run the necessary checks and cite concrete evidence."
39
+ - "Return: verdict, evidence, risk list, and required fixes."
40
+
41
+ ## Monitoring and collection
42
+
43
+ ```bash
44
+ tmux capture-pane -pt "$review_session" -S -300
45
+ tmux attach -t "$review_session"
46
+ ```
47
+
48
+ If the reviewer leaves open questions, ask follow-up turns in the same reviewer session:
49
+
50
+ ```bash
51
+ mu session list --json --pretty
52
+ mu turn --session-kind operator --session-id <session-id> --body "Clarify blocker #2"
53
+ ```
54
+
55
+ Use `--session-kind operator` for terminal/tmux reviewer sessions.
56
+ If omitted, `mu turn` defaults to control-plane operator sessions (`cp_operator`).
57
+
58
+ Once complete, summarize reviewer findings back into the main workflow and create follow-up issues for each blocker.
59
+
60
+ ## Safety rules
61
+
62
+ - Reviewer must not silently relax acceptance criteria.
63
+ - Prefer failing with explicit evidence over guessing.
64
+ - Keep reviewer output actionable (file paths, commands, failing checks).
65
+ - Close/kill temporary tmux sessions after review.
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: subagents
3
+ description: Break work into issue-tracked shards and dispatch mu subagents in tmux sessions.
4
+ ---
5
+
6
+ # Subagents
7
+
8
+ Use this skill when work can be split into independent shards and run concurrently.
9
+
10
+ ## When to use
11
+
12
+ - The task can be decomposed into parallelizable parts.
13
+ - Each shard can be specified with a clear prompt and bounded outcome.
14
+ - You need a durable terminal surface to inspect each shard separately.
15
+
16
+ ## Workflow
17
+
18
+ 1. Create a root issue and decompose into 2–4 actionable child issues in `mu issues`.
19
+ 2. Ensure each child has clear acceptance criteria and dependency edges.
20
+ 3. Launch one detached tmux session per ready child issue.
21
+ 4. Monitor both tmux sessions and issue queue state, then reconcile outputs.
22
+
23
+ ## Launch pattern
24
+
25
+ Issue-first decomposition (required before dispatch):
26
+
27
+ ```bash
28
+ # Root issue
29
+ mu issues create "Root: <goal>" --tag node:root --role orchestrator
30
+
31
+ # Child issues (repeat as needed)
32
+ mu issues create "<child-1 deliverable>" --parent <root-id> --role worker --priority 2
33
+ mu issues create "<child-2 deliverable>" --parent <root-id> --role worker --priority 2
34
+
35
+ # Optional ordering constraints
36
+ mu issues dep <child-1> blocks <child-2>
37
+
38
+ # Verify queue before fan-out
39
+ mu issues ready --root <root-id> --tag role:worker --pretty
40
+ ```
41
+
42
+ Dispatch one tmux subagent per ready issue id:
43
+
44
+ ```bash
45
+ run_id="$(date +%Y%m%d-%H%M%S)"
46
+
47
+ # Optional: keep one shared server alive for all shards
48
+ mu serve --port 3000
49
+
50
+ for issue_id in <issue-a> <issue-b> <issue-c>; do
51
+ session="mu-sub-${run_id}-${issue_id}"
52
+ tmux new-session -d -s "$session" \
53
+ "cd '$PWD' && mu exec 'Work issue ${issue_id}. First: mu issues claim ${issue_id}. Keep forum updates on issue:${issue_id}. Close when complete.' ; rc=\$?; echo __MU_DONE__:\$rc"
54
+ done
55
+ ```
56
+
57
+ Use `mu exec` for lightweight one-shot subagent work.
58
+ If you need queued orchestration runs, use `mu runs start ...` / `mu run ...` instead.
59
+
60
+ ## Monitoring
61
+
62
+ ```bash
63
+ tmux ls | rg '^mu-sub-'
64
+ tmux capture-pane -pt mu-sub-<run-id>-<issue-id> -S -200
65
+ tmux attach -t mu-sub-<run-id>-<issue-id>
66
+
67
+ # Issue queue visibility (same root used for dispatch)
68
+ mu issues ready --root <root-id> --tag role:worker --pretty
69
+ mu issues list --root <root-id> --status in_progress --tag role:worker --pretty
70
+ ```
71
+
72
+ Optional live monitor widget (interactive operator session):
73
+
74
+ ```text
75
+ /mu subagents on
76
+ /mu subagents prefix mu-sub-
77
+ /mu subagents root <root-id>
78
+ /mu subagents role role:worker
79
+ /mu subagents refresh
80
+ /mu subagents spawn 3
81
+ ```
82
+
83
+ The widget picks up tracker decomposition by reading `mu issues ready` and
84
+ `mu issues list --status in_progress`.
85
+ Use `spawn` to launch tmux sessions directly from the ready queue for the
86
+ current root/tag filter.
87
+
88
+ ## Handoffs and follow-up turns
89
+
90
+ With `mu exec`, follow up by issuing another `mu exec` command in the same tmux pane
91
+ (scoped to the same issue id):
92
+
93
+ ```bash
94
+ mu exec "Continue issue <issue-id>. Address feedback: ..."
95
+ ```
96
+
97
+ If you intentionally use long-lived terminal operator sessions (`mu run`/`mu serve`),
98
+ you can hand off with `mu turn`:
99
+
100
+ ```bash
101
+ mu session list --json --pretty
102
+ mu turn --session-kind operator --session-id <session-id> --body "Follow-up question"
103
+ ```
104
+
105
+ Use `--session-kind operator` for terminal/tmux sessions.
106
+ If omitted, `mu turn` defaults to control-plane operator sessions (`cp_operator`).
107
+
108
+ ## Reconciliation checklist
109
+
110
+ - Collect outputs from each issue-owned shard.
111
+ - Confirm each claimed issue is closed with an explicit outcome.
112
+ - Identify conflicts or overlaps across child issues.
113
+ - Produce one merged plan/result with explicit decisions.
114
+ - Record follow-up tasks in `mu issues` / `mu forum`.
115
+
116
+ ## Safety rules
117
+
118
+ - Keep shard prompts scoped and explicit.
119
+ - Prefer fewer, higher-quality shards over many noisy shards.
120
+ - Do not overwrite unrelated files across shards.
121
+ - Tear down temporary tmux sessions when done.