@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.
Files changed (219) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +555 -0
  4. package/agents/builder.md +141 -0
  5. package/agents/coordinator.md +351 -0
  6. package/agents/cto.md +196 -0
  7. package/agents/gateway.md +276 -0
  8. package/agents/lead.md +281 -0
  9. package/agents/merger.md +156 -0
  10. package/agents/monitor.md +212 -0
  11. package/agents/reviewer.md +142 -0
  12. package/agents/scout.md +131 -0
  13. package/agents/supervisor.md +416 -0
  14. package/bin/legio.mjs +38 -0
  15. package/package.json +77 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +102 -0
  18. package/src/agents/hooks-deployer.test.ts +1820 -0
  19. package/src/agents/hooks-deployer.ts +574 -0
  20. package/src/agents/identity.test.ts +614 -0
  21. package/src/agents/identity.ts +385 -0
  22. package/src/agents/lifecycle.test.ts +202 -0
  23. package/src/agents/lifecycle.ts +184 -0
  24. package/src/agents/manifest.test.ts +558 -0
  25. package/src/agents/manifest.ts +297 -0
  26. package/src/agents/overlay.test.ts +592 -0
  27. package/src/agents/overlay.ts +316 -0
  28. package/src/beads/client.test.ts +210 -0
  29. package/src/beads/client.ts +227 -0
  30. package/src/beads/molecules.test.ts +320 -0
  31. package/src/beads/molecules.ts +209 -0
  32. package/src/commands/agents.test.ts +325 -0
  33. package/src/commands/agents.ts +286 -0
  34. package/src/commands/clean.test.ts +730 -0
  35. package/src/commands/clean.ts +653 -0
  36. package/src/commands/completions.test.ts +346 -0
  37. package/src/commands/completions.ts +950 -0
  38. package/src/commands/coordinator.test.ts +1524 -0
  39. package/src/commands/coordinator.ts +880 -0
  40. package/src/commands/costs.test.ts +1015 -0
  41. package/src/commands/costs.ts +473 -0
  42. package/src/commands/dashboard.test.ts +94 -0
  43. package/src/commands/dashboard.ts +607 -0
  44. package/src/commands/doctor.test.ts +295 -0
  45. package/src/commands/doctor.ts +213 -0
  46. package/src/commands/down.test.ts +308 -0
  47. package/src/commands/down.ts +124 -0
  48. package/src/commands/errors.test.ts +648 -0
  49. package/src/commands/errors.ts +255 -0
  50. package/src/commands/feed.test.ts +579 -0
  51. package/src/commands/feed.ts +368 -0
  52. package/src/commands/gateway.test.ts +698 -0
  53. package/src/commands/gateway.ts +419 -0
  54. package/src/commands/group.test.ts +262 -0
  55. package/src/commands/group.ts +539 -0
  56. package/src/commands/hooks.test.ts +292 -0
  57. package/src/commands/hooks.ts +210 -0
  58. package/src/commands/init.test.ts +211 -0
  59. package/src/commands/init.ts +622 -0
  60. package/src/commands/inspect.test.ts +670 -0
  61. package/src/commands/inspect.ts +455 -0
  62. package/src/commands/log.test.ts +1556 -0
  63. package/src/commands/log.ts +752 -0
  64. package/src/commands/logs.test.ts +379 -0
  65. package/src/commands/logs.ts +544 -0
  66. package/src/commands/mail.test.ts +1726 -0
  67. package/src/commands/mail.ts +926 -0
  68. package/src/commands/merge.test.ts +676 -0
  69. package/src/commands/merge.ts +374 -0
  70. package/src/commands/metrics.test.ts +444 -0
  71. package/src/commands/metrics.ts +150 -0
  72. package/src/commands/monitor.test.ts +151 -0
  73. package/src/commands/monitor.ts +394 -0
  74. package/src/commands/nudge.test.ts +230 -0
  75. package/src/commands/nudge.ts +373 -0
  76. package/src/commands/prime.test.ts +467 -0
  77. package/src/commands/prime.ts +386 -0
  78. package/src/commands/replay.test.ts +742 -0
  79. package/src/commands/replay.ts +367 -0
  80. package/src/commands/run.test.ts +443 -0
  81. package/src/commands/run.ts +365 -0
  82. package/src/commands/server.test.ts +626 -0
  83. package/src/commands/server.ts +298 -0
  84. package/src/commands/sling.test.ts +810 -0
  85. package/src/commands/sling.ts +700 -0
  86. package/src/commands/spec.test.ts +206 -0
  87. package/src/commands/spec.ts +171 -0
  88. package/src/commands/status.test.ts +276 -0
  89. package/src/commands/status.ts +339 -0
  90. package/src/commands/stop.test.ts +357 -0
  91. package/src/commands/stop.ts +119 -0
  92. package/src/commands/supervisor.test.ts +186 -0
  93. package/src/commands/supervisor.ts +544 -0
  94. package/src/commands/trace.test.ts +746 -0
  95. package/src/commands/trace.ts +332 -0
  96. package/src/commands/up.test.ts +597 -0
  97. package/src/commands/up.ts +275 -0
  98. package/src/commands/watch.test.ts +152 -0
  99. package/src/commands/watch.ts +238 -0
  100. package/src/commands/worktree.test.ts +648 -0
  101. package/src/commands/worktree.ts +266 -0
  102. package/src/config.test.ts +496 -0
  103. package/src/config.ts +616 -0
  104. package/src/doctor/agents.test.ts +448 -0
  105. package/src/doctor/agents.ts +396 -0
  106. package/src/doctor/config-check.test.ts +184 -0
  107. package/src/doctor/config-check.ts +185 -0
  108. package/src/doctor/consistency.test.ts +645 -0
  109. package/src/doctor/consistency.ts +294 -0
  110. package/src/doctor/databases.test.ts +284 -0
  111. package/src/doctor/databases.ts +211 -0
  112. package/src/doctor/dependencies.test.ts +150 -0
  113. package/src/doctor/dependencies.ts +179 -0
  114. package/src/doctor/logs.test.ts +244 -0
  115. package/src/doctor/logs.ts +295 -0
  116. package/src/doctor/merge-queue.test.ts +210 -0
  117. package/src/doctor/merge-queue.ts +144 -0
  118. package/src/doctor/structure.test.ts +285 -0
  119. package/src/doctor/structure.ts +195 -0
  120. package/src/doctor/types.ts +37 -0
  121. package/src/doctor/version.test.ts +130 -0
  122. package/src/doctor/version.ts +131 -0
  123. package/src/e2e/chat-flow.test.ts +346 -0
  124. package/src/e2e/init-sling-lifecycle.test.ts +288 -0
  125. package/src/errors.test.ts +21 -0
  126. package/src/errors.ts +246 -0
  127. package/src/events/store.test.ts +660 -0
  128. package/src/events/store.ts +344 -0
  129. package/src/events/tool-filter.test.ts +330 -0
  130. package/src/events/tool-filter.ts +126 -0
  131. package/src/global-setup.ts +14 -0
  132. package/src/index.ts +339 -0
  133. package/src/insights/analyzer.test.ts +466 -0
  134. package/src/insights/analyzer.ts +203 -0
  135. package/src/logging/color.test.ts +118 -0
  136. package/src/logging/color.ts +71 -0
  137. package/src/logging/logger.test.ts +812 -0
  138. package/src/logging/logger.ts +266 -0
  139. package/src/logging/reporter.test.ts +258 -0
  140. package/src/logging/reporter.ts +109 -0
  141. package/src/logging/sanitizer.test.ts +190 -0
  142. package/src/logging/sanitizer.ts +57 -0
  143. package/src/mail/broadcast.test.ts +203 -0
  144. package/src/mail/broadcast.ts +92 -0
  145. package/src/mail/client.test.ts +873 -0
  146. package/src/mail/client.ts +236 -0
  147. package/src/mail/store.test.ts +815 -0
  148. package/src/mail/store.ts +402 -0
  149. package/src/merge/queue.test.ts +449 -0
  150. package/src/merge/queue.ts +262 -0
  151. package/src/merge/resolver.test.ts +1453 -0
  152. package/src/merge/resolver.ts +759 -0
  153. package/src/metrics/store.test.ts +1167 -0
  154. package/src/metrics/store.ts +511 -0
  155. package/src/metrics/summary.test.ts +397 -0
  156. package/src/metrics/summary.ts +178 -0
  157. package/src/metrics/transcript.test.ts +643 -0
  158. package/src/metrics/transcript.ts +351 -0
  159. package/src/mulch/client.test.ts +547 -0
  160. package/src/mulch/client.ts +416 -0
  161. package/src/server/audit-store.test.ts +384 -0
  162. package/src/server/audit-store.ts +257 -0
  163. package/src/server/headless.test.ts +180 -0
  164. package/src/server/headless.ts +151 -0
  165. package/src/server/index.test.ts +241 -0
  166. package/src/server/index.ts +317 -0
  167. package/src/server/public/app.js +187 -0
  168. package/src/server/public/apple-touch-icon.png +0 -0
  169. package/src/server/public/components/agent-badge.js +37 -0
  170. package/src/server/public/components/data-table.js +114 -0
  171. package/src/server/public/components/gateway-chat.js +256 -0
  172. package/src/server/public/components/issue-card.js +96 -0
  173. package/src/server/public/components/layout.js +88 -0
  174. package/src/server/public/components/message-bubble.js +120 -0
  175. package/src/server/public/components/stat-card.js +26 -0
  176. package/src/server/public/components/terminal-panel.js +140 -0
  177. package/src/server/public/favicon-16.png +0 -0
  178. package/src/server/public/favicon-32.png +0 -0
  179. package/src/server/public/favicon.ico +0 -0
  180. package/src/server/public/favicon.png +0 -0
  181. package/src/server/public/index.html +64 -0
  182. package/src/server/public/lib/api.js +35 -0
  183. package/src/server/public/lib/markdown.js +8 -0
  184. package/src/server/public/lib/preact-setup.js +8 -0
  185. package/src/server/public/lib/state.js +99 -0
  186. package/src/server/public/lib/utils.js +309 -0
  187. package/src/server/public/lib/ws.js +79 -0
  188. package/src/server/public/views/chat.js +983 -0
  189. package/src/server/public/views/costs.js +692 -0
  190. package/src/server/public/views/dashboard.js +781 -0
  191. package/src/server/public/views/gateway-chat.js +622 -0
  192. package/src/server/public/views/inspect.js +399 -0
  193. package/src/server/public/views/issues.js +470 -0
  194. package/src/server/public/views/setup.js +94 -0
  195. package/src/server/public/views/task-detail.js +422 -0
  196. package/src/server/routes.test.ts +3816 -0
  197. package/src/server/routes.ts +1964 -0
  198. package/src/server/websocket.test.ts +288 -0
  199. package/src/server/websocket.ts +196 -0
  200. package/src/sessions/compat.test.ts +109 -0
  201. package/src/sessions/compat.ts +17 -0
  202. package/src/sessions/store.test.ts +969 -0
  203. package/src/sessions/store.ts +480 -0
  204. package/src/test-helpers.test.ts +97 -0
  205. package/src/test-helpers.ts +143 -0
  206. package/src/types.ts +708 -0
  207. package/src/watchdog/daemon.test.ts +1233 -0
  208. package/src/watchdog/daemon.ts +533 -0
  209. package/src/watchdog/health.test.ts +371 -0
  210. package/src/watchdog/health.ts +248 -0
  211. package/src/watchdog/triage.test.ts +162 -0
  212. package/src/watchdog/triage.ts +193 -0
  213. package/src/worktree/manager.test.ts +444 -0
  214. package/src/worktree/manager.ts +224 -0
  215. package/src/worktree/tmux.test.ts +1238 -0
  216. package/src/worktree/tmux.ts +644 -0
  217. package/templates/CLAUDE.md.tmpl +89 -0
  218. package/templates/hooks.json.tmpl +132 -0
  219. package/templates/overlay.md.tmpl +79 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Beads molecule management helpers.
