@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 +4 -0
- package/dist/extensions/index.d.ts +2 -0
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/mu-operator.d.ts.map +1 -1
- package/dist/extensions/mu-operator.js +4 -0
- package/dist/extensions/mu-serve.d.ts.map +1 -1
- package/dist/extensions/mu-serve.js +4 -0
- package/dist/extensions/planning-ui.d.ts +4 -0
- package/dist/extensions/planning-ui.d.ts.map +1 -0
- package/dist/extensions/planning-ui.js +179 -0
- package/dist/extensions/subagents-ui.d.ts +4 -0
- package/dist/extensions/subagents-ui.d.ts.map +1 -0
- package/dist/extensions/subagents-ui.js +602 -0
- package/package.json +2 -2
- package/prompts/skills/planning/SKILL.md +16 -0
- package/prompts/skills/subagents/SKILL.md +65 -17
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;
|
|
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"}
|
package/dist/extensions/index.js
CHANGED
|
@@ -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;
|
|
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;
|
|
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 @@
|
|
|
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 @@
|
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
19
|
-
2.
|
|
20
|
-
3.
|
|
21
|
-
4. Monitor
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
46
|
-
tmux attach -t mu-sub-<run-id
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
|
114
|
+
- Record follow-up tasks in `mu issues` / `mu forum`.
|
|
67
115
|
|
|
68
116
|
## Safety rules
|
|
69
117
|
|