@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 +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 +95 -0
- package/prompts/skills/reviewer/SKILL.md +65 -0
- package/prompts/skills/subagents/SKILL.md +121 -0
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",
|
|
@@ -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.
|