@katyella/legio 0.1.0
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/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: legio supervisor start|stop|status
|
|
3
|
+
*
|
|
4
|
+
* Manages per-project supervisor agent lifecycle. The supervisor is a persistent
|
|
5
|
+
* agent that runs at the project root (NOT in a worktree), assigned to a specific
|
|
6
|
+
* bead task, and operates at depth 1 (between coordinator and leaf workers).
|
|
7
|
+
*
|
|
8
|
+
* Unlike the coordinator:
|
|
9
|
+
* - Has a bead assignment (required via --task flag)
|
|
10
|
+
* - Has a parent agent (typically "coordinator")
|
|
11
|
+
* - Has depth 1 (default)
|
|
12
|
+
* - Multiple supervisors can run concurrently (distinguished by --name)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { deployHooks } from "../agents/hooks-deployer.ts";
|
|
18
|
+
import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
19
|
+
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
20
|
+
import { createBeadsClient } from "../beads/client.ts";
|
|
21
|
+
import { collectProviderEnv, loadConfig } from "../config.ts";
|
|
22
|
+
import { AgentError, isRunningAsRoot, ValidationError } from "../errors.ts";
|
|
23
|
+
import { openSessionStore } from "../sessions/compat.ts";
|
|
24
|
+
import type { AgentSession } from "../types.ts";
|
|
25
|
+
import {
|
|
26
|
+
createSession,
|
|
27
|
+
isSessionAlive,
|
|
28
|
+
killSession,
|
|
29
|
+
sendKeys,
|
|
30
|
+
waitForTuiReady,
|
|
31
|
+
} from "../worktree/tmux.ts";
|
|
32
|
+
|
|
33
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
await access(path);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the supervisor startup beacon.
|
|
44
|
+
*
|
|
45
|
+
* @param opts.name - Supervisor agent name
|
|
46
|
+
* @param opts.beadId - Bead task ID
|
|
47
|
+
* @param opts.depth - Hierarchy depth (default 1)
|
|
48
|
+
* @param opts.parent - Parent agent name (default "coordinator")
|
|
49
|
+
*/
|
|
50
|
+
export function buildSupervisorBeacon(opts: {
|
|
51
|
+
name: string;
|
|
52
|
+
beadId: string;
|
|
53
|
+
depth: number;
|
|
54
|
+
parent: string;
|
|
55
|
+
}): string {
|
|
56
|
+
const timestamp = new Date().toISOString();
|
|
57
|
+
const parts = [
|
|
58
|
+
`[LEGIO] ${opts.name} (supervisor) ${timestamp} task:${opts.beadId}`,
|
|
59
|
+
`Depth: ${opts.depth} | Parent: ${opts.parent} | Role: per-project supervisor`,
|
|
60
|
+
`Startup: run mulch prime, check mail (legio mail check --agent ${opts.name}), read task (bd show ${opts.beadId}), then begin supervising`,
|
|
61
|
+
];
|
|
62
|
+
return parts.join(" — ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse flags from command args.
|
|
67
|
+
*/
|
|
68
|
+
function parseFlags(args: string[]): {
|
|
69
|
+
task: string | null;
|
|
70
|
+
name: string | null;
|
|
71
|
+
parent: string;
|
|
72
|
+
depth: number;
|
|
73
|
+
json: boolean;
|
|
74
|
+
} {
|
|
75
|
+
const flags = {
|
|
76
|
+
task: null as string | null,
|
|
77
|
+
name: null as string | null,
|
|
78
|
+
parent: "coordinator",
|
|
79
|
+
depth: 1,
|
|
80
|
+
json: false,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < args.length; i++) {
|
|
84
|
+
const arg = args[i];
|
|
85
|
+
if (arg === "--task" && i + 1 < args.length) {
|
|
86
|
+
const val = args[i + 1];
|
|
87
|
+
if (val !== undefined) {
|
|
88
|
+
flags.task = val;
|
|
89
|
+
}
|
|
90
|
+
i++;
|
|
91
|
+
} else if (arg === "--name" && i + 1 < args.length) {
|
|
92
|
+
const val = args[i + 1];
|
|
93
|
+
if (val !== undefined) {
|
|
94
|
+
flags.name = val;
|
|
95
|
+
}
|
|
96
|
+
i++;
|
|
97
|
+
} else if (arg === "--parent" && i + 1 < args.length) {
|
|
98
|
+
const val = args[i + 1];
|
|
99
|
+
if (val !== undefined) {
|
|
100
|
+
flags.parent = val;
|
|
101
|
+
}
|
|
102
|
+
i++;
|
|
103
|
+
} else if (arg === "--depth" && i + 1 < args.length) {
|
|
104
|
+
const val = args[i + 1];
|
|
105
|
+
if (val !== undefined) {
|
|
106
|
+
flags.depth = Number.parseInt(val, 10);
|
|
107
|
+
}
|
|
108
|
+
i++;
|
|
109
|
+
} else if (arg === "--json") {
|
|
110
|
+
flags.json = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return flags;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Start a supervisor agent.
|
|
119
|
+
*
|
|
120
|
+
* 1. Parse flags (--task required, --name required)
|
|
121
|
+
* 2. Load config
|
|
122
|
+
* 3. Validate: name is unique in sessions, bead exists and is workable
|
|
123
|
+
* 4. Check no supervisor with same name is already running
|
|
124
|
+
* 5. Deploy hooks with capability "supervisor"
|
|
125
|
+
* 6. Create identity if first run
|
|
126
|
+
* 7. Spawn tmux session at project root with Claude Code
|
|
127
|
+
* 8. Send startup beacon
|
|
128
|
+
* 9. Record session in SessionStore (sessions.db)
|
|
129
|
+
*/
|
|
130
|
+
async function startSupervisor(args: string[]): Promise<void> {
|
|
131
|
+
const flags = parseFlags(args);
|
|
132
|
+
|
|
133
|
+
if (isRunningAsRoot()) {
|
|
134
|
+
throw new ValidationError(
|
|
135
|
+
"legio must not run as root — agent processes execute arbitrary code",
|
|
136
|
+
{
|
|
137
|
+
field: "uid",
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!flags.task) {
|
|
143
|
+
throw new ValidationError("--task <bead-id> is required", {
|
|
144
|
+
field: "task",
|
|
145
|
+
value: flags.task ?? "",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (!flags.name) {
|
|
149
|
+
throw new ValidationError("--name <name> is required", {
|
|
150
|
+
field: "name",
|
|
151
|
+
value: flags.name ?? "",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const cwd = process.cwd();
|
|
156
|
+
const config = await loadConfig(cwd);
|
|
157
|
+
const projectRoot = config.project.root;
|
|
158
|
+
|
|
159
|
+
// Validate bead exists and is workable (open or in_progress)
|
|
160
|
+
const beads = createBeadsClient(projectRoot);
|
|
161
|
+
const bead = await beads.show(flags.task);
|
|
162
|
+
if (bead.status !== "open" && bead.status !== "in_progress") {
|
|
163
|
+
throw new ValidationError(`Bead ${flags.task} is not workable (status: ${bead.status})`, {
|
|
164
|
+
field: "task",
|
|
165
|
+
value: flags.task,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for existing supervisor with same name
|
|
170
|
+
const legioDir = join(projectRoot, ".legio");
|
|
171
|
+
const { store } = openSessionStore(legioDir);
|
|
172
|
+
try {
|
|
173
|
+
const existing = store.getByName(flags.name);
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
existing &&
|
|
177
|
+
existing.capability === "supervisor" &&
|
|
178
|
+
existing.state !== "completed" &&
|
|
179
|
+
existing.state !== "zombie"
|
|
180
|
+
) {
|
|
181
|
+
const alive = await isSessionAlive(existing.tmuxSession);
|
|
182
|
+
if (alive) {
|
|
183
|
+
throw new AgentError(
|
|
184
|
+
`Supervisor '${flags.name}' is already running (tmux: ${existing.tmuxSession}, since: ${existing.startedAt})`,
|
|
185
|
+
{ agentName: flags.name },
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// Session recorded but tmux is dead — mark as completed and continue
|
|
189
|
+
store.updateState(flags.name, "completed");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Deploy supervisor-specific hooks to the project root's .claude/ directory.
|
|
193
|
+
await deployHooks(projectRoot, flags.name, "supervisor");
|
|
194
|
+
|
|
195
|
+
// Create supervisor identity if first run
|
|
196
|
+
const identityBaseDir = join(projectRoot, ".legio", "agents");
|
|
197
|
+
await mkdir(identityBaseDir, { recursive: true });
|
|
198
|
+
const existingIdentity = await loadIdentity(identityBaseDir, flags.name);
|
|
199
|
+
if (!existingIdentity) {
|
|
200
|
+
await createIdentity(identityBaseDir, {
|
|
201
|
+
name: flags.name,
|
|
202
|
+
capability: "supervisor",
|
|
203
|
+
created: new Date().toISOString(),
|
|
204
|
+
sessionsCompleted: 0,
|
|
205
|
+
expertiseDomains: config.mulch.enabled ? config.mulch.domains : [],
|
|
206
|
+
recentTasks: [],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Resolve model from config > manifest > fallback
|
|
211
|
+
const manifestLoader = createManifestLoader(
|
|
212
|
+
join(projectRoot, config.agents.manifestPath),
|
|
213
|
+
join(projectRoot, config.agents.baseDir),
|
|
214
|
+
);
|
|
215
|
+
const manifest = await manifestLoader.load();
|
|
216
|
+
const model = resolveModel(config, manifest, "supervisor", "opus");
|
|
217
|
+
|
|
218
|
+
// Build settings JSON file to skip the bypass dialog and inject the
|
|
219
|
+
// agent definition. Avoids --append-system-prompt's ERR_STREAM_DESTROYED
|
|
220
|
+
// crash with large payloads on Claude Code v2.1.50.
|
|
221
|
+
const tmuxSession = `legio-${config.project.name}-supervisor-${flags.name}`;
|
|
222
|
+
const agentDefPath = join(projectRoot, ".legio", "agent-defs", "supervisor.md");
|
|
223
|
+
const legioDir = join(projectRoot, ".legio");
|
|
224
|
+
const settings: Record<string, unknown> = { skipDangerousModePermissionPrompt: true };
|
|
225
|
+
if (await fileExists(agentDefPath)) {
|
|
226
|
+
settings.appendSystemPrompt = await readFile(agentDefPath, "utf-8");
|
|
227
|
+
}
|
|
228
|
+
const settingsPath = join(legioDir, `settings-${flags.name}.json`);
|
|
229
|
+
await writeFile(settingsPath, JSON.stringify(settings), "utf-8");
|
|
230
|
+
const claudeCmd = `claude --model ${model} --dangerously-skip-permissions --settings ${settingsPath}`;
|
|
231
|
+
const pid = await createSession(tmuxSession, projectRoot, claudeCmd, {
|
|
232
|
+
...collectProviderEnv(),
|
|
233
|
+
LEGIO_AGENT_NAME: flags.name,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Record session BEFORE the blocking beacon send so hook-triggered
|
|
237
|
+
// updateLastActivity() can find the entry.
|
|
238
|
+
const session: AgentSession = {
|
|
239
|
+
id: `session-${Date.now()}-${flags.name}`,
|
|
240
|
+
agentName: flags.name,
|
|
241
|
+
capability: "supervisor",
|
|
242
|
+
worktreePath: projectRoot, // Supervisor uses project root, not a worktree
|
|
243
|
+
branchName: config.project.canonicalBranch, // Operates on canonical branch
|
|
244
|
+
beadId: flags.task,
|
|
245
|
+
tmuxSession,
|
|
246
|
+
state: "booting",
|
|
247
|
+
pid,
|
|
248
|
+
parentAgent: flags.parent,
|
|
249
|
+
depth: flags.depth,
|
|
250
|
+
runId: null,
|
|
251
|
+
startedAt: new Date().toISOString(),
|
|
252
|
+
lastActivity: new Date().toISOString(),
|
|
253
|
+
escalationLevel: 0,
|
|
254
|
+
stalledSince: null,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
store.upsert(session);
|
|
258
|
+
|
|
259
|
+
// Write output BEFORE the blocking sleep+sendKeys so that callers
|
|
260
|
+
// reading stdout (e.g., runLegio in the server) get the response
|
|
261
|
+
// immediately and don't hang waiting for the pipe to close.
|
|
262
|
+
const output = {
|
|
263
|
+
agentName: flags.name,
|
|
264
|
+
capability: "supervisor",
|
|
265
|
+
tmuxSession,
|
|
266
|
+
projectRoot,
|
|
267
|
+
beadId: flags.task,
|
|
268
|
+
parent: flags.parent,
|
|
269
|
+
depth: flags.depth,
|
|
270
|
+
pid,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (flags.json) {
|
|
274
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
275
|
+
} else {
|
|
276
|
+
process.stdout.write(`Supervisor '${flags.name}' started\n`);
|
|
277
|
+
process.stdout.write(` Tmux: ${tmuxSession}\n`);
|
|
278
|
+
process.stdout.write(` Root: ${projectRoot}\n`);
|
|
279
|
+
process.stdout.write(` Task: ${flags.task}\n`);
|
|
280
|
+
process.stdout.write(` Parent: ${flags.parent}\n`);
|
|
281
|
+
process.stdout.write(` Depth: ${flags.depth}\n`);
|
|
282
|
+
process.stdout.write(` PID: ${pid}\n`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Wait for Claude Code's TUI to render before sending beacon.
|
|
286
|
+
await waitForTuiReady(tmuxSession);
|
|
287
|
+
const beacon = buildSupervisorBeacon({
|
|
288
|
+
name: flags.name,
|
|
289
|
+
beadId: flags.task,
|
|
290
|
+
depth: flags.depth,
|
|
291
|
+
parent: flags.parent,
|
|
292
|
+
});
|
|
293
|
+
await sendKeys(tmuxSession, beacon);
|
|
294
|
+
|
|
295
|
+
// Follow-up Enter to ensure submission
|
|
296
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
297
|
+
await sendKeys(tmuxSession, "");
|
|
298
|
+
} finally {
|
|
299
|
+
store.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Stop a supervisor agent.
|
|
305
|
+
*
|
|
306
|
+
* 1. Find the active supervisor session by name
|
|
307
|
+
* 2. Kill the tmux session (with process tree cleanup)
|
|
308
|
+
* 3. Mark session as completed in SessionStore
|
|
309
|
+
*/
|
|
310
|
+
async function stopSupervisor(args: string[]): Promise<void> {
|
|
311
|
+
const flags = parseFlags(args);
|
|
312
|
+
|
|
313
|
+
if (!flags.name) {
|
|
314
|
+
throw new ValidationError("--name <name> is required", {
|
|
315
|
+
field: "name",
|
|
316
|
+
value: flags.name ?? "",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const cwd = process.cwd();
|
|
321
|
+
const config = await loadConfig(cwd);
|
|
322
|
+
const projectRoot = config.project.root;
|
|
323
|
+
|
|
324
|
+
const legioDir = join(projectRoot, ".legio");
|
|
325
|
+
const { store } = openSessionStore(legioDir);
|
|
326
|
+
try {
|
|
327
|
+
const session = store.getByName(flags.name);
|
|
328
|
+
|
|
329
|
+
if (
|
|
330
|
+
!session ||
|
|
331
|
+
session.capability !== "supervisor" ||
|
|
332
|
+
session.state === "completed" ||
|
|
333
|
+
session.state === "zombie"
|
|
334
|
+
) {
|
|
335
|
+
throw new AgentError(`No active supervisor session found for '${flags.name}'`, {
|
|
336
|
+
agentName: flags.name,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Kill tmux session with process tree cleanup
|
|
341
|
+
const alive = await isSessionAlive(session.tmuxSession);
|
|
342
|
+
if (alive) {
|
|
343
|
+
await killSession(session.tmuxSession);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update session state
|
|
347
|
+
store.updateState(flags.name, "completed");
|
|
348
|
+
store.updateLastActivity(flags.name);
|
|
349
|
+
|
|
350
|
+
if (flags.json) {
|
|
351
|
+
process.stdout.write(`${JSON.stringify({ stopped: true, sessionId: session.id })}\n`);
|
|
352
|
+
} else {
|
|
353
|
+
process.stdout.write(`Supervisor '${flags.name}' stopped (session: ${session.id})\n`);
|
|
354
|
+
}
|
|
355
|
+
} finally {
|
|
356
|
+
store.close();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Show supervisor status.
|
|
362
|
+
*
|
|
363
|
+
* If --name is provided, show status for that specific supervisor.
|
|
364
|
+
* Otherwise, list all supervisors.
|
|
365
|
+
*/
|
|
366
|
+
async function statusSupervisor(args: string[]): Promise<void> {
|
|
367
|
+
const flags = parseFlags(args);
|
|
368
|
+
const cwd = process.cwd();
|
|
369
|
+
const config = await loadConfig(cwd);
|
|
370
|
+
const projectRoot = config.project.root;
|
|
371
|
+
|
|
372
|
+
const legioDir = join(projectRoot, ".legio");
|
|
373
|
+
const { store } = openSessionStore(legioDir);
|
|
374
|
+
try {
|
|
375
|
+
if (flags.name) {
|
|
376
|
+
// Show specific supervisor
|
|
377
|
+
const session = store.getByName(flags.name);
|
|
378
|
+
|
|
379
|
+
if (
|
|
380
|
+
!session ||
|
|
381
|
+
session.capability !== "supervisor" ||
|
|
382
|
+
session.state === "completed" ||
|
|
383
|
+
session.state === "zombie"
|
|
384
|
+
) {
|
|
385
|
+
if (flags.json) {
|
|
386
|
+
process.stdout.write(`${JSON.stringify({ running: false })}\n`);
|
|
387
|
+
} else {
|
|
388
|
+
process.stdout.write(`Supervisor '${flags.name}' is not running\n`);
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const alive = await isSessionAlive(session.tmuxSession);
|
|
394
|
+
|
|
395
|
+
// Reconcile state: we already filtered out completed/zombie above,
|
|
396
|
+
// so if tmux is dead this session needs to be marked as zombie.
|
|
397
|
+
if (!alive) {
|
|
398
|
+
store.updateState(flags.name, "zombie");
|
|
399
|
+
store.updateLastActivity(flags.name);
|
|
400
|
+
session.state = "zombie";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const status = {
|
|
404
|
+
running: alive,
|
|
405
|
+
sessionId: session.id,
|
|
406
|
+
agentName: session.agentName,
|
|
407
|
+
state: session.state,
|
|
408
|
+
tmuxSession: session.tmuxSession,
|
|
409
|
+
beadId: session.beadId,
|
|
410
|
+
parentAgent: session.parentAgent,
|
|
411
|
+
depth: session.depth,
|
|
412
|
+
pid: session.pid,
|
|
413
|
+
startedAt: session.startedAt,
|
|
414
|
+
lastActivity: session.lastActivity,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
if (flags.json) {
|
|
418
|
+
process.stdout.write(`${JSON.stringify(status)}\n`);
|
|
419
|
+
} else {
|
|
420
|
+
const stateLabel = alive ? "running" : session.state;
|
|
421
|
+
process.stdout.write(`Supervisor '${flags.name}': ${stateLabel}\n`);
|
|
422
|
+
process.stdout.write(` Session: ${session.id}\n`);
|
|
423
|
+
process.stdout.write(` Tmux: ${session.tmuxSession}\n`);
|
|
424
|
+
process.stdout.write(` Task: ${session.beadId}\n`);
|
|
425
|
+
process.stdout.write(` Parent: ${session.parentAgent}\n`);
|
|
426
|
+
process.stdout.write(` Depth: ${session.depth}\n`);
|
|
427
|
+
process.stdout.write(` PID: ${session.pid}\n`);
|
|
428
|
+
process.stdout.write(` Started: ${session.startedAt}\n`);
|
|
429
|
+
process.stdout.write(` Activity: ${session.lastActivity}\n`);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
// List all supervisors
|
|
433
|
+
const allSessions = store.getAll();
|
|
434
|
+
const supervisors = allSessions.filter((s) => s.capability === "supervisor");
|
|
435
|
+
|
|
436
|
+
if (supervisors.length === 0) {
|
|
437
|
+
if (flags.json) {
|
|
438
|
+
process.stdout.write(`${JSON.stringify([])}\n`);
|
|
439
|
+
} else {
|
|
440
|
+
process.stdout.write("No supervisor sessions found\n");
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const statuses = await Promise.all(
|
|
446
|
+
supervisors.map(async (session) => {
|
|
447
|
+
const alive = await isSessionAlive(session.tmuxSession);
|
|
448
|
+
|
|
449
|
+
// Reconcile state
|
|
450
|
+
if (!alive && session.state !== "completed" && session.state !== "zombie") {
|
|
451
|
+
store.updateState(session.agentName, "zombie");
|
|
452
|
+
store.updateLastActivity(session.agentName);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
agentName: session.agentName,
|
|
457
|
+
running: alive,
|
|
458
|
+
state:
|
|
459
|
+
!alive && session.state !== "completed" && session.state !== "zombie"
|
|
460
|
+
? ("zombie" as const)
|
|
461
|
+
: session.state,
|
|
462
|
+
tmuxSession: session.tmuxSession,
|
|
463
|
+
beadId: session.beadId,
|
|
464
|
+
parentAgent: session.parentAgent,
|
|
465
|
+
depth: session.depth,
|
|
466
|
+
startedAt: session.startedAt,
|
|
467
|
+
};
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (flags.json) {
|
|
472
|
+
process.stdout.write(`${JSON.stringify(statuses)}\n`);
|
|
473
|
+
} else {
|
|
474
|
+
process.stdout.write("Supervisor sessions:\n");
|
|
475
|
+
for (const status of statuses) {
|
|
476
|
+
const stateLabel = status.running ? "running" : status.state;
|
|
477
|
+
process.stdout.write(
|
|
478
|
+
` ${status.agentName}: ${stateLabel} (task: ${status.beadId}, parent: ${status.parentAgent})\n`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} finally {
|
|
484
|
+
store.close();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const SUPERVISOR_HELP = `legio supervisor — Manage per-project supervisor agents
|
|
489
|
+
|
|
490
|
+
Usage: legio supervisor <subcommand> [flags]
|
|
491
|
+
|
|
492
|
+
Subcommands:
|
|
493
|
+
start Start a supervisor (spawns Claude Code at project root)
|
|
494
|
+
stop Stop a supervisor (kills tmux session)
|
|
495
|
+
status Show supervisor state
|
|
496
|
+
|
|
497
|
+
Options (start):
|
|
498
|
+
--task <bead-id> Bead task ID (required)
|
|
499
|
+
--name <name> Unique supervisor name (required)
|
|
500
|
+
--parent <agent> Parent agent name (default: "coordinator")
|
|
501
|
+
--depth <n> Hierarchy depth (default: 1)
|
|
502
|
+
--json Output as JSON
|
|
503
|
+
|
|
504
|
+
Options (stop):
|
|
505
|
+
--name <name> Supervisor name to stop (required)
|
|
506
|
+
--json Output as JSON
|
|
507
|
+
|
|
508
|
+
Options (status):
|
|
509
|
+
--name <name> Show specific supervisor (optional, lists all if omitted)
|
|
510
|
+
--json Output as JSON
|
|
511
|
+
|
|
512
|
+
The supervisor runs at the project root (like the coordinator) but is assigned
|
|
513
|
+
to a specific bead task and operates at depth 1. Supervisors can spawn workers
|
|
514
|
+
via legio sling and coordinate their work.`;
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Entry point for `legio supervisor <subcommand>`.
|
|
518
|
+
*/
|
|
519
|
+
export async function supervisorCommand(args: string[]): Promise<void> {
|
|
520
|
+
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
521
|
+
process.stdout.write(`${SUPERVISOR_HELP}\n`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const subcommand = args[0];
|
|
526
|
+
const subArgs = args.slice(1);
|
|
527
|
+
|
|
528
|
+
switch (subcommand) {
|
|
529
|
+
case "start":
|
|
530
|
+
await startSupervisor(subArgs);
|
|
531
|
+
break;
|
|
532
|
+
case "stop":
|
|
533
|
+
await stopSupervisor(subArgs);
|
|
534
|
+
break;
|
|
535
|
+
case "status":
|
|
536
|
+
await statusSupervisor(subArgs);
|
|
537
|
+
break;
|
|
538
|
+
default:
|
|
539
|
+
throw new ValidationError(
|
|
540
|
+
`Unknown supervisor subcommand: ${subcommand}. Run 'legio supervisor --help' for usage.`,
|
|
541
|
+
{ field: "subcommand", value: subcommand },
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|