@femtomc/mu-agent 26.2.87 → 26.2.89
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 +573 -38
- package/dist/extensions/subagents-ui.d.ts.map +1 -1
- package/dist/extensions/subagents-ui.js +434 -38
- package/package.json +2 -2
- package/prompts/skills/planning/SKILL.md +71 -13
- package/prompts/skills/subagents/SKILL.md +26 -11
|
@@ -1 +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;
|
|
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;AA2uBpF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,QAwmBpD;AAED,eAAe,oBAAoB,CAAC"}
|
|
@@ -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();
|
|
@@ -283,6 +327,107 @@ function formatRefreshAge(lastUpdatedMs) {
|
|
|
283
327
|
const hours = Math.floor(mins / 60);
|
|
284
328
|
return `${hours}h ago`;
|
|
285
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(" · ");
|
|
430
|
+
}
|
|
286
431
|
function renderSubagentsUi(ctx, state) {
|
|
287
432
|
if (!ctx.hasUI) {
|
|
288
433
|
return;
|
|
@@ -294,15 +439,24 @@ function renderSubagentsUi(ctx, state) {
|
|
|
294
439
|
}
|
|
295
440
|
const issueScope = state.issueRootId ? `root:${state.issueRootId}` : "all-roots";
|
|
296
441
|
const roleScope = state.issueRoleTag ? state.issueRoleTag : "(all roles)";
|
|
297
|
-
const
|
|
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);
|
|
298
446
|
const healthColor = hasError ? "warning" : "success";
|
|
299
447
|
const healthLabel = hasError ? "degraded" : "healthy";
|
|
300
448
|
const queueTotal = state.readyIssues.length + state.activeIssues.length;
|
|
301
449
|
const queueBar = queueMeter(state.activeIssues.length, Math.max(1, queueTotal), 10);
|
|
302
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);
|
|
303
455
|
ctx.ui.setStatus("mu-subagents", [
|
|
304
456
|
ctx.ui.theme.fg("dim", "subagents"),
|
|
305
457
|
ctx.ui.theme.fg(healthColor, healthLabel),
|
|
458
|
+
ctx.ui.theme.fg("dim", `${state.spawnMode}`),
|
|
459
|
+
ctx.ui.theme.fg(pausedColor, `paused:${pausedLabel}`),
|
|
306
460
|
ctx.ui.theme.fg("dim", `${state.sessions.length} tmux`),
|
|
307
461
|
ctx.ui.theme.fg("dim", `${state.readyIssues.length} ready/${state.activeIssues.length} active`),
|
|
308
462
|
ctx.ui.theme.fg("muted", issueScope),
|
|
@@ -312,8 +466,11 @@ function renderSubagentsUi(ctx, state) {
|
|
|
312
466
|
` ${ctx.ui.theme.fg("muted", "health:")} ${ctx.ui.theme.fg(healthColor, healthLabel)}`,
|
|
313
467
|
` ${ctx.ui.theme.fg("muted", "scope:")} ${ctx.ui.theme.fg("dim", `${issueScope} · ${roleScope}`)}`,
|
|
314
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`)}`,
|
|
315
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)}`,
|
|
316
|
-
` ${ctx.ui.theme.fg("muted", "last refresh:")} ${ctx.ui.theme.fg("dim", refreshAge)}`,
|
|
473
|
+
` ${ctx.ui.theme.fg("muted", "last refresh:")} ${ctx.ui.theme.fg(refreshStale ? "warning" : "dim", refreshAge)}`,
|
|
317
474
|
` ${ctx.ui.theme.fg("dim", "────────────────────────────")}`,
|
|
318
475
|
ctx.ui.theme.fg("accent", `tmux sessions (${state.sessions.length})`),
|
|
319
476
|
];
|
|
@@ -361,40 +518,55 @@ function renderSubagentsUi(ctx, state) {
|
|
|
361
518
|
}
|
|
362
519
|
}
|
|
363
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
|
+
}
|
|
364
532
|
ctx.ui.setWidget("mu-subagents", lines, { placement: "belowEditor" });
|
|
365
533
|
}
|
|
366
534
|
function subagentsUsageText() {
|
|
367
535
|
return [
|
|
368
536
|
"Usage:",
|
|
369
|
-
" /mu subagents on|off|toggle|status|refresh",
|
|
537
|
+
" /mu subagents on|off|toggle|status|refresh|snapshot",
|
|
370
538
|
" /mu subagents prefix <text|clear>",
|
|
371
539
|
" /mu subagents root <issue-id|clear>",
|
|
372
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>",
|
|
373
545
|
" /mu subagents spawn [N|all]",
|
|
374
546
|
].join("\n");
|
|
375
547
|
}
|
|
376
|
-
function normalizeRoleTag(raw) {
|
|
377
|
-
const trimmed = raw.trim();
|
|
378
|
-
if (!trimmed || trimmed.toLowerCase() === "clear") {
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
|
-
if (trimmed === "worker" || trimmed === "orchestrator") {
|
|
382
|
-
return `role:${trimmed}`;
|
|
383
|
-
}
|
|
384
|
-
return trimmed;
|
|
385
|
-
}
|
|
386
548
|
function subagentsDetails(state) {
|
|
549
|
+
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
387
550
|
return {
|
|
388
551
|
enabled: state.enabled,
|
|
389
552
|
prefix: state.prefix,
|
|
390
553
|
issue_root_id: state.issueRootId,
|
|
391
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),
|
|
392
559
|
sessions: [...state.sessions],
|
|
393
560
|
ready_issue_ids: state.readyIssues.map((issue) => issue.id),
|
|
394
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),
|
|
395
565
|
issue_error: state.issueError,
|
|
396
566
|
session_error: state.sessionError,
|
|
397
567
|
last_updated_ms: state.lastUpdatedMs,
|
|
568
|
+
snapshot_compact: subagentsSnapshot(state, "compact"),
|
|
569
|
+
snapshot_multiline: subagentsSnapshot(state, "multiline"),
|
|
398
570
|
};
|
|
399
571
|
}
|
|
400
572
|
function subagentsToolError(message, state) {
|
|
@@ -410,7 +582,7 @@ function subagentsToolError(message, state) {
|
|
|
410
582
|
export function subagentsUiExtension(pi) {
|
|
411
583
|
let activeCtx = null;
|
|
412
584
|
let pollTimer = null;
|
|
413
|
-
|
|
585
|
+
const state = createDefaultState();
|
|
414
586
|
const refresh = async (ctx) => {
|
|
415
587
|
if (!state.enabled) {
|
|
416
588
|
renderSubagentsUi(ctx, state);
|
|
@@ -444,7 +616,14 @@ export function subagentsUiExtension(pi) {
|
|
|
444
616
|
return;
|
|
445
617
|
}
|
|
446
618
|
void refresh(activeCtx);
|
|
447
|
-
},
|
|
619
|
+
}, state.refreshIntervalMs);
|
|
620
|
+
};
|
|
621
|
+
const restartPolling = () => {
|
|
622
|
+
if (!state.enabled) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
stopPolling();
|
|
626
|
+
ensurePolling();
|
|
448
627
|
};
|
|
449
628
|
const notify = (ctx, message, level = "info") => {
|
|
450
629
|
ctx.ui.notify(`${message}\n\n${subagentsUsageText()}`, level);
|
|
@@ -454,20 +633,40 @@ export function subagentsUiExtension(pi) {
|
|
|
454
633
|
const status = state.enabled ? "enabled" : "disabled";
|
|
455
634
|
const issueScope = state.issueRootId ?? "(all roots)";
|
|
456
635
|
const issueRole = state.issueRoleTag ?? "(all roles)";
|
|
636
|
+
const drift = computeQueueDrift(state.sessions, state.activeIssues);
|
|
637
|
+
const refreshStale = isRefreshStale(state.lastUpdatedMs, state.staleAfterMs);
|
|
457
638
|
const issueError = state.issueError ? `\nissue_error: ${state.issueError}` : "";
|
|
458
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";
|
|
459
644
|
return {
|
|
460
|
-
level: state.issueError ||
|
|
645
|
+
level: state.issueError ||
|
|
646
|
+
state.sessionError ||
|
|
647
|
+
refreshStale ||
|
|
648
|
+
drift.activeWithoutSessionIds.length > 0 ||
|
|
649
|
+
drift.orphanSessions.length > 0
|
|
650
|
+
? "warning"
|
|
651
|
+
: "info",
|
|
461
652
|
text: [
|
|
462
653
|
`Subagents monitor ${status}`,
|
|
463
654
|
`prefix: ${state.prefix || "(all sessions)"}`,
|
|
464
655
|
`issue_root: ${issueScope}`,
|
|
465
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)}`,
|
|
466
661
|
`sessions: ${state.sessions.length}`,
|
|
467
662
|
`ready_issues: ${state.readyIssues.length}`,
|
|
468
663
|
`active_issues: ${state.activeIssues.length}`,
|
|
469
664
|
`last refresh: ${when}`,
|
|
470
|
-
].join("\n") +
|
|
665
|
+
].join("\n") +
|
|
666
|
+
issueError +
|
|
667
|
+
tmuxError +
|
|
668
|
+
driftInfo +
|
|
669
|
+
staleInfo,
|
|
471
670
|
};
|
|
472
671
|
};
|
|
473
672
|
const applySubagentsAction = async (params, ctx) => {
|
|
@@ -476,6 +675,10 @@ export function subagentsUiExtension(pi) {
|
|
|
476
675
|
const summary = statusSummary();
|
|
477
676
|
return { ok: true, message: summary.text, level: summary.level };
|
|
478
677
|
}
|
|
678
|
+
case "snapshot": {
|
|
679
|
+
const format = parseSnapshotFormat(params.snapshot_format);
|
|
680
|
+
return { ok: true, message: subagentsSnapshot(state, format), level: "info" };
|
|
681
|
+
}
|
|
479
682
|
case "on":
|
|
480
683
|
state.enabled = true;
|
|
481
684
|
ensurePolling();
|
|
@@ -531,9 +734,160 @@ export function subagentsUiExtension(pi) {
|
|
|
531
734
|
state.enabled = true;
|
|
532
735
|
ensurePolling();
|
|
533
736
|
await refresh(ctx);
|
|
534
|
-
return {
|
|
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" };
|
|
535
882
|
}
|
|
536
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
|
+
}
|
|
537
891
|
if (!state.issueRootId) {
|
|
538
892
|
return {
|
|
539
893
|
ok: false,
|
|
@@ -598,6 +952,7 @@ export function subagentsUiExtension(pi) {
|
|
|
598
952
|
cwd: ctx.cwd,
|
|
599
953
|
sessionName,
|
|
600
954
|
issue,
|
|
955
|
+
mode: state.spawnMode,
|
|
601
956
|
});
|
|
602
957
|
if (spawned.ok) {
|
|
603
958
|
existingSessions.push(sessionName);
|
|
@@ -611,7 +966,7 @@ export function subagentsUiExtension(pi) {
|
|
|
611
966
|
ensurePolling();
|
|
612
967
|
await refresh(ctx);
|
|
613
968
|
const summary = [
|
|
614
|
-
`Spawned ${launched.length}/${candidates.length} ready issue sessions.`,
|
|
969
|
+
`Spawned ${launched.length}/${candidates.length} ready issue sessions (mode=${state.spawnMode}).`,
|
|
615
970
|
launched.length > 0 ? `launched: ${launched.join(", ")}` : "launched: (none)",
|
|
616
971
|
`skipped: ${skipped.length}`,
|
|
617
972
|
`failed: ${failed.length}`,
|
|
@@ -644,7 +999,7 @@ export function subagentsUiExtension(pi) {
|
|
|
644
999
|
registerMuSubcommand(pi, {
|
|
645
1000
|
subcommand: "subagents",
|
|
646
1001
|
summary: "Monitor tmux subagent sessions + issue queue, and spawn ready issue sessions",
|
|
647
|
-
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",
|
|
648
1003
|
handler: async (args, ctx) => {
|
|
649
1004
|
activeCtx = ctx;
|
|
650
1005
|
const tokens = args
|
|
@@ -657,6 +1012,9 @@ export function subagentsUiExtension(pi) {
|
|
|
657
1012
|
case "status":
|
|
658
1013
|
params = { action: "status" };
|
|
659
1014
|
break;
|
|
1015
|
+
case "snapshot":
|
|
1016
|
+
params = { action: "snapshot", snapshot_format: tokens[1] };
|
|
1017
|
+
break;
|
|
660
1018
|
case "on":
|
|
661
1019
|
params = { action: "on" };
|
|
662
1020
|
break;
|
|
@@ -678,6 +1036,20 @@ export function subagentsUiExtension(pi) {
|
|
|
678
1036
|
case "role":
|
|
679
1037
|
params = { action: "set_role", role_tag: tokens.slice(1).join(" ") };
|
|
680
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;
|
|
1052
|
+
}
|
|
681
1053
|
case "spawn":
|
|
682
1054
|
params = {
|
|
683
1055
|
action: "spawn",
|
|
@@ -706,19 +1078,43 @@ export function subagentsUiExtension(pi) {
|
|
|
706
1078
|
pi.registerTool({
|
|
707
1079
|
name: "mu_subagents_hud",
|
|
708
1080
|
label: "mu subagents HUD",
|
|
709
|
-
description: "Control or inspect subagents HUD state, including tmux scope,
|
|
1081
|
+
description: "Control or inspect subagents HUD state, including tmux scope, queue filters, spawn profile, and health policies.",
|
|
710
1082
|
parameters: {
|
|
711
1083
|
type: "object",
|
|
712
1084
|
properties: {
|
|
713
1085
|
action: {
|
|
714
1086
|
type: "string",
|
|
715
|
-
enum: [
|
|
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
|
+
],
|
|
716
1104
|
},
|
|
717
1105
|
prefix: { type: "string" },
|
|
718
1106
|
root_issue_id: { type: "string" },
|
|
719
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"] },
|
|
720
1113
|
count: {
|
|
721
|
-
anyOf: [
|
|
1114
|
+
anyOf: [
|
|
1115
|
+
{ type: "integer", minimum: 1, maximum: ISSUE_LIST_LIMIT },
|
|
1116
|
+
{ type: "string", enum: ["all"] },
|
|
1117
|
+
],
|
|
722
1118
|
},
|
|
723
1119
|
},
|
|
724
1120
|
required: ["action"],
|