@femtomc/mu-agent 26.2.85 → 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.85",
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.85",
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",
@@ -38,6 +38,13 @@ mu forum read user:context --limit 50 --pretty
38
38
  mu memory search --query "<topic>" --limit 30
39
39
  ```
40
40
 
41
+ Optional planning HUD (interactive operator session):
42
+
43
+ ```text
44
+ /mu plan on
45
+ /mu plan phase investigating
46
+ ```
47
+
41
48
  Also inspect repo files directly (read/bash) for implementation constraints.
42
49
 
43
50
  ### B) Draft DAG in mu-issue
@@ -71,6 +78,15 @@ mu issues ready --root <root-id> --pretty
71
78
  - Re-run `mu issues ready --root <root-id> --pretty`.
72
79
  - Present a concise diff of what changed and why.
73
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
+
74
90
  ## Quality bar
75
91
 
76
92
  - Every issue should be actionable and testable.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: subagents
3
- description: Break a task into independent parts and dispatch mu subagents in tmux sessions.
3
+ description: Break work into issue-tracked shards and dispatch mu subagents in tmux sessions.
4
4
  ---
5
5
 
6
6
  # Subagents
@@ -15,55 +15,103 @@ Use this skill when work can be split into independent shards and run concurrent
15
15
 
16
16
  ## Workflow
17
17
 
18
- 1. Decompose into 2–4 independent subtasks with explicit deliverables.
19
- 2. Launch one detached tmux session per subtask.
20
- 3. Run a `mu` invocation in each session.
21
- 4. Monitor each pane, then reconcile outputs into one final synthesis.
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
22
 
23
23
  ## Launch pattern
24
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
+
25
44
  ```bash
26
45
  run_id="$(date +%Y%m%d-%H%M%S)"
27
46
 
28
47
  # Optional: keep one shared server alive for all shards
29
48
  mu serve --port 3000
30
49
 
31
- # In another terminal, dispatch shards
32
- for shard in 1 2 3; do
33
- session="mu-sub-${run_id}-${shard}"
50
+ for issue_id in <issue-a> <issue-b> <issue-c>; do
51
+ session="mu-sub-${run_id}-${issue_id}"
34
52
  tmux new-session -d -s "$session" \
35
- "cd '$PWD' && mu run 'SUBTASK_PROMPT_${shard}' ; rc=\$?; echo __MU_DONE__:\$rc"
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"
36
54
  done
37
55
  ```
38
56
 
39
- If you need non-interactive queueing, use `mu runs start ...` in tmux windows instead of `mu run ...`.
57
+ Use `mu exec` for lightweight one-shot subagent work.
58
+ If you need queued orchestration runs, use `mu runs start ...` / `mu run ...` instead.
40
59
 
41
60
  ## Monitoring
42
61
 
43
62
  ```bash
44
63
  tmux ls | rg '^mu-sub-'
45
- tmux capture-pane -pt mu-sub-<run-id>-1 -S -200
46
- tmux attach -t mu-sub-<run-id>-1
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
47
70
  ```
48
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
+
49
88
  ## Handoffs and follow-up turns
50
89
 
51
- When a subagent session already exists, ask direct follow-ups in that same session:
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`:
52
99
 
53
100
  ```bash
54
101
  mu session list --json --pretty
55
102
  mu turn --session-kind operator --session-id <session-id> --body "Follow-up question"
56
103
  ```
57
104
 
58
- Use `--session-kind operator` for terminal/tmux subagent sessions.
105
+ Use `--session-kind operator` for terminal/tmux sessions.
59
106
  If omitted, `mu turn` defaults to control-plane operator sessions (`cp_operator`).
60
107
 
61
108
  ## Reconciliation checklist
62
109
 
63
- - Collect outputs from each shard.
64
- - Identify conflicts or overlaps.
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.
65
113
  - Produce one merged plan/result with explicit decisions.
66
- - Record any follow-up tasks in `mu issues` / `mu forum`.
114
+ - Record follow-up tasks in `mu issues` / `mu forum`.
67
115
 
68
116
  ## Safety rules
69
117