3
+ *
4
+ * Wraps `bd mol` commands via node:child_process for multi-step workflow prototypes.
5
+ * Molecules are templates with ordered steps. "Pouring" a prototype creates
6
+ * actual issues with dependencies pre-wired.
7
+ *
8
+ * Zero runtime npm dependencies — only node:child_process built-in.
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+ import { AgentError } from "../errors.ts";
13
+
14
+ // === Types ===
15
+
16
+ export interface MoleculeStep {
17
+ title: string;
18
+ type?: string;
19
+ }
20
+
21
+ export interface MoleculePrototype {
22
+ id: string;
23
+ name: string;
24
+ stepCount: number;
25
+ }
26
+
27
+ export interface ConvoyStatus {
28
+ total: number;
29
+ completed: number;
30
+ inProgress: number;
31
+ blocked: number;
32
+ }
33
+
34
+ // === Internal helpers ===
35
+
36
+ /**
37
+ * Run a shell command and capture its output.
38
+ */
39
+ async function runCommand(
40
+ cmd: string[],
41
+ cwd: string,
42
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
43
+ const [command, ...args] = cmd;
44
+ if (!command) throw new Error("Empty command");
45
+ return new Promise((resolve, reject) => {
46
+ const proc = spawn(command, args, {
47
+ cwd,
48
+ stdio: ["ignore", "pipe", "pipe"],
49
+ });
50
+ const chunks: { stdout: Buffer[]; stderr: Buffer[] } = { stdout: [], stderr: [] };
51
+ proc.stdout.on("data", (data: Buffer) => chunks.stdout.push(data));
52
+ proc.stderr.on("data", (data: Buffer) => chunks.stderr.push(data));
53
+ proc.on("error", reject);
54
+ proc.on("close", (code) => {
55
+ resolve({
56
+ stdout: Buffer.concat(chunks.stdout).toString(),
57
+ stderr: Buffer.concat(chunks.stderr).toString(),
58
+ exitCode: code ?? 1,
59
+ });
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Run a `bd` subcommand and throw on failure.
66
+ */
67
+ async function runBd(
68
+ args: string[],
69
+ cwd: string,
70
+ context: string,
71
+ ): Promise<{ stdout: string; stderr: string }> {
72
+ const { stdout, stderr, exitCode } = await runCommand(["bd", ...args], cwd);
73
+ if (exitCode !== 0) {
74
+ throw new AgentError(`bd ${context} failed (exit ${exitCode}): ${stderr.trim()}`);
75
+ }
76
+ return { stdout, stderr };
77
+ }
78
+
79
+ /**
80
+ * Parse JSON output from a bd command.
81
+ * Handles the case where output may be empty or malformed.
82
+ */
83
+ function parseJsonOutput<T>(stdout: string, context: string): T {
84
+ const trimmed = stdout.trim();
85
+ if (trimmed === "") {
86
+ throw new AgentError(`Empty output from bd ${context}`);
87
+ }
88
+ try {
89
+ return JSON.parse(trimmed) as T;
90
+ } catch {
91
+ throw new AgentError(
92
+ `Failed to parse JSON output from bd ${context}: ${trimmed.slice(0, 200)}`,
93
+ );
94
+ }
95
+ }
96
+
97
+ // === Public API ===
98
+
99
+ /**
100
+ * Create a molecule prototype with ordered steps.
101
+ *
102
+ * First creates the prototype via `bd mol create`, then adds each step
103
+ * in order via `bd mol step add`. Returns the prototype ID.
104
+ *
105
+ * @param cwd - Working directory where bd commands should run
106
+ * @param options - Prototype name and ordered steps
107
+ * @returns The molecule prototype ID
108
+ */
109
+ export async function createMoleculePrototype(
110
+ cwd: string,
111
+ options: { name: string; steps: MoleculeStep[] },
112
+ ): Promise<string> {
113
+ const { stdout } = await runBd(
114
+ ["mol", "create", "--name", options.name, "--json"],
115
+ cwd,
116
+ "mol create",
117
+ );
118
+ const result = parseJsonOutput<{ id: string }>(stdout, "mol create");
119
+ const molId = result.id;
120
+
121
+ for (const step of options.steps) {
122
+ const stepArgs = [
123
+ "mol",
124
+ "step",
125
+ "add",
126
+ molId,
127
+ "--title",
128
+ step.title,
129
+ "--type",
130
+ step.type ?? "task",
131
+ "--json",
132
+ ];
133
+ await runBd(stepArgs, cwd, `mol step add (${step.title})`);
134
+ }
135
+
136
+ return molId;
137
+ }
138
+
139
+ /**
140
+ * Pour (instantiate) a molecule prototype into actual issues.
141
+ *
142
+ * Creates issues from the prototype with dependencies pre-wired.
143
+ * Optionally applies a prefix to all created issue titles.
144
+ *
145
+ * @param cwd - Working directory where bd commands should run
146
+ * @param options - Prototype ID and optional title prefix
147
+ * @returns Array of created issue IDs
148
+ */
149
+ export async function pourMolecule(
150
+ cwd: string,
151
+ options: { prototypeId: string; prefix?: string },
152
+ ): Promise<string[]> {
153
+ const args = ["mol", "pour", options.prototypeId, "--json"];
154
+ if (options.prefix !== undefined) {
155
+ args.push("--prefix", options.prefix);
156
+ }
157
+ const { stdout } = await runBd(args, cwd, "mol pour");
158
+ const result = parseJsonOutput<{ ids: string[] }>(stdout, "mol pour");
159
+ return result.ids;
160
+ }
161
+
162
+ /**
163
+ * List all molecule prototypes.
164
+ *
165
+ * @param cwd - Working directory where bd commands should run
166
+ * @returns Array of prototype summaries
167
+ */
168
+ export async function listPrototypes(cwd: string): Promise<MoleculePrototype[]> {
169
+ const { stdout } = await runBd(["mol", "list", "--json"], cwd, "mol list");
170
+ const result = parseJsonOutput<Array<{ id: string; name: string; stepCount: number }>>(
171
+ stdout,
172
+ "mol list",
173
+ );
174
+ return result.map((entry) => ({
175
+ id: entry.id,
176
+ name: entry.name,
177
+ stepCount: entry.stepCount,
178
+ }));
179
+ }
180
+
181
+ /**
182
+ * Get the convoy status for a molecule prototype instance.
183
+ *
184
+ * Returns counts of total, completed, in-progress, and blocked issues
185
+ * that were poured from this prototype.
186
+ *
187
+ * @param cwd - Working directory where bd commands should run
188
+ * @param prototypeId - The prototype ID to check status for
189
+ * @returns Status counts for the convoy
190
+ */
191
+ export async function getConvoyStatus(cwd: string, prototypeId: string): Promise<ConvoyStatus> {
192
+ const { stdout } = await runBd(
193
+ ["mol", "status", prototypeId, "--json"],
194
+ cwd,
195
+ `mol status ${prototypeId}`,
196
+ );
197
+ const result = parseJsonOutput<{
198
+ total: number;
199
+ completed: number;
200
+ inProgress: number;
201
+ blocked: number;
202
+ }>(stdout, `mol status ${prototypeId}`);
203
+ return {
204
+ total: result.total,
205
+ completed: result.completed,
206
+ inProgress: result.inProgress,
207
+ blocked: result.blocked,
208
+ };
209
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Tests for the agents command.
3
+ */
4
+
5
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import { createSessionStore } from "../sessions/store.ts";
10
+ import type { AgentSession } from "../types.ts";
11
+ import { agentsCommand, discoverAgents, extractFileScope } from "./agents.ts";
12
+
13
+ describe("extractFileScope", () => {
14
+ let tempDir: string;
15
+
16
+ beforeEach(async () => {
17
+ tempDir = await mkdtemp(join(tmpdir(), "legio-test-"));
18
+ });
19
+
20
+ it("should return empty array when overlay doesn't exist", async () => {
21
+ const scope = await extractFileScope(tempDir);
22
+ expect(scope).toEqual([]);
23
+ });
24
+
25
+ it("should return empty array when 'No file scope restrictions'", async () => {
26
+ const overlayPath = join(tempDir, ".claude", "CLAUDE.md");
27
+ const content = `# Agent Overlay
28
+
29
+ ## File Scope (exclusive ownership)
30
+
31
+ No file scope restrictions. You may modify any file in the worktree.
32
+
33
+ ## Expertise
34
+
35
+ Some expertise here.
36
+ `;
37
+ await mkdir(join(tempDir, ".claude"), { recursive: true });
38
+ await writeFile(overlayPath, content);
39
+ const scope = await extractFileScope(tempDir);
40
+ expect(scope).toEqual([]);
41
+ });
42
+
43
+ it("should extract file paths from valid overlay", async () => {
44
+ const overlayPath = join(tempDir, ".claude", "CLAUDE.md");
45
+ const content = `# Agent Overlay
46
+
47
+ ## File Scope (exclusive ownership)
48
+
49
+ These files are yours to modify:
50
+
51
+ - \`src/commands/agents.ts\`
52
+ - \`src/commands/agents.test.ts\`
53
+ - \`src/index.ts\`
54
+
55
+ ## Expertise
56
+
57
+ Some expertise here.
58
+ `;
59
+ await mkdir(join(tempDir, ".claude"), { recursive: true });
60
+ await writeFile(overlayPath, content);
61
+ const scope = await extractFileScope(tempDir);
62
+ expect(scope).toEqual([
63
+ "src/commands/agents.ts",
64
+ "src/commands/agents.test.ts",
65
+ "src/index.ts",
66
+ ]);
67
+ });
68
+
69
+ afterEach(async () => {
70
+ await rm(tempDir, { recursive: true, force: true });
71
+ });
72
+ });
73
+
74
+ describe("discoverAgents", () => {
75
+ let tempDir: string;
76
+ let dbPath: string;
77
+
78
+ beforeEach(async () => {
79
+ tempDir = await mkdtemp(join(tmpdir(), "legio-test-"));
80
+ const legioDir = join(tempDir, ".legio");
81
+ await mkdir(legioDir, { recursive: true });
82
+ await writeFile(join(legioDir, ".gitkeep"), "");
83
+ dbPath = join(legioDir, "sessions.db");
84
+ });
85
+
86
+ it("should return empty when no sessions", async () => {
87
+ const store = createSessionStore(dbPath);
88
+ store.close();
89
+
90
+ const agents = await discoverAgents(tempDir);
91
+ expect(agents).toEqual([]);
92
+ });
93
+
94
+ it("should return active agents", async () => {
95
+ const store = createSessionStore(dbPath);
96
+
97
+ const session: AgentSession = {
98
+ id: "session-1",
99
+ agentName: "builder-test",
100
+ capability: "builder",
101
+ worktreePath: join(tempDir, ".legio", "worktrees", "builder-test"),
102
+ branchName: "legio/builder-test/task-123",
103
+ beadId: "task-123",
104
+ tmuxSession: "legio-test-builder",
105
+ state: "working",
106
+ pid: 12345,
107
+ parentAgent: null,
108
+ depth: 0,
109
+ runId: "run-1",
110
+ startedAt: "2024-01-01T00:00:00Z",
111
+ lastActivity: "2024-01-01T00:01:00Z",
112
+ escalationLevel: 0,
113
+ stalledSince: null,
114
+ };
115
+
116
+ store.upsert(session);
117
+ store.close();
118
+
119
+ const agents = await discoverAgents(tempDir);
120
+ expect(agents).toHaveLength(1);
121
+ expect(agents[0]?.agentName).toBe("builder-test");
122
+ expect(agents[0]?.capability).toBe("builder");
123
+ expect(agents[0]?.state).toBe("working");
124
+ });
125
+
126
+ it("should filter by capability", async () => {
127
+ const store = createSessionStore(dbPath);
128
+
129
+ const builder: AgentSession = {
130
+ id: "session-1",
131
+ agentName: "builder-test",
132
+ capability: "builder",
133
+ worktreePath: join(tempDir, ".legio", "worktrees", "builder-test"),
134
+ branchName: "legio/builder-test/task-123",
135
+ beadId: "task-123",
136
+ tmuxSession: "legio-test-builder",
137
+ state: "working",
138
+ pid: 12345,
139
+ parentAgent: null,
140
+ depth: 0,
141
+ runId: "run-1",
142
+ startedAt: "2024-01-01T00:00:00Z",
143
+ lastActivity: "2024-01-01T00:01:00Z",
144
+ escalationLevel: 0,
145
+ stalledSince: null,
146
+ };
147
+
148
+ const scout: AgentSession = {
149
+ id: "session-2",
150
+ agentName: "scout-test",
151
+ capability: "scout",
152
+ worktreePath: join(tempDir, ".legio", "worktrees", "scout-test"),
153
+ branchName: "legio/scout-test/task-456",
154
+ beadId: "task-456",
155
+ tmuxSession: "legio-test-scout",
156
+ state: "working",
157
+ pid: 12346,
158
+ parentAgent: null,
159
+ depth: 0,
160
+ runId: "run-1",
161
+ startedAt: "2024-01-01T00:00:00Z",
162
+ lastActivity: "2024-01-01T00:01:00Z",
163
+ escalationLevel: 0,
164
+ stalledSince: null,
165
+ };
166
+
167
+ store.upsert(builder);
168
+ store.upsert(scout);
169
+ store.close();
170
+
171
+ const agents = await discoverAgents(tempDir, { capability: "builder" });
172
+ expect(agents).toHaveLength(1);
173
+ expect(agents[0]?.agentName).toBe("builder-test");
174
+ expect(agents[0]?.capability).toBe("builder");
175
+ });
176
+
177
+ it("should includeAll returns completed agents too", async () => {
178
+ const store = createSessionStore(dbPath);
179
+
180
+ const working: AgentSession = {
181
+ id: "session-1",
182
+ agentName: "builder-working",
183
+ capability: "builder",
184
+ worktreePath: join(tempDir, ".legio", "worktrees", "builder-working"),
185
+ branchName: "legio/builder-working/task-123",
186
+ beadId: "task-123",
187
+ tmuxSession: "legio-test-working",
188
+ state: "working",
189
+ pid: 12345,
190
+ parentAgent: null,
191
+ depth: 0,
192
+ runId: "run-1",
193
+ startedAt: "2024-01-01T00:00:00Z",
194
+ lastActivity: "2024-01-01T00:01:00Z",
195
+ escalationLevel: 0,
196
+ stalledSince: null,
197
+ };
198
+
199
+ const completed: AgentSession = {
200
+ id: "session-2",
201
+ agentName: "builder-completed",
202
+ capability: "builder",
203
+ worktreePath: join(tempDir, ".legio", "worktrees", "builder-completed"),
204
+ branchName: "legio/builder-completed/task-456",
205
+ beadId: "task-456",
206
+ tmuxSession: "legio-test-completed",
207
+ state: "completed",
208
+ pid: null,
209
+ parentAgent: null,
210
+ depth: 0,
211
+ runId: "run-1",
212
+ startedAt: "2024-01-01T00:00:00Z",
213
+ lastActivity: "2024-01-01T00:02:00Z",
214
+ escalationLevel: 0,
215
+ stalledSince: null,
216
+ };
217
+
218
+ store.upsert(working);
219
+ store.upsert(completed);
220
+ store.close();
221
+
222
+ // Without includeAll, only active agents
223
+ const activeAgents = await discoverAgents(tempDir);
224
+ expect(activeAgents).toHaveLength(1);
225
+ expect(activeAgents[0]?.agentName).toBe("builder-working");
226
+
227
+ // With includeAll, both working and completed
228
+ const allAgents = await discoverAgents(tempDir, { includeAll: true });
229
+ expect(allAgents).toHaveLength(2);
230
+ const names = allAgents.map((a) => a.agentName);
231
+ expect(names).toContain("builder-working");
232
+ expect(names).toContain("builder-completed");
233
+ });
234
+
235
+ afterEach(async () => {
236
+ await rm(tempDir, { recursive: true, force: true });
237
+ });
238
+ });
239
+
240
+ describe("agentsCommand", () => {
241
+ let tempDir: string;
242
+ let originalCwd: string;
243
+ let originalStdoutWrite: typeof process.stdout.write;
244
+ let stdoutBuffer: string;
245
+
246
+ beforeEach(async () => {
247
+ tempDir = await mkdtemp(join(tmpdir(), "legio-test-"));
248
+ const legioDir = join(tempDir, ".legio");
249
+ await mkdir(legioDir, { recursive: true });
250
+
251
+ // Create config.yaml
252
+ const configContent = `project:
253
+ name: test-project
254
+ root: ${tempDir}
255
+ canonicalBranch: main
256
+ agents:
257
+ manifestPath: .legio/agent-manifest.json
258
+ baseDir: agents
259
+ maxConcurrent: 5
260
+ staggerDelayMs: 100
261
+ maxDepth: 2
262
+ worktrees:
263
+ baseDir: .legio/worktrees
264
+ beads:
265
+ enabled: true
266
+ mulch:
267
+ enabled: true
268
+ domains: []
269
+ primeFormat: markdown
270
+ merge:
271
+ aiResolveEnabled: false
272
+ reimagineEnabled: false
273
+ watchdog:
274
+ tier0Enabled: false
275
+ tier0IntervalMs: 30000
276
+ tier1Enabled: false
277
+ tier2Enabled: false
278
+ zombieThresholdMs: 600000
279
+ nudgeIntervalMs: 60000
280
+ logging:
281
+ verbose: false
282
+ redactSecrets: true
283
+ `;
284
+ await writeFile(join(legioDir, "config.yaml"), configContent);
285
+
286
+ // Create sessions.db
287
+ const dbPath = join(legioDir, "sessions.db");
288
+ const store = createSessionStore(dbPath);
289
+ store.close();
290
+
291
+ // Mock stdout.write
292
+ stdoutBuffer = "";
293
+ originalStdoutWrite = process.stdout.write;
294
+ process.stdout.write = vi.fn((chunk: unknown) => {
295
+ stdoutBuffer += String(chunk);
296
+ return true;
297
+ });
298
+
299
+ // Change to temp dir
300
+ originalCwd = process.cwd();
301
+ process.chdir(tempDir);
302
+ });
303
+
304
+ it("should show help with --help flag", async () => {
305
+ await agentsCommand(["--help"]);
306
+ expect(stdoutBuffer).toContain("legio agents");
307
+ expect(stdoutBuffer).toContain("discover");
308
+ });
309
+
310
+ it("should show help with no subcommand", async () => {
311
+ await agentsCommand([]);
312
+ expect(stdoutBuffer).toContain("legio agents");
313
+ expect(stdoutBuffer).toContain("discover");
314
+ });
315
+
316
+ it("should error on unknown subcommand", async () => {
317
+ await expect(agentsCommand(["unknown"])).rejects.toThrow("Unknown subcommand");
318
+ });
319
+
320
+ afterEach(async () => {
321
+ process.stdout.write = originalStdoutWrite;
322
+ process.chdir(originalCwd);
323
+ await rm(tempDir, { recursive: true, force: true });
324
+ });
325
+ });