@femtomc/mu-agent 26.2.86 → 26.2.88
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 -4
- package/dist/extensions/planning-ui.d.ts.map +1 -1
- package/dist/extensions/planning-ui.js +728 -92
- package/dist/extensions/subagents-ui.d.ts.map +1 -1
- package/dist/extensions/subagents-ui.js +762 -223
- package/package.json +2 -2
- package/prompts/skills/planning/SKILL.md +31 -1
- package/prompts/skills/subagents/SKILL.md +36 -4
|
@@ -1,30 +1,62 @@
|
|
|
1
1
|
import { registerMuSubcommand } from "./mu-command-dispatcher.js";
|
|
2
2
|
const DEFAULT_PREFIX = "mu-sub-";
|
|
3
3
|
const DEFAULT_ROLE_TAG = "role:worker";
|
|
4
|
+
const DEFAULT_SPAWN_MODE = "worker";
|
|
4
5
|
const ISSUE_LIST_LIMIT = 40;
|
|
5
6
|
const MU_CLI_TIMEOUT_MS = 12_000;
|
|
7
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 8_000;
|
|
8
|
+
const MIN_REFRESH_SECONDS = 2;
|
|
9
|
+
const MAX_REFRESH_SECONDS = 120;
|
|
10
|
+
const DEFAULT_STALE_AFTER_MS = 60_000;
|
|
11
|
+
const MIN_STALE_SECONDS = 10;
|
|
12
|
+
const MAX_STALE_SECONDS = 3_600;
|
|
6
13
|
function shellQuote(value) {
|
|
7
14
|
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
8
15
|
}
|
|
9
16
|
function spawnRunId(now = new Date()) {
|
|
10
|
-
return now
|
|
17
|
+
return now
|
|
18
|
+
.toISOString()
|
|
19
|
+
.replaceAll(/[-:TZ.]/g, "")
|
|
20
|
+
.slice(0, 14);
|
|
21
|
+
}
|
|
22
|
+
function sessionMatchesIssue(sessionName, issueId) {
|
|
23
|
+
return (sessionName === issueId ||
|
|
24
|
+
sessionName.endsWith(`-${issueId}`) ||
|
|
25
|
+
sessionName.includes(`-${issueId}-`) ||
|
|
26
|
+
sessionName.includes(`_${issueId}`));
|
|
11
27
|
}
|
|
12
28
|
function issueHasSession(sessions, issueId) {
|
|
13
|
-
return sessions.some((
|
|
14
|
-
session.endsWith(`-${issueId}`) ||
|
|
15
|
-
session.includes(`-${issueId}-`) ||
|
|
16
|
-
session.includes(`_${issueId}`));
|
|
29
|
+
return sessions.some((sessionName) => sessionMatchesIssue(sessionName, issueId));
|
|
17
30
|
}
|
|
18
|
-
function buildSubagentPrompt(issue) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
function buildSubagentPrompt(issue, mode) {
|
|
32
|
+
switch (mode) {
|
|
33
|
+
case "worker":
|
|
34
|
+
return [
|
|
35
|
+
`Work issue ${issue.id} (${truncateOneLine(issue.title, 80)}).`,
|
|
36
|
+
`First run: mu issues claim ${issue.id}.`,
|
|
37
|
+
`Keep forum updates in topic issue:${issue.id}.`,
|
|
38
|
+
"When done, close with an explicit outcome and summary.",
|
|
39
|
+
].join(" ");
|
|
40
|
+
case "reviewer":
|
|
41
|
+
return [
|
|
42
|
+
`Review issue ${issue.id} (${truncateOneLine(issue.title, 80)}).`,
|
|
43
|
+
`First run: mu issues claim ${issue.id}.`,
|
|
44
|
+
"Validate acceptance criteria with concrete checks and evidence.",
|
|
45
|
+
"Post PASS/FAIL verdict, blockers, and required fixes in topic issue:<id>.",
|
|
46
|
+
"Close the issue with explicit outcome and review summary.",
|
|
47
|
+
].join(" ");
|
|
48
|
+
case "researcher":
|
|
49
|
+
return [
|
|
50
|
+
`Research issue ${issue.id} (${truncateOneLine(issue.title, 80)}).`,
|
|
51
|
+
`First run: mu issues claim ${issue.id}.`,
|
|
52
|
+
"Collect concrete evidence and options; call out uncertainty explicitly.",
|
|
53
|
+
`Keep findings in topic issue:${issue.id}.`,
|
|
54
|
+
"Close the issue with a concise recommendation and rationale.",
|
|
55
|
+
].join(" ");
|
|
56
|
+
}
|
|
25
57
|
}
|
|
26
58
|
async function spawnIssueTmuxSession(opts) {
|
|
27
|
-
const shellCommand = `cd ${shellQuote(opts.cwd)} && mu exec ${shellQuote(buildSubagentPrompt(opts.issue))} ; rc=$?; echo __MU_DONE__:$rc`;
|
|
59
|
+
const shellCommand = `cd ${shellQuote(opts.cwd)} && mu exec ${shellQuote(buildSubagentPrompt(opts.issue, opts.mode))} ; rc=$?; echo __MU_DONE__:$rc`;
|
|
28
60
|
let proc = null;
|
|
29
61
|
try {
|
|
30
62
|
proc = Bun.spawn({
|
|
@@ -63,6 +95,10 @@ function createDefaultState() {
|
|
|
63
95
|
activeIssues: [],
|
|
64
96
|
issueError: null,
|
|
65
97
|
lastUpdatedMs: null,
|
|
98
|
+
refreshIntervalMs: DEFAULT_REFRESH_INTERVAL_MS,
|
|
99
|
+
staleAfterMs: DEFAULT_STALE_AFTER_MS,
|
|
100
|
+
spawnPaused: false,
|
|
101
|
+
spawnMode: DEFAULT_SPAWN_MODE,
|
|
66
102
|
};
|
|
67
103
|
}
|
|
68
104
|
function truncateOneLine(input, maxLen = 68) {
|
|
@@ -164,7 +200,11 @@ async function runMuCli(args) {
|
|
|
164
200
|
timedOut = true;
|
|
165
201
|
proc?.kill();
|
|
166
202
|
}, MU_CLI_TIMEOUT_MS);
|
|
167
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
203
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
204
|
+
proc.exited,
|
|
205
|
+
readableText(proc.stdout),
|
|
206
|
+
readableText(proc.stderr),
|
|
207
|
+
]);
|
|
168
208
|
clearTimeout(timeout);
|
|
169
209
|
return {
|
|
170
210
|
exitCode,
|
|
@@ -188,7 +228,11 @@ async function listTmuxSessions(prefix) {
|
|
|
188
228
|
const message = err instanceof Error ? err.message : String(err);
|
|
189
229
|
return { sessions: [], error: `failed to launch tmux: ${message}` };
|
|
190
230
|
}
|
|
191
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
231
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
232
|
+
proc.exited,
|
|
233
|
+
readableText(proc.stdout),
|
|
234
|
+
readableText(proc.stderr),
|
|
235
|
+
]);
|
|
192
236
|
const stderrTrimmed = stderr.trim();
|
|
193
237
|
if (exitCode !== 0) {
|
|
194
238
|
const lowered = stderrTrimmed.toLowerCase();
|
|
@@ -251,10 +295,138 @@ async function listIssueSlices(rootId, roleTag) {
|
|
|
251
295
|
error: null,
|
|
252
296
|
};
|
|
253
297
|
}
|
|
254
|
-
function formatIssueLine(ctx, issue) {
|
|
298
|
+
function formatIssueLine(ctx, issue, opts = {}) {
|
|
299
|
+
const marker = opts.marker ?? "•";
|
|
300
|
+
const tone = opts.tone ?? "accent";
|
|
255
301
|
const id = ctx.ui.theme.fg("dim", issue.id);
|
|
256
302
|
const priority = ctx.ui.theme.fg("muted", `p${issue.priority}`);
|
|
257
|
-
|
|
303
|
+
const title = ctx.ui.theme.fg("text", truncateOneLine(issue.title));
|
|
304
|
+
return ` ${ctx.ui.theme.fg(tone, marker)} ${id} ${priority} ${title}`;
|
|
305
|
+
}
|
|
306
|
+
function queueMeter(value, total, width = 10) {
|
|
307
|
+
if (width <= 0 || total <= 0) {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
const clamped = Math.max(0, Math.min(total, value));
|
|
311
|
+
const full = Math.floor((clamped / total) * width);
|
|
312
|
+
const empty = Math.max(0, width - full);
|
|
313
|
+
return "█".repeat(full) + "░".repeat(empty);
|
|
314
|
+
}
|
|
315
|
+
function formatRefreshAge(lastUpdatedMs) {
|
|
316
|
+
if (lastUpdatedMs == null) {
|
|
317
|
+
return "never";
|
|
318
|
+
}
|
|
319
|
+
const deltaSec = Math.max(0, Math.round((Date.now() - lastUpdatedMs) / 1000));
|
|
320
|
+
if (deltaSec < 60) {
|
|
321
|
+
return `${deltaSec}s ago`;
|
|
322
|
+
}
|
|
323
|
+
const mins = Math.floor(deltaSec / 60);
|
|
324
|
+
if (mins < 60) {
|
|
325
|
+
return `${mins}m ago`;
|
|
326
|
+
}
|
|
327
|
+
const hours = Math.floor(mins / 60);
|
|
328
|
+
return `${hours}h ago`;
|
|
329
|
+
}
|
|
330
|
+
function isRefreshStale(lastUpdatedMs, staleAfterMs) {
|
|
331
|
+
if (lastUpdatedMs == null) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return Date.now() - lastUpdatedMs > staleAfterMs;
|
|
335
|
+
}
|
|
336
|
+
function computeQueueDrift(sessions, activeIssues) {
|
|
337
|
+
const activeWithoutSessionIds = activeIssues
|
|
338
|
+
.filter((issue) => !issueHasSession(sessions, issue.id))
|
|
339
|
+
.map((issue) => issue.id);
|
|
340
|
+
const orphanSessions = sessions.filter((sessionName) => !activeIssues.some((issue) => sessionMatchesIssue(sessionName, issue.id)));
|
|
341
|
+
return {
|
|
342
|
+
activeWithoutSessionIds,
|
|
343
|
+
orphanSessions,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function normalizeRoleTag(raw) {
|
|
347
|
+
const trimmed = raw.trim();
|
|
348
|
+
if (!trimmed || trimmed.toLowerCase() === "clear") {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
if (trimmed === "worker" || trimmed === "orchestrator" || trimmed === "reviewer" || trimmed === "researcher") {
|
|
352
|
+
return `role:${trimmed}`;
|
|
353
|
+
}
|
|
354
|
+
return trimmed;
|
|
355
|
+
}
|
|
356
|
+
function parseSpawnMode(raw) {
|
|
357
|
+
const value = raw.trim().toLowerCase();
|
|
358
|
+
if (value === "worker" || value === "reviewer" || value === "researcher") {
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function parseOnOff(raw) {
|
|
364
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
365
|
+
if (value === "on" || value === "yes" || value === "true" || value === "1") {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
if (value === "off" || value === "no" || value === "false" || value === "0") {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
function parseSnapshotFormat(raw) {
|
|
374
|
+
const value = (raw ?? "compact").trim().toLowerCase();
|
|
375
|
+
return value === "multiline" ? "multiline" : "compact";
|
|
376
|
+
}
|
|
377
|
+
function parseSecondsBounded(secondsRaw, minSeconds, maxSeconds, field) {
|
|
378
|
+
if (typeof secondsRaw !== "number" || !Number.isFinite(secondsRaw)) {
|
|
379
|
+
return { ok: false, error: `${field} must be a number.` };
|
|
380
|
+
}
|
|
381
|
+
const rounded = Math.round(secondsRaw);
|
|
382
|
+
if (rounded < minSeconds || rounded > maxSeconds) {
|
|
383
|
+
return { ok: false, error: `${field} must be ${minSeconds}-${maxSeconds} seconds.` };
|
|
384
|
+
}
|
|
385
|
+
return { ok: true, ms: rounded * 1_000 };
|
|
386
|
+
}
|
|
387
|
+
function subagentsSnapshot(state, format) {
|
|
388
|
+
const issueScope = state.issueRootId ?? "(all roots)";
|
|
389
|
+
const roleScope = state.issueRoleTag ?? "(all roles)";
|
|
390
|
+
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
391
|
+
const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
|
|
392
|
+
const staleCount = drift.activeWithoutSessionIds.length + drift.orphanSessions.length;
|
|
393
|
+
const health = state.issueError || state.sessionError || refreshStale || staleCount > 0 ? "degraded" : "healthy";
|
|
394
|
+
const refreshAge = formatRefreshAge(state.lastUpdatedMs);
|
|
395
|
+
const paused = state.spawnPaused ? "yes" : "no";
|
|
396
|
+
const refreshSeconds = Math.round(state.refreshIntervalMs / 1_000);
|
|
397
|
+
const staleAfterSeconds = Math.round(state.staleAfterMs / 1_000);
|
|
398
|
+
if (format === "multiline") {
|
|
399
|
+
return [
|
|
400
|
+
"Subagents HUD snapshot",
|
|
401
|
+
`health: ${health}`,
|
|
402
|
+
`prefix: ${state.prefix || "(all sessions)"}`,
|
|
403
|
+
`issue_root: ${issueScope}`,
|
|
404
|
+
`issue_role: ${roleScope}`,
|
|
405
|
+
`spawn_mode: ${state.spawnMode}`,
|
|
406
|
+
`spawn_paused: ${paused}`,
|
|
407
|
+
`queues: ${state.readyIssues.length} ready / ${state.activeIssues.length} active`,
|
|
408
|
+
`sessions: ${state.sessions.length}`,
|
|
409
|
+
`drift_active_without_session: ${drift.activeWithoutSessionIds.length}`,
|
|
410
|
+
`drift_orphan_sessions: ${drift.orphanSessions.length}`,
|
|
411
|
+
`refresh_age: ${refreshAge}`,
|
|
412
|
+
`refresh_stale: ${refreshStale ? "yes" : "no"}`,
|
|
413
|
+
`refresh_seconds: ${refreshSeconds}`,
|
|
414
|
+
`stale_after_seconds: ${staleAfterSeconds}`,
|
|
415
|
+
].join("\n");
|
|
416
|
+
}
|
|
417
|
+
return [
|
|
418
|
+
"HUD(subagents)",
|
|
419
|
+
`health=${health}`,
|
|
420
|
+
`root=${issueScope}`,
|
|
421
|
+
`role=${roleScope}`,
|
|
422
|
+
`mode=${state.spawnMode}`,
|
|
423
|
+
`paused=${paused}`,
|
|
424
|
+
`ready=${state.readyIssues.length}`,
|
|
425
|
+
`active=${state.activeIssues.length}`,
|
|
426
|
+
`sessions=${state.sessions.length}`,
|
|
427
|
+
`drift=${staleCount}`,
|
|
428
|
+
`refresh=${refreshAge}`,
|
|
429
|
+
].join(" · ");
|
|
258
430
|
}
|
|
259
431
|
function renderSubagentsUi(ctx, state) {
|
|
260
432
|
if (!ctx.hasUI) {
|
|
@@ -265,89 +437,152 @@ function renderSubagentsUi(ctx, state) {
|
|
|
265
437
|
ctx.ui.setWidget("mu-subagents", undefined);
|
|
266
438
|
return;
|
|
267
439
|
}
|
|
268
|
-
const issueScope = state.issueRootId ? `root
|
|
440
|
+
const issueScope = state.issueRootId ? `root:${state.issueRootId}` : "all-roots";
|
|
269
441
|
const roleScope = state.issueRoleTag ? state.issueRoleTag : "(all roles)";
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
442
|
+
const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
|
|
443
|
+
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
444
|
+
const staleCount = drift.activeWithoutSessionIds.length + drift.orphanSessions.length;
|
|
445
|
+
const hasError = Boolean(state.sessionError || state.issueError || refreshStale || staleCount > 0);
|
|
446
|
+
const healthColor = hasError ? "warning" : "success";
|
|
447
|
+
const healthLabel = hasError ? "degraded" : "healthy";
|
|
448
|
+
const queueTotal = state.readyIssues.length + state.activeIssues.length;
|
|
449
|
+
const queueBar = queueMeter(state.activeIssues.length, Math.max(1, queueTotal), 10);
|
|
450
|
+
const refreshAge = formatRefreshAge(state.lastUpdatedMs);
|
|
451
|
+
const pausedLabel = state.spawnPaused ? "yes" : "no";
|
|
452
|
+
const pausedColor = state.spawnPaused ? "warning" : "dim";
|
|
453
|
+
const refreshSeconds = Math.round(state.refreshIntervalMs / 1_000);
|
|
454
|
+
const staleAfterSeconds = Math.round(state.staleAfterMs / 1_000);
|
|
455
|
+
ctx.ui.setStatus("mu-subagents", [
|
|
456
|
+
ctx.ui.theme.fg("dim", "subagents"),
|
|
457
|
+
ctx.ui.theme.fg(healthColor, healthLabel),
|
|
458
|
+
ctx.ui.theme.fg("dim", `${state.spawnMode}`),
|
|
459
|
+
ctx.ui.theme.fg(pausedColor, `paused:${pausedLabel}`),
|
|
460
|
+
ctx.ui.theme.fg("dim", `${state.sessions.length} tmux`),
|
|
461
|
+
ctx.ui.theme.fg("dim", `${state.readyIssues.length} ready/${state.activeIssues.length} active`),
|
|
462
|
+
ctx.ui.theme.fg("muted", issueScope),
|
|
463
|
+
].join(` ${ctx.ui.theme.fg("muted", "·")} `));
|
|
277
464
|
const lines = [
|
|
278
|
-
ctx.ui.theme.fg("accent", "Subagents
|
|
279
|
-
ctx.ui.theme.fg("
|
|
280
|
-
ctx.ui.theme.fg("
|
|
465
|
+
ctx.ui.theme.fg("accent", ctx.ui.theme.bold("Subagents board")),
|
|
466
|
+
` ${ctx.ui.theme.fg("muted", "health:")} ${ctx.ui.theme.fg(healthColor, healthLabel)}`,
|
|
467
|
+
` ${ctx.ui.theme.fg("muted", "scope:")} ${ctx.ui.theme.fg("dim", `${issueScope} · ${roleScope}`)}`,
|
|
468
|
+
` ${ctx.ui.theme.fg("muted", "tmux prefix:")} ${ctx.ui.theme.fg("dim", state.prefix || "(all sessions)")}`,
|
|
469
|
+
` ${ctx.ui.theme.fg("muted", "spawn mode:")} ${ctx.ui.theme.fg("accent", state.spawnMode)}`,
|
|
470
|
+
` ${ctx.ui.theme.fg("muted", "spawn paused:")} ${ctx.ui.theme.fg(pausedColor, pausedLabel)}`,
|
|
471
|
+
` ${ctx.ui.theme.fg("muted", "refresh:")} ${ctx.ui.theme.fg("dim", `${refreshSeconds}s`)} ${ctx.ui.theme.fg("muted", "| stale after:")} ${ctx.ui.theme.fg("dim", `${staleAfterSeconds}s`)}`,
|
|
472
|
+
` ${ctx.ui.theme.fg("muted", "queues:")} ${ctx.ui.theme.fg("accent", `${state.readyIssues.length} ready`)} ${ctx.ui.theme.fg("muted", "| ")} ${ctx.ui.theme.fg("warning", `${state.activeIssues.length} active`)} ${ctx.ui.theme.fg("dim", queueBar)}`,
|
|
473
|
+
` ${ctx.ui.theme.fg("muted", "last refresh:")} ${ctx.ui.theme.fg(refreshStale ? "warning" : "dim", refreshAge)}`,
|
|
474
|
+
` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`,
|
|
475
|
+
ctx.ui.theme.fg("accent", `tmux sessions (${state.sessions.length})`),
|
|
281
476
|
];
|
|
282
477
|
if (state.sessionError) {
|
|
283
478
|
lines.push(ctx.ui.theme.fg("warning", ` tmux error: ${state.sessionError}`));
|
|
284
479
|
}
|
|
285
480
|
else if (state.sessions.length === 0) {
|
|
286
|
-
lines.push(ctx.ui.theme.fg("muted", "
|
|
481
|
+
lines.push(ctx.ui.theme.fg("muted", " (no matching sessions)"));
|
|
287
482
|
}
|
|
288
483
|
else {
|
|
289
484
|
for (const name of state.sessions.slice(0, 8)) {
|
|
290
|
-
lines.push(` ${ctx.ui.theme.fg("success", "●")} ${name}`);
|
|
485
|
+
lines.push(` ${ctx.ui.theme.fg("success", "●")} ${ctx.ui.theme.fg("text", name)}`);
|
|
291
486
|
}
|
|
292
487
|
if (state.sessions.length > 8) {
|
|
293
488
|
lines.push(ctx.ui.theme.fg("muted", ` ... +${state.sessions.length - 8} more tmux sessions`));
|
|
294
489
|
}
|
|
295
490
|
}
|
|
491
|
+
lines.push(` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`);
|
|
296
492
|
if (state.issueError) {
|
|
297
|
-
lines.push(ctx.ui.theme.fg("warning", `
|
|
493
|
+
lines.push(ctx.ui.theme.fg("warning", `issue error: ${state.issueError}`));
|
|
298
494
|
}
|
|
299
495
|
else {
|
|
300
|
-
lines.push(ctx.ui.theme.fg("accent",
|
|
496
|
+
lines.push(ctx.ui.theme.fg("accent", `ready queue (${state.readyIssues.length})`));
|
|
301
497
|
if (state.readyIssues.length === 0) {
|
|
302
498
|
lines.push(ctx.ui.theme.fg("muted", " (no ready issues)"));
|
|
303
499
|
}
|
|
304
500
|
else {
|
|
305
501
|
for (const issue of state.readyIssues.slice(0, 6)) {
|
|
306
|
-
lines.push(formatIssueLine(ctx, issue));
|
|
502
|
+
lines.push(formatIssueLine(ctx, issue, { marker: "→", tone: "accent" }));
|
|
307
503
|
}
|
|
308
504
|
if (state.readyIssues.length > 6) {
|
|
309
505
|
lines.push(ctx.ui.theme.fg("muted", ` ... +${state.readyIssues.length - 6} more ready issues`));
|
|
310
506
|
}
|
|
311
507
|
}
|
|
312
|
-
lines.push(ctx.ui.theme.fg("accent",
|
|
508
|
+
lines.push(ctx.ui.theme.fg("accent", `active queue (${state.activeIssues.length})`));
|
|
313
509
|
if (state.activeIssues.length === 0) {
|
|
314
510
|
lines.push(ctx.ui.theme.fg("muted", " (no in-progress issues)"));
|
|
315
511
|
}
|
|
316
512
|
else {
|
|
317
513
|
for (const issue of state.activeIssues.slice(0, 6)) {
|
|
318
|
-
lines.push(formatIssueLine(ctx, issue));
|
|
514
|
+
lines.push(formatIssueLine(ctx, issue, { marker: "●", tone: "warning" }));
|
|
319
515
|
}
|
|
320
516
|
if (state.activeIssues.length > 6) {
|
|
321
517
|
lines.push(ctx.ui.theme.fg("muted", ` ... +${state.activeIssues.length - 6} more active issues`));
|
|
322
518
|
}
|
|
323
519
|
}
|
|
324
520
|
}
|
|
521
|
+
if (refreshStale) {
|
|
522
|
+
lines.push(ctx.ui.theme.fg("warning", `refresh warning: last successful refresh is stale (>${staleAfterSeconds}s)`));
|
|
523
|
+
}
|
|
524
|
+
if (drift.activeWithoutSessionIds.length > 0) {
|
|
525
|
+
lines.push(ctx.ui.theme.fg("warning", `drift warning: active issues without tmux sessions (${drift.activeWithoutSessionIds.length})`));
|
|
526
|
+
lines.push(ctx.ui.theme.fg("warning", ` missing sessions for: ${drift.activeWithoutSessionIds.slice(0, 8).join(", ")}${drift.activeWithoutSessionIds.length > 8 ? " ..." : ""}`));
|
|
527
|
+
}
|
|
528
|
+
if (drift.orphanSessions.length > 0) {
|
|
529
|
+
lines.push(ctx.ui.theme.fg("warning", `drift warning: tmux sessions without active issues (${drift.orphanSessions.length})`));
|
|
530
|
+
lines.push(ctx.ui.theme.fg("warning", ` orphan sessions: ${drift.orphanSessions.slice(0, 8).join(", ")}${drift.orphanSessions.length > 8 ? " ..." : ""}`));
|
|
531
|
+
}
|
|
325
532
|
ctx.ui.setWidget("mu-subagents", lines, { placement: "belowEditor" });
|
|
326
533
|
}
|
|
327
534
|
function subagentsUsageText() {
|
|
328
535
|
return [
|
|
329
536
|
"Usage:",
|
|
330
|
-
" /mu subagents on|off|toggle|status|refresh",
|
|
537
|
+
" /mu subagents on|off|toggle|status|refresh|snapshot",
|
|
331
538
|
" /mu subagents prefix <text|clear>",
|
|
332
539
|
" /mu subagents root <issue-id|clear>",
|
|
333
540
|
" /mu subagents role <tag|clear>",
|
|
541
|
+
" /mu subagents mode <worker|reviewer|researcher>",
|
|
542
|
+
" /mu subagents refresh-interval <seconds>",
|
|
543
|
+
" /mu subagents stale-after <seconds>",
|
|
544
|
+
" /mu subagents pause <on|off>",
|
|
334
545
|
" /mu subagents spawn [N|all]",
|
|
335
546
|
].join("\n");
|
|
336
547
|
}
|
|
337
|
-
function
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
548
|
+
function subagentsDetails(state) {
|
|
549
|
+
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
550
|
+
return {
|
|
551
|
+
enabled: state.enabled,
|
|
552
|
+
prefix: state.prefix,
|
|
553
|
+
issue_root_id: state.issueRootId,
|
|
554
|
+
issue_role_tag: state.issueRoleTag,
|
|
555
|
+
spawn_mode: state.spawnMode,
|
|
556
|
+
spawn_paused: state.spawnPaused,
|
|
557
|
+
refresh_seconds: Math.round(state.refreshIntervalMs / 1_000),
|
|
558
|
+
stale_after_seconds: Math.round(state.staleAfterMs / 1_000),
|
|
559
|
+
sessions: [...state.sessions],
|
|
560
|
+
ready_issue_ids: state.readyIssues.map((issue) => issue.id),
|
|
561
|
+
active_issue_ids: state.activeIssues.map((issue) => issue.id),
|
|
562
|
+
active_without_session_ids: drift.activeWithoutSessionIds,
|
|
563
|
+
orphan_sessions: drift.orphanSessions,
|
|
564
|
+
refresh_stale: isRefreshStale(state.lastUpdatedMs, state.staleAfterMs),
|
|
565
|
+
issue_error: state.issueError,
|
|
566
|
+
session_error: state.sessionError,
|
|
567
|
+
last_updated_ms: state.lastUpdatedMs,
|
|
568
|
+
snapshot_compact: subagentsSnapshot(state, "compact"),
|
|
569
|
+
snapshot_multiline: subagentsSnapshot(state, "multiline"),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function subagentsToolError(message, state) {
|
|
573
|
+
return {
|
|
574
|
+
content: [{ type: "text", text: message }],
|
|
575
|
+
details: {
|
|
576
|
+
ok: false,
|
|
577
|
+
error: message,
|
|
578
|
+
...subagentsDetails(state),
|
|
579
|
+
},
|
|
580
|
+
};
|
|
346
581
|
}
|
|
347
582
|
export function subagentsUiExtension(pi) {
|
|
348
583
|
let activeCtx = null;
|
|
349
584
|
let pollTimer = null;
|
|
350
|
-
|
|
585
|
+
const state = createDefaultState();
|
|
351
586
|
const refresh = async (ctx) => {
|
|
352
587
|
if (!state.enabled) {
|
|
353
588
|
renderSubagentsUi(ctx, state);
|
|
@@ -381,11 +616,368 @@ export function subagentsUiExtension(pi) {
|
|
|
381
616
|
return;
|
|
382
617
|
}
|
|
383
618
|
void refresh(activeCtx);
|
|
384
|
-
},
|
|
619
|
+
}, state.refreshIntervalMs);
|
|
620
|
+
};
|
|
621
|
+
const restartPolling = () => {
|
|
622
|
+
if (!state.enabled) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
stopPolling();
|
|
626
|
+
ensurePolling();
|
|
385
627
|
};
|
|
386
628
|
const notify = (ctx, message, level = "info") => {
|
|
387
629
|
ctx.ui.notify(`${message}\n\n${subagentsUsageText()}`, level);
|
|
388
630
|
};
|
|
631
|
+
const statusSummary = () => {
|
|
632
|
+
const when = state.lastUpdatedMs == null ? "never" : new Date(state.lastUpdatedMs).toLocaleTimeString();
|
|
633
|
+
const status = state.enabled ? "enabled" : "disabled";
|
|
634
|
+
const issueScope = state.issueRootId ?? "(all roots)";
|
|
635
|
+
const issueRole = state.issueRoleTag ?? "(all roles)";
|
|
636
|
+
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
637
|
+
const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
|
|
638
|
+
const issueError = state.issueError ? `\nissue_error: ${state.issueError}` : "";
|
|
639
|
+
const tmuxError = state.sessionError ? `\ntmux_error: ${state.sessionError}` : "";
|
|
640
|
+
const driftInfo = drift.activeWithoutSessionIds.length > 0 || drift.orphanSessions.length > 0
|
|
641
|
+
? `\ndrift_active_without_session: ${drift.activeWithoutSessionIds.length}\ndrift_orphan_sessions: ${drift.orphanSessions.length}`
|
|
642
|
+
: "";
|
|
643
|
+
const staleInfo = refreshStale ? "\nrefresh_stale: yes" : "\nrefresh_stale: no";
|
|
644
|
+
return {
|
|
645
|
+
level: state.issueError ||
|
|
646
|
+
state.sessionError ||
|
|
647
|
+
refreshStale ||
|
|
648
|
+
drift.activeWithoutSessionIds.length > 0 ||
|
|
649
|
+
drift.orphanSessions.length > 0
|
|
650
|
+
? "warning"
|
|
651
|
+
: "info",
|
|
652
|
+
text: [
|
|
653
|
+
`Subagents monitor ${status}`,
|
|
654
|
+
`prefix: ${state.prefix || "(all sessions)"}`,
|
|
655
|
+
`issue_root: ${issueScope}`,
|
|
656
|
+
`issue_role: ${issueRole}`,
|
|
657
|
+
`spawn_mode: ${state.spawnMode}`,
|
|
658
|
+
`spawn_paused: ${state.spawnPaused ? "yes" : "no"}`,
|
|
659
|
+
`refresh_seconds: ${Math.round(state.refreshIntervalMs / 1_000)}`,
|
|
660
|
+
`stale_after_seconds: ${Math.round(state.staleAfterMs / 1_000)}`,
|
|
661
|
+
`sessions: ${state.sessions.length}`,
|
|
662
|
+
`ready_issues: ${state.readyIssues.length}`,
|
|
663
|
+
`active_issues: ${state.activeIssues.length}`,
|
|
664
|
+
`last refresh: ${when}`,
|
|
665
|
+
].join("\n") +
|
|
666
|
+
issueError +
|
|
667
|
+
tmuxError +
|
|
668
|
+
driftInfo +
|
|
669
|
+
staleInfo,
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
const applySubagentsAction = async (params, ctx) => {
|
|
673
|
+
switch (params.action) {
|
|
674
|
+
case "status": {
|
|
675
|
+
const summary = statusSummary();
|
|
676
|
+
return { ok: true, message: summary.text, level: summary.level };
|
|
677
|
+
}
|
|
678
|
+
case "snapshot": {
|
|
679
|
+
const format = parseSnapshotFormat(params.snapshot_format);
|
|
680
|
+
return { ok: true, message: subagentsSnapshot(state, format), level: "info" };
|
|
681
|
+
}
|
|
682
|
+
case "on":
|
|
683
|
+
state.enabled = true;
|
|
684
|
+
ensurePolling();
|
|
685
|
+
await refresh(ctx);
|
|
686
|
+
return { ok: true, message: "Subagents monitor enabled.", level: "info" };
|
|
687
|
+
case "off":
|
|
688
|
+
state.enabled = false;
|
|
689
|
+
stopPolling();
|
|
690
|
+
renderSubagentsUi(ctx, state);
|
|
691
|
+
return { ok: true, message: "Subagents monitor disabled.", level: "info" };
|
|
692
|
+
case "toggle":
|
|
693
|
+
state.enabled = !state.enabled;
|
|
694
|
+
if (state.enabled) {
|
|
695
|
+
ensurePolling();
|
|
696
|
+
await refresh(ctx);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
stopPolling();
|
|
700
|
+
renderSubagentsUi(ctx, state);
|
|
701
|
+
}
|
|
702
|
+
return { ok: true, message: `Subagents monitor ${state.enabled ? "enabled" : "disabled"}.`, level: "info" };
|
|
703
|
+
case "refresh":
|
|
704
|
+
await refresh(ctx);
|
|
705
|
+
return { ok: true, message: "Subagents monitor refreshed.", level: "info" };
|
|
706
|
+
case "set_prefix": {
|
|
707
|
+
const value = params.prefix?.trim();
|
|
708
|
+
if (!value) {
|
|
709
|
+
return { ok: false, message: "Missing prefix value.", level: "error" };
|
|
710
|
+
}
|
|
711
|
+
state.prefix = value.toLowerCase() === "clear" ? "" : value;
|
|
712
|
+
state.enabled = true;
|
|
713
|
+
ensurePolling();
|
|
714
|
+
await refresh(ctx);
|
|
715
|
+
return { ok: true, message: `Subagents prefix set to ${state.prefix || "(all sessions)"}.`, level: "info" };
|
|
716
|
+
}
|
|
717
|
+
case "set_root": {
|
|
718
|
+
const value = params.root_issue_id?.trim();
|
|
719
|
+
if (!value) {
|
|
720
|
+
return { ok: false, message: "Missing root issue id.", level: "error" };
|
|
721
|
+
}
|
|
722
|
+
state.issueRootId = value.toLowerCase() === "clear" ? null : value;
|
|
723
|
+
state.enabled = true;
|
|
724
|
+
ensurePolling();
|
|
725
|
+
await refresh(ctx);
|
|
726
|
+
return { ok: true, message: `Subagents root set to ${state.issueRootId ?? "(all roots)"}.`, level: "info" };
|
|
727
|
+
}
|
|
728
|
+
case "set_role": {
|
|
729
|
+
const value = params.role_tag?.trim();
|
|
730
|
+
if (!value) {
|
|
731
|
+
return { ok: false, message: "Missing role/tag value.", level: "error" };
|
|
732
|
+
}
|
|
733
|
+
state.issueRoleTag = normalizeRoleTag(value);
|
|
734
|
+
state.enabled = true;
|
|
735
|
+
ensurePolling();
|
|
736
|
+
await refresh(ctx);
|
|
737
|
+
return {
|
|
738
|
+
ok: true,
|
|
739
|
+
message: `Subagents issue tag filter set to ${state.issueRoleTag ?? "(all roles)"}.`,
|
|
740
|
+
level: "info",
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
case "set_mode": {
|
|
744
|
+
const modeRaw = params.spawn_mode?.trim() ?? "";
|
|
745
|
+
const mode = parseSpawnMode(modeRaw);
|
|
746
|
+
if (!mode) {
|
|
747
|
+
return { ok: false, message: "Invalid spawn mode.", level: "error" };
|
|
748
|
+
}
|
|
749
|
+
state.spawnMode = mode;
|
|
750
|
+
state.enabled = true;
|
|
751
|
+
ensurePolling();
|
|
752
|
+
await refresh(ctx);
|
|
753
|
+
return { ok: true, message: `Subagents spawn mode set to ${mode}.`, level: "info" };
|
|
754
|
+
}
|
|
755
|
+
case "set_refresh_interval": {
|
|
756
|
+
const parsed = parseSecondsBounded(params.refresh_seconds, MIN_REFRESH_SECONDS, MAX_REFRESH_SECONDS, "refresh_seconds");
|
|
757
|
+
if (!parsed.ok) {
|
|
758
|
+
return { ok: false, message: parsed.error, level: "error" };
|
|
759
|
+
}
|
|
760
|
+
state.refreshIntervalMs = parsed.ms;
|
|
761
|
+
state.enabled = true;
|
|
762
|
+
restartPolling();
|
|
763
|
+
await refresh(ctx);
|
|
764
|
+
return {
|
|
765
|
+
ok: true,
|
|
766
|
+
message: `Subagents refresh interval set to ${Math.round(state.refreshIntervalMs / 1_000)}s.`,
|
|
767
|
+
level: "info",
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
case "set_stale_after": {
|
|
771
|
+
const parsed = parseSecondsBounded(params.stale_after_seconds, MIN_STALE_SECONDS, MAX_STALE_SECONDS, "stale_after_seconds");
|
|
772
|
+
if (!parsed.ok) {
|
|
773
|
+
return { ok: false, message: parsed.error, level: "error" };
|
|
774
|
+
}
|
|
775
|
+
state.staleAfterMs = parsed.ms;
|
|
776
|
+
state.enabled = true;
|
|
777
|
+
ensurePolling();
|
|
778
|
+
await refresh(ctx);
|
|
779
|
+
return {
|
|
780
|
+
ok: true,
|
|
781
|
+
message: `Subagents stale threshold set to ${Math.round(state.staleAfterMs / 1_000)}s.`,
|
|
782
|
+
level: "info",
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
case "set_spawn_paused": {
|
|
786
|
+
if (typeof params.spawn_paused !== "boolean") {
|
|
787
|
+
return { ok: false, message: "spawn_paused must be a boolean.", level: "error" };
|
|
788
|
+
}
|
|
789
|
+
state.spawnPaused = params.spawn_paused;
|
|
790
|
+
state.enabled = true;
|
|
791
|
+
ensurePolling();
|
|
792
|
+
await refresh(ctx);
|
|
793
|
+
return {
|
|
794
|
+
ok: true,
|
|
795
|
+
message: `Subagents spawn pause set to ${state.spawnPaused ? "on" : "off"}.`,
|
|
796
|
+
level: "info",
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
case "update": {
|
|
800
|
+
const changed = [];
|
|
801
|
+
let refreshIntervalChanged = false;
|
|
802
|
+
if (params.prefix !== undefined) {
|
|
803
|
+
if (typeof params.prefix !== "string") {
|
|
804
|
+
return { ok: false, message: "prefix must be a string.", level: "error" };
|
|
805
|
+
}
|
|
806
|
+
const trimmed = params.prefix.trim();
|
|
807
|
+
if (trimmed.length === 0) {
|
|
808
|
+
return { ok: false, message: "prefix must not be empty.", level: "error" };
|
|
809
|
+
}
|
|
810
|
+
state.prefix = trimmed.toLowerCase() === "clear" ? "" : trimmed;
|
|
811
|
+
changed.push("prefix");
|
|
812
|
+
}
|
|
813
|
+
if (params.root_issue_id !== undefined) {
|
|
814
|
+
if (typeof params.root_issue_id !== "string") {
|
|
815
|
+
return { ok: false, message: "root_issue_id must be a string.", level: "error" };
|
|
816
|
+
}
|
|
817
|
+
const trimmed = params.root_issue_id.trim();
|
|
818
|
+
if (trimmed.length === 0) {
|
|
819
|
+
return { ok: false, message: "root_issue_id must not be empty.", level: "error" };
|
|
820
|
+
}
|
|
821
|
+
state.issueRootId = trimmed.toLowerCase() === "clear" ? null : trimmed;
|
|
822
|
+
changed.push("root_issue_id");
|
|
823
|
+
}
|
|
824
|
+
if (params.role_tag !== undefined) {
|
|
825
|
+
if (typeof params.role_tag !== "string") {
|
|
826
|
+
return { ok: false, message: "role_tag must be a string.", level: "error" };
|
|
827
|
+
}
|
|
828
|
+
const trimmed = params.role_tag.trim();
|
|
829
|
+
if (trimmed.length === 0) {
|
|
830
|
+
return { ok: false, message: "role_tag must not be empty.", level: "error" };
|
|
831
|
+
}
|
|
832
|
+
state.issueRoleTag = normalizeRoleTag(trimmed);
|
|
833
|
+
changed.push("role_tag");
|
|
834
|
+
}
|
|
835
|
+
if (params.spawn_mode !== undefined) {
|
|
836
|
+
if (typeof params.spawn_mode !== "string") {
|
|
837
|
+
return { ok: false, message: "spawn_mode must be a string.", level: "error" };
|
|
838
|
+
}
|
|
839
|
+
const mode = parseSpawnMode(params.spawn_mode);
|
|
840
|
+
if (!mode) {
|
|
841
|
+
return { ok: false, message: "Invalid spawn mode.", level: "error" };
|
|
842
|
+
}
|
|
843
|
+
state.spawnMode = mode;
|
|
844
|
+
changed.push("spawn_mode");
|
|
845
|
+
}
|
|
846
|
+
if (params.refresh_seconds !== undefined) {
|
|
847
|
+
const parsed = parseSecondsBounded(params.refresh_seconds, MIN_REFRESH_SECONDS, MAX_REFRESH_SECONDS, "refresh_seconds");
|
|
848
|
+
if (!parsed.ok) {
|
|
849
|
+
return { ok: false, message: parsed.error, level: "error" };
|
|
850
|
+
}
|
|
851
|
+
state.refreshIntervalMs = parsed.ms;
|
|
852
|
+
refreshIntervalChanged = true;
|
|
853
|
+
changed.push("refresh_seconds");
|
|
854
|
+
}
|
|
855
|
+
if (params.stale_after_seconds !== undefined) {
|
|
856
|
+
const parsed = parseSecondsBounded(params.stale_after_seconds, MIN_STALE_SECONDS, MAX_STALE_SECONDS, "stale_after_seconds");
|
|
857
|
+
if (!parsed.ok) {
|
|
858
|
+
return { ok: false, message: parsed.error, level: "error" };
|
|
859
|
+
}
|
|
860
|
+
state.staleAfterMs = parsed.ms;
|
|
861
|
+
changed.push("stale_after_seconds");
|
|
862
|
+
}
|
|
863
|
+
if (params.spawn_paused !== undefined) {
|
|
864
|
+
if (typeof params.spawn_paused !== "boolean") {
|
|
865
|
+
return { ok: false, message: "spawn_paused must be a boolean.", level: "error" };
|
|
866
|
+
}
|
|
867
|
+
state.spawnPaused = params.spawn_paused;
|
|
868
|
+
changed.push("spawn_paused");
|
|
869
|
+
}
|
|
870
|
+
if (changed.length === 0) {
|
|
871
|
+
return { ok: false, message: "No update fields provided.", level: "error" };
|
|
872
|
+
}
|
|
873
|
+
state.enabled = true;
|
|
874
|
+
if (refreshIntervalChanged) {
|
|
875
|
+
restartPolling();
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
ensurePolling();
|
|
879
|
+
}
|
|
880
|
+
await refresh(ctx);
|
|
881
|
+
return { ok: true, message: `Subagents monitor updated (${changed.join(", ")}).`, level: "info" };
|
|
882
|
+
}
|
|
883
|
+
case "spawn": {
|
|
884
|
+
if (state.spawnPaused) {
|
|
885
|
+
return {
|
|
886
|
+
ok: false,
|
|
887
|
+
message: "Spawn is paused. Use set_spawn_paused=false before spawning.",
|
|
888
|
+
level: "error",
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (!state.issueRootId) {
|
|
892
|
+
return {
|
|
893
|
+
ok: false,
|
|
894
|
+
message: "Set a root first (`/mu subagents root <root-id>`) before spawning.",
|
|
895
|
+
level: "error",
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
let spawnLimit = null;
|
|
899
|
+
if (params.count != null && params.count !== "all") {
|
|
900
|
+
const countNum = typeof params.count === "number" ? params.count : Number.parseInt(String(params.count), 10);
|
|
901
|
+
const parsed = Math.trunc(countNum);
|
|
902
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > ISSUE_LIST_LIMIT) {
|
|
903
|
+
return { ok: false, message: `Spawn count must be 1-${ISSUE_LIST_LIMIT} or 'all'.`, level: "error" };
|
|
904
|
+
}
|
|
905
|
+
spawnLimit = parsed;
|
|
906
|
+
}
|
|
907
|
+
const issueSlices = await listIssueSlices(state.issueRootId, state.issueRoleTag);
|
|
908
|
+
state.readyIssues = issueSlices.ready;
|
|
909
|
+
state.activeIssues = issueSlices.active;
|
|
910
|
+
state.issueError = issueSlices.error;
|
|
911
|
+
if (issueSlices.error) {
|
|
912
|
+
state.enabled = true;
|
|
913
|
+
ensurePolling();
|
|
914
|
+
renderSubagentsUi(ctx, state);
|
|
915
|
+
return { ok: false, message: `Cannot spawn: ${issueSlices.error}`, level: "error" };
|
|
916
|
+
}
|
|
917
|
+
const candidates = spawnLimit == null ? issueSlices.ready : issueSlices.ready.slice(0, spawnLimit);
|
|
918
|
+
if (candidates.length === 0) {
|
|
919
|
+
state.enabled = true;
|
|
920
|
+
ensurePolling();
|
|
921
|
+
await refresh(ctx);
|
|
922
|
+
return { ok: true, message: "No ready issues to spawn for current root/tag filter.", level: "info" };
|
|
923
|
+
}
|
|
924
|
+
const spawnPrefix = state.prefix.length > 0 ? state.prefix : DEFAULT_PREFIX;
|
|
925
|
+
const tmux = await listTmuxSessions(spawnPrefix);
|
|
926
|
+
if (tmux.error) {
|
|
927
|
+
state.sessionError = tmux.error;
|
|
928
|
+
state.enabled = true;
|
|
929
|
+
ensurePolling();
|
|
930
|
+
renderSubagentsUi(ctx, state);
|
|
931
|
+
return { ok: false, message: `Cannot spawn: ${tmux.error}`, level: "error" };
|
|
932
|
+
}
|
|
933
|
+
const existingSessions = [...tmux.sessions];
|
|
934
|
+
const runId = spawnRunId();
|
|
935
|
+
const launched = [];
|
|
936
|
+
const skipped = [];
|
|
937
|
+
const failed = [];
|
|
938
|
+
for (const issue of candidates) {
|
|
939
|
+
if (issueHasSession(existingSessions, issue.id)) {
|
|
940
|
+
skipped.push(`${issue.id} (session exists)`);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
let sessionName = `${spawnPrefix}${runId}-${issue.id}`;
|
|
944
|
+
if (existingSessions.includes(sessionName)) {
|
|
945
|
+
let suffix = 1;
|
|
946
|
+
while (existingSessions.includes(`${sessionName}-${suffix}`)) {
|
|
947
|
+
suffix += 1;
|
|
948
|
+
}
|
|
949
|
+
sessionName = `${sessionName}-${suffix}`;
|
|
950
|
+
}
|
|
951
|
+
const spawned = await spawnIssueTmuxSession({
|
|
952
|
+
cwd: ctx.cwd,
|
|
953
|
+
sessionName,
|
|
954
|
+
issue,
|
|
955
|
+
mode: state.spawnMode,
|
|
956
|
+
});
|
|
957
|
+
if (spawned.ok) {
|
|
958
|
+
existingSessions.push(sessionName);
|
|
959
|
+
launched.push(`${issue.id} -> ${sessionName}`);
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
failed.push(`${issue.id} (${spawned.error ?? "unknown error"})`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
state.enabled = true;
|
|
966
|
+
ensurePolling();
|
|
967
|
+
await refresh(ctx);
|
|
968
|
+
const summary = [
|
|
969
|
+
`Spawned ${launched.length}/${candidates.length} ready issue sessions (mode=${state.spawnMode}).`,
|
|
970
|
+
launched.length > 0 ? `launched: ${launched.join(", ")}` : "launched: (none)",
|
|
971
|
+
`skipped: ${skipped.length}`,
|
|
972
|
+
`failed: ${failed.length}`,
|
|
973
|
+
];
|
|
974
|
+
if (failed.length > 0) {
|
|
975
|
+
summary.push(`failures: ${failed.join("; ")}`);
|
|
976
|
+
}
|
|
977
|
+
return { ok: true, message: summary.join("\n"), level: failed.length > 0 ? "warning" : "info" };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
389
981
|
pi.on("session_start", async (_event, ctx) => {
|
|
390
982
|
activeCtx = ctx;
|
|
391
983
|
if (state.enabled) {
|
|
@@ -407,195 +999,142 @@ export function subagentsUiExtension(pi) {
|
|
|
407
999
|
registerMuSubcommand(pi, {
|
|
408
1000
|
subcommand: "subagents",
|
|
409
1001
|
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",
|
|
1002
|
+
usage: "/mu subagents on|off|toggle|status|refresh|snapshot|prefix|root|role|mode|refresh-interval|stale-after|pause|spawn",
|
|
411
1003
|
handler: async (args, ctx) => {
|
|
412
1004
|
activeCtx = ctx;
|
|
413
1005
|
const tokens = args
|
|
414
1006
|
.trim()
|
|
415
1007
|
.split(/\s+/)
|
|
416
1008
|
.filter((token) => token.length > 0);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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]) {
|
|
1009
|
+
const command = tokens[0] ?? "status";
|
|
1010
|
+
let params;
|
|
1011
|
+
switch (command) {
|
|
1012
|
+
case "status":
|
|
1013
|
+
params = { action: "status" };
|
|
1014
|
+
break;
|
|
1015
|
+
case "snapshot":
|
|
1016
|
+
params = { action: "snapshot", snapshot_format: tokens[1] };
|
|
1017
|
+
break;
|
|
438
1018
|
case "on":
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
await refresh(ctx);
|
|
442
|
-
ctx.ui.notify("Subagents monitor enabled.", "info");
|
|
443
|
-
return;
|
|
1019
|
+
params = { action: "on" };
|
|
1020
|
+
break;
|
|
444
1021
|
case "off":
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
renderSubagentsUi(ctx, state);
|
|
448
|
-
ctx.ui.notify("Subagents monitor disabled.", "info");
|
|
449
|
-
return;
|
|
1022
|
+
params = { action: "off" };
|
|
1023
|
+
break;
|
|
450
1024
|
case "toggle":
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
case "
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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;
|
|
1025
|
+
params = { action: "toggle" };
|
|
1026
|
+
break;
|
|
1027
|
+
case "refresh":
|
|
1028
|
+
params = { action: "refresh" };
|
|
1029
|
+
break;
|
|
1030
|
+
case "prefix":
|
|
1031
|
+
params = { action: "set_prefix", prefix: tokens.slice(1).join(" ") };
|
|
1032
|
+
break;
|
|
1033
|
+
case "root":
|
|
1034
|
+
params = { action: "set_root", root_issue_id: tokens.slice(1).join(" ") };
|
|
1035
|
+
break;
|
|
1036
|
+
case "role":
|
|
1037
|
+
params = { action: "set_role", role_tag: tokens.slice(1).join(" ") };
|
|
1038
|
+
break;
|
|
1039
|
+
case "mode":
|
|
1040
|
+
params = { action: "set_mode", spawn_mode: tokens[1] };
|
|
1041
|
+
break;
|
|
1042
|
+
case "refresh-interval":
|
|
1043
|
+
params = { action: "set_refresh_interval", refresh_seconds: Number.parseFloat(tokens[1] ?? "") };
|
|
1044
|
+
break;
|
|
1045
|
+
case "stale-after":
|
|
1046
|
+
params = { action: "set_stale_after", stale_after_seconds: Number.parseFloat(tokens[1] ?? "") };
|
|
1047
|
+
break;
|
|
1048
|
+
case "pause": {
|
|
1049
|
+
const parsed = parseOnOff(tokens[1]);
|
|
1050
|
+
params = { action: "set_spawn_paused", spawn_paused: parsed ?? undefined };
|
|
1051
|
+
break;
|
|
505
1052
|
}
|
|
506
|
-
case "spawn":
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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;
|
|
1053
|
+
case "spawn":
|
|
1054
|
+
params = {
|
|
1055
|
+
action: "spawn",
|
|
1056
|
+
count: (() => {
|
|
1057
|
+
const token = tokens[1]?.trim();
|
|
1058
|
+
if (!token || token.toLowerCase() === "all") {
|
|
1059
|
+
return "all";
|
|
565
1060
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
}
|
|
1061
|
+
const parsed = Number.parseInt(token, 10);
|
|
1062
|
+
return Number.isFinite(parsed) ? parsed : Number.NaN;
|
|
1063
|
+
})(),
|
|
1064
|
+
};
|
|
1065
|
+
break;
|
|
596
1066
|
default:
|
|
597
|
-
notify(ctx, `Unknown subagents command: ${
|
|
1067
|
+
notify(ctx, `Unknown subagents command: ${command}`, "error");
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const result = await applySubagentsAction(params, ctx);
|
|
1071
|
+
if (!result.ok) {
|
|
1072
|
+
notify(ctx, result.message, result.level);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
ctx.ui.notify(result.message, result.level);
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
pi.registerTool({
|
|
1079
|
+
name: "mu_subagents_hud",
|
|
1080
|
+
label: "mu subagents HUD",
|
|
1081
|
+
description: "Control or inspect subagents HUD state, including tmux scope, queue filters, spawn profile, and health policies.",
|
|
1082
|
+
parameters: {
|
|
1083
|
+
type: "object",
|
|
1084
|
+
properties: {
|
|
1085
|
+
action: {
|
|
1086
|
+
type: "string",
|
|
1087
|
+
enum: [
|
|
1088
|
+
"status",
|
|
1089
|
+
"snapshot",
|
|
1090
|
+
"on",
|
|
1091
|
+
"off",
|
|
1092
|
+
"toggle",
|
|
1093
|
+
"refresh",
|
|
1094
|
+
"set_prefix",
|
|
1095
|
+
"set_root",
|
|
1096
|
+
"set_role",
|
|
1097
|
+
"set_mode",
|
|
1098
|
+
"set_refresh_interval",
|
|
1099
|
+
"set_stale_after",
|
|
1100
|
+
"set_spawn_paused",
|
|
1101
|
+
"update",
|
|
1102
|
+
"spawn",
|
|
1103
|
+
],
|
|
1104
|
+
},
|
|
1105
|
+
prefix: { type: "string" },
|
|
1106
|
+
root_issue_id: { type: "string" },
|
|
1107
|
+
role_tag: { type: "string" },
|
|
1108
|
+
spawn_mode: { type: "string", enum: ["worker", "reviewer", "researcher"] },
|
|
1109
|
+
refresh_seconds: { type: "number", minimum: MIN_REFRESH_SECONDS, maximum: MAX_REFRESH_SECONDS },
|
|
1110
|
+
stale_after_seconds: { type: "number", minimum: MIN_STALE_SECONDS, maximum: MAX_STALE_SECONDS },
|
|
1111
|
+
spawn_paused: { type: "boolean" },
|
|
1112
|
+
snapshot_format: { type: "string", enum: ["compact", "multiline"] },
|
|
1113
|
+
count: {
|
|
1114
|
+
anyOf: [
|
|
1115
|
+
{ type: "integer", minimum: 1, maximum: ISSUE_LIST_LIMIT },
|
|
1116
|
+
{ type: "string", enum: ["all"] },
|
|
1117
|
+
],
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
required: ["action"],
|
|
1121
|
+
additionalProperties: false,
|
|
1122
|
+
},
|
|
1123
|
+
execute: async (_toolCallId, paramsRaw, _signal, _onUpdate, ctx) => {
|
|
1124
|
+
activeCtx = ctx;
|
|
1125
|
+
const params = paramsRaw;
|
|
1126
|
+
const result = await applySubagentsAction(params, ctx);
|
|
1127
|
+
if (!result.ok) {
|
|
1128
|
+
return subagentsToolError(result.message, state);
|
|
598
1129
|
}
|
|
1130
|
+
return {
|
|
1131
|
+
content: [{ type: "text", text: result.message }],
|
|
1132
|
+
details: {
|
|
1133
|
+
ok: true,
|
|
1134
|
+
action: params.action,
|
|
1135
|
+
...subagentsDetails(state),
|
|
1136
|
+
},
|
|
1137
|
+
};
|
|
599
1138
|
},
|
|
600
1139
|
});
|
|
601
1140
|
}
|