@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,298 @@
1
+ /**
2
+ * CLI command: legio server <subcommand>
3
+ *
4
+ * Starts the local web UI server for project monitoring.
5
+ * The actual server implementation lives in src/server/index.ts.
6
+ */
7
+
8
+ import { type SpawnOptions, spawn } from "node:child_process";
9
+ import { access, readFile, unlink, writeFile } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { loadConfig } from "../config.ts";
12
+ import { ValidationError } from "../errors.ts";
13
+ import { isProcessRunning } from "../watchdog/health.ts";
14
+
15
+ function getFlag(args: string[], flag: string): string | undefined {
16
+ const idx = args.indexOf(flag);
17
+ if (idx === -1 || idx + 1 >= args.length) return undefined;
18
+ return args[idx + 1];
19
+ }
20
+
21
+ function hasFlag(args: string[], flag: string): boolean {
22
+ return args.includes(flag);
23
+ }
24
+
25
+ /** Dependency injection for testing. Uses real implementations when omitted. */
26
+ export interface ServerDeps {
27
+ _isProcessRunning?: (pid: number) => boolean;
28
+ _spawn?: (cmd: string, args: string[], opts: SpawnOptions) => { pid?: number; unref: () => void };
29
+ _sleep?: (ms: number) => Promise<void>;
30
+ _startServer?: (opts: {
31
+ port: number;
32
+ host: string;
33
+ root: string;
34
+ shouldOpen: boolean;
35
+ autoStartCoordinator: boolean;
36
+ }) => Promise<void>;
37
+ }
38
+
39
+ const SERVER_HELP = `legio server <subcommand>
40
+
41
+ Subcommands:
42
+ start Start the local web UI server
43
+ stop Stop the daemon server
44
+ status Show daemon server status
45
+
46
+ Options (start):
47
+ --port <n> Port to listen on (default: 4173)
48
+ --host <addr> Bind address (default: 127.0.0.1)
49
+ --open Auto-open browser after server starts
50
+ --daemon Run server as a background daemon
51
+
52
+ Options (status):
53
+ --json JSON output
54
+
55
+ --help, -h Show this help
56
+ `;
57
+
58
+ /**
59
+ * Check if a file exists at the given path.
60
+ */
61
+ async function fileExists(path: string): Promise<boolean> {
62
+ try {
63
+ await access(path);
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Returns the path to the server PID file.
72
+ */
73
+ function serverPidPath(projectRoot: string): string {
74
+ return join(projectRoot, ".legio", "server.pid");
75
+ }
76
+
77
+ /**
78
+ * Read the PID from the server PID file.
79
+ * Returns null if the file doesn't exist or can't be parsed.
80
+ */
81
+ export async function readServerPid(projectRoot: string): Promise<number | null> {
82
+ const pidPath = serverPidPath(projectRoot);
83
+ if (!(await fileExists(pidPath))) {
84
+ return null;
85
+ }
86
+ try {
87
+ const text = await readFile(pidPath, "utf-8");
88
+ const pid = Number.parseInt(text.trim(), 10);
89
+ if (Number.isNaN(pid) || pid <= 0) {
90
+ return null;
91
+ }
92
+ return pid;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Write the server PID to the PID file.
100
+ */
101
+ export async function writeServerPid(projectRoot: string, pid: number): Promise<void> {
102
+ const pidPath = serverPidPath(projectRoot);
103
+ await writeFile(pidPath, String(pid), "utf-8");
104
+ }
105
+
106
+ /**
107
+ * Remove the server PID file.
108
+ */
109
+ export async function removeServerPid(projectRoot: string): Promise<void> {
110
+ const pidPath = serverPidPath(projectRoot);
111
+ try {
112
+ await unlink(pidPath);
113
+ } catch {
114
+ // File may already be gone — not an error
115
+ }
116
+ }
117
+
118
+ export async function serverCommand(args: string[], deps: ServerDeps = {}): Promise<void> {
119
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
120
+ process.stdout.write(`${SERVER_HELP}\n`);
121
+ return;
122
+ }
123
+
124
+ const subcommand = args[0];
125
+ const subArgs = args.slice(1);
126
+
127
+ switch (subcommand) {
128
+ case "start":
129
+ await startServer(subArgs, deps);
130
+ break;
131
+ case "stop":
132
+ await stopServer(subArgs, deps);
133
+ break;
134
+ case "status":
135
+ await statusServer(subArgs, deps);
136
+ break;
137
+ default:
138
+ process.stderr.write(`Unknown server subcommand: ${subcommand}\n`);
139
+ process.stderr.write("Run 'legio server --help' for usage.\n");
140
+ process.exit(1);
141
+ }
142
+ }
143
+
144
+ async function startServer(args: string[], deps: ServerDeps): Promise<void> {
145
+ if (args.includes("--help") || args.includes("-h")) {
146
+ process.stdout.write(`${SERVER_HELP}\n`);
147
+ return;
148
+ }
149
+
150
+ const portStr = getFlag(args, "--port");
151
+ const host = getFlag(args, "--host") ?? "127.0.0.1";
152
+ const shouldOpen = hasFlag(args, "--open");
153
+ const isDaemon = hasFlag(args, "--daemon");
154
+
155
+ const port = portStr ? Number.parseInt(portStr, 10) : 4173;
156
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
157
+ throw new ValidationError("--port must be a valid port number (1-65535)", {
158
+ field: "port",
159
+ value: portStr,
160
+ });
161
+ }
162
+
163
+ // Resolve project root and validate .legio exists
164
+ const root = process.cwd();
165
+ const config = await loadConfig(root);
166
+ const projectRoot = config.project.root || root;
167
+
168
+ if (isDaemon) {
169
+ // Check if already running
170
+ const existingPid = await readServerPid(projectRoot);
171
+ const checkRunning = deps._isProcessRunning ?? isProcessRunning;
172
+ if (existingPid !== null && checkRunning(existingPid)) {
173
+ process.stdout.write(`Server already running (PID ${existingPid})\n`);
174
+ return;
175
+ }
176
+ // Clean up stale PID file if present
177
+ if (existingPid !== null) {
178
+ await removeServerPid(projectRoot);
179
+ }
180
+
181
+ // Spawn detached child without --daemon, with LEGIO_SERVER_DAEMON=1.
182
+ // Strip __LEGIO_TSX_LOADED so the shim re-execs with --import tsx:
183
+ // the daemon child inherits the parent env, and on Node v23+ the shim
184
+ // must re-exec to load TypeScript — it cannot strip types without tsx active.
185
+ const spawnFn = deps._spawn ?? spawn;
186
+ const childArgs = ["server", "start", "--port", String(port), "--host", host];
187
+ if (shouldOpen) childArgs.push("--open");
188
+
189
+ const childEnv: NodeJS.ProcessEnv = { ...process.env, LEGIO_SERVER_DAEMON: "1" };
190
+ delete childEnv.__LEGIO_TSX_LOADED;
191
+
192
+ const child = spawnFn("legio", childArgs, {
193
+ detached: true,
194
+ stdio: "ignore",
195
+ env: childEnv,
196
+ });
197
+ child.unref();
198
+
199
+ if (child.pid !== undefined) {
200
+ await writeServerPid(projectRoot, child.pid);
201
+
202
+ // Wait for daemon to attempt port binding, then verify it survived
203
+ const sleep = deps._sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
204
+ await sleep(500);
205
+
206
+ if (!checkRunning(child.pid)) {
207
+ await removeServerPid(projectRoot);
208
+ process.stderr.write("Daemon process exited immediately — port may already be in use\n");
209
+ process.exit(1);
210
+ }
211
+
212
+ process.stdout.write(
213
+ `Server started as daemon (PID ${child.pid}) at http://${host}:${port}\n`,
214
+ );
215
+ } else {
216
+ process.stderr.write("Failed to spawn daemon process\n");
217
+ process.exit(1);
218
+ }
219
+ return;
220
+ }
221
+
222
+ // Import the server module dynamically to avoid circular deps
223
+ const autoStartCoordinator = process.env.LEGIO_SERVER_DAEMON === "1";
224
+ const start =
225
+ deps._startServer ??
226
+ (async (opts) => {
227
+ const { startServer: serverStart } = await import("../server/index.ts");
228
+ await serverStart(opts);
229
+ });
230
+ await start({ port, host, root, shouldOpen, autoStartCoordinator });
231
+ }
232
+
233
+ async function stopServer(_args: string[], deps: ServerDeps): Promise<void> {
234
+ const root = process.cwd();
235
+ const config = await loadConfig(root);
236
+ const projectRoot = config.project.root || root;
237
+
238
+ const pid = await readServerPid(projectRoot);
239
+ if (pid === null) {
240
+ process.stdout.write("Server not running\n");
241
+ return;
242
+ }
243
+
244
+ const checkRunning = deps._isProcessRunning ?? isProcessRunning;
245
+ if (!checkRunning(pid)) {
246
+ await removeServerPid(projectRoot);
247
+ process.stdout.write("Server not running (stale PID file cleaned up)\n");
248
+ return;
249
+ }
250
+
251
+ try {
252
+ process.kill(pid, "SIGTERM");
253
+ } catch {
254
+ // Process may have just died
255
+ }
256
+ await removeServerPid(projectRoot);
257
+ process.stdout.write(`Server stopped (PID ${pid})\n`);
258
+ }
259
+
260
+ async function statusServer(args: string[], deps: ServerDeps): Promise<void> {
261
+ const jsonMode = hasFlag(args, "--json");
262
+
263
+ const root = process.cwd();
264
+ const config = await loadConfig(root);
265
+ const projectRoot = config.project.root || root;
266
+
267
+ const pid = await readServerPid(projectRoot);
268
+ const checkRunning = deps._isProcessRunning ?? isProcessRunning;
269
+
270
+ if (pid === null) {
271
+ if (jsonMode) {
272
+ process.stdout.write(`${JSON.stringify({ running: false, pid: null })}\n`);
273
+ } else {
274
+ process.stdout.write("Server not running\n");
275
+ }
276
+ return;
277
+ }
278
+
279
+ const alive = checkRunning(pid);
280
+ if (!alive) {
281
+ // Clean up stale PID file
282
+ await removeServerPid(projectRoot);
283
+ if (jsonMode) {
284
+ process.stdout.write(`${JSON.stringify({ running: false, pid: null, stale: true })}\n`);
285
+ } else {
286
+ process.stdout.write("Server not running (stale PID file cleaned up)\n");
287
+ }
288
+ return;
289
+ }
290
+
291
+ // Determine port from config or default
292
+ const port = 4173;
293
+ if (jsonMode) {
294
+ process.stdout.write(`${JSON.stringify({ running: true, pid, port })}\n`);
295
+ } else {
296
+ process.stdout.write(`Server running (PID ${pid}) on port ${port}\n`);
297
+ }
298
+ }