@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,622 @@
1
+ /**
2
+ * CLI command: legio init [--force]
3
+ *
4
+ * Scaffolds the `.legio/` directory in the current project with:
5
+ * - config.yaml (serialized from DEFAULT_CONFIG)
6
+ * - agent-manifest.json (starter agent definitions)
7
+ * - hooks.json (central hooks config)
8
+ * - Required subdirectories (agents/, worktrees/, specs/, logs/)
9
+ * - .gitignore entries for transient files
10
+ */
11
+
12
+ import { spawn } from "node:child_process";
13
+ import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
14
+ import { basename, join } from "node:path";
15
+ import Database from "better-sqlite3";
16
+ import { DEFAULT_CONFIG } from "../config.ts";
17
+ import { ValidationError } from "../errors.ts";
18
+ import type { AgentManifest, LegioConfig } from "../types.ts";
19
+
20
+ const LEGIO_DIR = ".legio";
21
+
22
+ /**
23
+ * Check if a file exists using access().
24
+ */
25
+ async function fileExists(path: string): Promise<boolean> {
26
+ try {
27
+ await access(path);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Run an external command and collect stdout/stderr + exit code.
36
+ */
37
+ async function runCommand(
38
+ cmd: string[],
39
+ opts?: { cwd?: string },
40
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
41
+ const [command, ...args] = cmd;
42
+ if (!command) {
43
+ return Promise.resolve({ stdout: "", stderr: "", exitCode: 1 });
44
+ }
45
+ return new Promise((resolve) => {
46
+ const proc = spawn(command, args, {
47
+ cwd: opts?.cwd,
48
+ stdio: ["ignore", "pipe", "pipe"],
49
+ });
50
+ const stdoutChunks: Buffer[] = [];
51
+ const stderrChunks: Buffer[] = [];
52
+ proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
53
+ proc.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
54
+ proc.on("close", (code) => {
55
+ resolve({
56
+ stdout: Buffer.concat(stdoutChunks).toString(),
57
+ stderr: Buffer.concat(stderrChunks).toString(),
58
+ exitCode: code ?? 1,
59
+ });
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Detect the project name from git or fall back to directory name.
66
+ */
67
+ async function detectProjectName(root: string): Promise<string> {
68
+ // Try git remote origin
69
+ try {
70
+ const { stdout, exitCode } = await runCommand(["git", "remote", "get-url", "origin"], {
71
+ cwd: root,
72
+ });
73
+ if (exitCode === 0) {
74
+ const url = stdout.trim();
75
+ // Extract repo name from URL: git@host:user/repo.git or https://host/user/repo.git
76
+ const match = url.match(/\/([^/]+?)(?:\.git)?$/);
77
+ if (match?.[1]) {
78
+ return match[1];
79
+ }
80
+ }
81
+ } catch {
82
+ // Git not available or not a git repo
83
+ }
84
+
85
+ return basename(root);
86
+ }
87
+
88
+ /**
89
+ * Detect the canonical branch name from git.
90
+ */
91
+ async function detectCanonicalBranch(root: string): Promise<string> {
92
+ try {
93
+ const { stdout, exitCode } = await runCommand(
94
+ ["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
95
+ { cwd: root },
96
+ );
97
+ if (exitCode === 0) {
98
+ const ref = stdout.trim();
99
+ // refs/remotes/origin/main -> main
100
+ const branch = ref.split("/").pop();
101
+ if (branch) {
102
+ return branch;
103
+ }
104
+ }
105
+ } catch {
106
+ // Not available
107
+ }
108
+
109
+ // Fall back to checking current branch
110
+ try {
111
+ const { stdout, exitCode } = await runCommand(["git", "branch", "--show-current"], {
112
+ cwd: root,
113
+ });
114
+ if (exitCode === 0) {
115
+ const branch = stdout.trim();
116
+ if (branch === "main" || branch === "master" || branch === "develop") {
117
+ return branch;
118
+ }
119
+ }
120
+ } catch {
121
+ // Not available
122
+ }
123
+
124
+ return "main";
125
+ }
126
+
127
+ /**
128
+ * Serialize an LegioConfig to YAML format.
129
+ *
130
+ * Handles nested objects with indentation, scalar values,
131
+ * arrays with `- item` syntax, and empty arrays as `[]`.
132
+ */
133
+ function serializeConfigToYaml(config: LegioConfig): string {
134
+ const lines: string[] = [];
135
+ lines.push("# Legio configuration");
136
+ lines.push("# See: https://github.com/legio/legio");
137
+ lines.push("");
138
+
139
+ serializeObject(config as unknown as Record<string, unknown>, lines, 0);
140
+
141
+ return `${lines.join("\n")}\n`;
142
+ }
143
+
144
+ /**
145
+ * Recursively serialize an object to YAML lines.
146
+ */
147
+ function serializeObject(obj: Record<string, unknown>, lines: string[], depth: number): void {
148
+ const indent = " ".repeat(depth);
149
+
150
+ for (const [key, value] of Object.entries(obj)) {
151
+ if (value === null || value === undefined) {
152
+ lines.push(`${indent}${key}: null`);
153
+ } else if (typeof value === "object" && !Array.isArray(value)) {
154
+ lines.push(`${indent}${key}:`);
155
+ serializeObject(value as Record<string, unknown>, lines, depth + 1);
156
+ } else if (Array.isArray(value)) {
157
+ if (value.length === 0) {
158
+ lines.push(`${indent}${key}: []`);
159
+ } else {
160
+ lines.push(`${indent}${key}:`);
161
+ const itemIndent = " ".repeat(depth + 1);
162
+ for (const item of value) {
163
+ lines.push(`${itemIndent}- ${formatYamlValue(item)}`);
164
+ }
165
+ }
166
+ } else {
167
+ lines.push(`${indent}${key}: ${formatYamlValue(value)}`);
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Format a scalar value for YAML output.
174
+ */
175
+ function formatYamlValue(value: unknown): string {
176
+ if (typeof value === "string") {
177
+ // Quote strings that could be misinterpreted
178
+ if (
179
+ value === "" ||
180
+ value === "true" ||
181
+ value === "false" ||
182
+ value === "null" ||
183
+ value.includes(":") ||
184
+ value.includes("#") ||
185
+ value.includes("'") ||
186
+ value.includes('"') ||
187
+ value.includes("\n") ||
188
+ /^\d/.test(value)
189
+ ) {
190
+ // Use double quotes, escaping inner double quotes
191
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
192
+ }
193
+ return value;
194
+ }
195
+
196
+ if (typeof value === "number") {
197
+ return String(value);
198
+ }
199
+
200
+ if (typeof value === "boolean") {
201
+ return value ? "true" : "false";
202
+ }
203
+
204
+ if (value === null || value === undefined) {
205
+ return "null";
206
+ }
207
+
208
+ return String(value);
209
+ }
210
+
211
+ /**
212
+ * Build the starter agent manifest.
213
+ */
214
+ function buildAgentManifest(): AgentManifest {
215
+ const agents: AgentManifest["agents"] = {
216
+ scout: {
217
+ file: "scout.md",
218
+ model: "haiku",
219
+ tools: ["Read", "Glob", "Grep", "Bash"],
220
+ capabilities: ["explore", "research"],
221
+ canSpawn: false,
222
+ constraints: ["read-only"],
223
+ },
224
+ builder: {
225
+ file: "builder.md",
226
+ model: "sonnet",
227
+ tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
228
+ capabilities: ["implement", "refactor", "fix"],
229
+ canSpawn: false,
230
+ constraints: [],
231
+ },
232
+ reviewer: {
233
+ file: "reviewer.md",
234
+ model: "sonnet",
235
+ tools: ["Read", "Glob", "Grep", "Bash"],
236
+ capabilities: ["review", "validate"],
237
+ canSpawn: false,
238
+ constraints: ["read-only"],
239
+ },
240
+ lead: {
241
+ file: "lead.md",
242
+ model: "sonnet",
243
+ tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Task"],
244
+ capabilities: ["coordinate", "implement", "review"],
245
+ canSpawn: true,
246
+ constraints: [],
247
+ },
248
+ merger: {
249
+ file: "merger.md",
250
+ model: "sonnet",
251
+ tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
252
+ capabilities: ["merge", "resolve-conflicts"],
253
+ canSpawn: false,
254
+ constraints: [],
255
+ },
256
+ coordinator: {
257
+ file: "coordinator.md",
258
+ model: "opus",
259
+ tools: ["Read", "Glob", "Grep", "Bash"],
260
+ capabilities: ["coordinate", "dispatch", "escalate"],
261
+ canSpawn: true,
262
+ constraints: ["read-only", "no-worktree"],
263
+ },
264
+ supervisor: {
265
+ file: "supervisor.md",
266
+ model: "opus",
267
+ tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Task"],
268
+ capabilities: ["coordinate", "supervise"],
269
+ canSpawn: true,
270
+ constraints: [],
271
+ },
272
+ monitor: {
273
+ file: "monitor.md",
274
+ model: "sonnet",
275
+ tools: ["Read", "Glob", "Grep", "Bash"],
276
+ capabilities: ["monitor", "patrol"],
277
+ canSpawn: false,
278
+ constraints: ["read-only", "no-worktree"],
279
+ },
280
+ cto: {
281
+ file: "cto.md",
282
+ model: "opus",
283
+ tools: ["Read", "Glob", "Grep", "Bash"],
284
+ capabilities: ["strategy", "analyze", "advise"],
285
+ canSpawn: false,
286
+ constraints: ["read-only"],
287
+ },
288
+ };
289
+
290
+ // Build capability index: map each capability to agent names that declare it
291
+ const capabilityIndex: Record<string, string[]> = {};
292
+ for (const [name, def] of Object.entries(agents)) {
293
+ for (const cap of def.capabilities) {
294
+ const existing = capabilityIndex[cap];
295
+ if (existing) {
296
+ existing.push(name);
297
+ } else {
298
+ capabilityIndex[cap] = [name];
299
+ }
300
+ }
301
+ }
302
+
303
+ return { version: "1.0", agents, capabilityIndex };
304
+ }
305
+
306
+ /**
307
+ * Build the hooks.json content for the project orchestrator.
308
+ *
309
+ * Always generates from scratch (not from the agent template, which contains
310
+ * {{AGENT_NAME}} placeholders and space indentation). Uses tab indentation
311
+ * to match Biome formatting rules.
312
+ */
313
+ function buildHooksJson(): string {
314
+ // Tool name extraction: reads hook stdin JSON and extracts tool_name field.
315
+ // Claude Code sends {"tool_name":"Bash","tool_input":{...}} on stdin for
316
+ // PreToolUse/PostToolUse hooks.
317
+ const toolNameExtract =
318
+ 'read -r INPUT; TOOL_NAME=$(echo "$INPUT" | sed \'s/.*"tool_name": *"\\([^"]*\\)".*/\\1/\');';
319
+
320
+ const hooks = {
321
+ hooks: {
322
+ SessionStart: [
323
+ {
324
+ matcher: "",
325
+ hooks: [
326
+ {
327
+ type: "command",
328
+ command: "legio prime --agent orchestrator",
329
+ },
330
+ ],
331
+ },
332
+ ],
333
+ UserPromptSubmit: [
334
+ {
335
+ matcher: "",
336
+ hooks: [
337
+ {
338
+ type: "command",
339
+ command: "legio mail check --inject --agent orchestrator",
340
+ },
341
+ ],
342
+ },
343
+ ],
344
+ PreToolUse: [
345
+ {
346
+ matcher: "Bash",
347
+ hooks: [
348
+ {
349
+ type: "command",
350
+ command:
351
+ 'read -r INPUT; CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\'); if echo "$CMD" | grep -qE \'\\bgit\\s+push\\b\'; then echo \'{"decision":"block","reason":"git push is blocked by legio — merge locally, push manually when ready"}\'; exit 0; fi;',
352
+ },
353
+ ],
354
+ },
355
+ {
356
+ matcher: "",
357
+ hooks: [
358
+ {
359
+ type: "command",
360
+ command: `${toolNameExtract} legio log tool-start --agent orchestrator --tool-name "$TOOL_NAME"`,
361
+ },
362
+ ],
363
+ },
364
+ ],
365
+ PostToolUse: [
366
+ {
367
+ matcher: "",
368
+ hooks: [
369
+ {
370
+ type: "command",
371
+ command: `${toolNameExtract} legio log tool-end --agent orchestrator --tool-name "$TOOL_NAME"`,
372
+ },
373
+ ],
374
+ },
375
+ ],
376
+ Stop: [
377
+ {
378
+ matcher: "",
379
+ hooks: [
380
+ {
381
+ type: "command",
382
+ command: "legio log session-end --agent orchestrator",
383
+ },
384
+ {
385
+ type: "command",
386
+ command: "mulch learn",
387
+ },
388
+ ],
389
+ },
390
+ ],
391
+ PreCompact: [
392
+ {
393
+ matcher: "",
394
+ hooks: [
395
+ {
396
+ type: "command",
397
+ command: "legio prime --agent orchestrator --compact",
398
+ },
399
+ ],
400
+ },
401
+ ],
402
+ },
403
+ };
404
+
405
+ return `${JSON.stringify(hooks, null, "\t")}\n`;
406
+ }
407
+
408
+ /**
409
+ * Migrate existing SQLite databases on --force reinit.
410
+ *
411
+ * Opens each DB, enables WAL mode, and re-runs CREATE TABLE/INDEX IF NOT EXISTS
412
+ * to apply any schema additions without losing existing data.
413
+ */
414
+ async function migrateExistingDatabases(legioPath: string): Promise<string[]> {
415
+ const migrated: string[] = [];
416
+
417
+ // Migrate mail.db
418
+ const mailDbPath = join(legioPath, "mail.db");
419
+ if (await fileExists(mailDbPath)) {
420
+ const db = new Database(mailDbPath);
421
+ db.exec("PRAGMA journal_mode = WAL");
422
+ db.exec("PRAGMA synchronous = NORMAL");
423
+ db.exec("PRAGMA busy_timeout = 5000");
424
+ db.exec(`
425
+ CREATE TABLE IF NOT EXISTS messages (
426
+ id TEXT PRIMARY KEY,
427
+ from_agent TEXT NOT NULL,
428
+ to_agent TEXT NOT NULL,
429
+ subject TEXT NOT NULL,
430
+ body TEXT NOT NULL,
431
+ type TEXT NOT NULL DEFAULT 'status',
432
+ priority TEXT NOT NULL DEFAULT 'normal',
433
+ thread_id TEXT,
434
+ read INTEGER NOT NULL DEFAULT 0,
435
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
436
+ )`);
437
+ db.exec(`
438
+ CREATE INDEX IF NOT EXISTS idx_inbox ON messages(to_agent, read);
439
+ CREATE INDEX IF NOT EXISTS idx_thread ON messages(thread_id)`);
440
+ db.close();
441
+ migrated.push("mail.db");
442
+ }
443
+
444
+ // Migrate metrics.db
445
+ const metricsDbPath = join(legioPath, "metrics.db");
446
+ if (await fileExists(metricsDbPath)) {
447
+ const db = new Database(metricsDbPath);
448
+ db.exec("PRAGMA journal_mode = WAL");
449
+ db.exec("PRAGMA synchronous = NORMAL");
450
+ db.exec("PRAGMA busy_timeout = 5000");
451
+ db.exec(`
452
+ CREATE TABLE IF NOT EXISTS sessions (
453
+ agent_name TEXT NOT NULL,
454
+ bead_id TEXT NOT NULL,
455
+ capability TEXT NOT NULL,
456
+ started_at TEXT NOT NULL,
457
+ completed_at TEXT,
458
+ duration_ms INTEGER NOT NULL DEFAULT 0,
459
+ exit_code INTEGER,
460
+ merge_result TEXT,
461
+ parent_agent TEXT,
462
+ PRIMARY KEY (agent_name, bead_id)
463
+ )`);
464
+ db.close();
465
+ migrated.push("metrics.db");
466
+ }
467
+
468
+ return migrated;
469
+ }
470
+
471
+ /**
472
+ * Content for .legio/.gitignore — runtime state that should not be tracked.
473
+ * Uses wildcard+whitelist pattern: ignore everything, whitelist tracked files.
474
+ * Auto-healed by legio prime on each session start.
475
+ * Config files (config.yaml, agent-manifest.json, hooks.json) remain tracked.
476
+ */
477
+ export const LEGIO_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
478
+ # Auto-healed by legio prime on each session start
479
+ *
480
+ !.gitignore
481
+ !config.yaml
482
+ !agent-manifest.json
483
+ !hooks.json
484
+ !groups.json
485
+ !agent-defs/
486
+ `;
487
+
488
+ /**
489
+ * Write .legio/.gitignore for runtime state files.
490
+ * Always overwrites to support --force reinit and auto-healing via prime.
491
+ */
492
+ export async function writeLegioGitignore(legioPath: string): Promise<void> {
493
+ const gitignorePath = join(legioPath, ".gitignore");
494
+ await writeFile(gitignorePath, LEGIO_GITIGNORE);
495
+ }
496
+
497
+ /**
498
+ * Print a success status line.
499
+ */
500
+ function printCreated(relativePath: string): void {
501
+ process.stdout.write(` \u2713 Created ${relativePath}\n`);
502
+ }
503
+
504
+ /**
505
+ * Entry point for `legio init [--force]`.
506
+ *
507
+ * Scaffolds the .legio/ directory structure in the current working directory.
508
+ *
509
+ * @param args - CLI arguments after "init" subcommand
510
+ */
511
+ const INIT_HELP = `legio init — Initialize .legio/ in current project
512
+
513
+ Usage: legio init [--force]
514
+
515
+ Options:
516
+ --force Reinitialize even if .legio/ already exists
517
+ --help, -h Show this help`;
518
+
519
+ export async function initCommand(args: string[]): Promise<void> {
520
+ if (args.includes("--help") || args.includes("-h")) {
521
+ process.stdout.write(`${INIT_HELP}\n`);
522
+ return;
523
+ }
524
+
525
+ const force = args.includes("--force");
526
+ const projectRoot = process.cwd();
527
+ const legioPath = join(projectRoot, LEGIO_DIR);
528
+
529
+ // 0. Verify we're inside a git repository
530
+ const { exitCode: gitCheckExit } = await runCommand(
531
+ ["git", "rev-parse", "--is-inside-work-tree"],
532
+ { cwd: projectRoot },
533
+ );
534
+ if (gitCheckExit !== 0) {
535
+ throw new ValidationError("legio requires a git repository. Run 'git init' first.", {
536
+ field: "git",
537
+ });
538
+ }
539
+
540
+ // 1. Check if .legio/ already exists
541
+ if (await fileExists(join(legioPath, "config.yaml"))) {
542
+ if (!force) {
543
+ process.stdout.write(
544
+ "Warning: .legio/ already initialized in this project.\n" +
545
+ "Use --force to reinitialize.\n",
546
+ );
547
+ return;
548
+ }
549
+ process.stdout.write("Reinitializing .legio/ (--force)\n\n");
550
+ }
551
+
552
+ // 2. Detect project info
553
+ const projectName = await detectProjectName(projectRoot);
554
+ const canonicalBranch = await detectCanonicalBranch(projectRoot);
555
+
556
+ process.stdout.write(`Initializing legio for "${projectName}"...\n\n`);
557
+
558
+ // 3. Create directory structure
559
+ const dirs = [
560
+ LEGIO_DIR,
561
+ join(LEGIO_DIR, "agents"),
562
+ join(LEGIO_DIR, "agent-defs"),
563
+ join(LEGIO_DIR, "worktrees"),
564
+ join(LEGIO_DIR, "specs"),
565
+ join(LEGIO_DIR, "logs"),
566
+ ];
567
+
568
+ for (const dir of dirs) {
569
+ await mkdir(join(projectRoot, dir), { recursive: true });
570
+ printCreated(`${dir}/`);
571
+ }
572
+
573
+ // 3b. Deploy agent definition .md files from legio install directory
574
+ const legioAgentsDir = join(import.meta.dirname, "..", "..", "agents");
575
+ const agentDefsTarget = join(legioPath, "agent-defs");
576
+ const agentDefFiles = await readdir(legioAgentsDir);
577
+ for (const fileName of agentDefFiles) {
578
+ if (!fileName.endsWith(".md")) continue;
579
+ const content = await readFile(join(legioAgentsDir, fileName), "utf-8");
580
+ await writeFile(join(agentDefsTarget, fileName), content);
581
+ printCreated(`${LEGIO_DIR}/agent-defs/${fileName}`);
582
+ }
583
+
584
+ // 4. Write config.yaml
585
+ const config = structuredClone(DEFAULT_CONFIG);
586
+ config.project.name = projectName;
587
+ config.project.root = projectRoot;
588
+ config.project.canonicalBranch = canonicalBranch;
589
+
590
+ const configYaml = serializeConfigToYaml(config);
591
+ const configPath = join(legioPath, "config.yaml");
592
+ await writeFile(configPath, configYaml);
593
+ printCreated(`${LEGIO_DIR}/config.yaml`);
594
+
595
+ // 5. Write agent-manifest.json
596
+ const manifest = buildAgentManifest();
597
+ const manifestPath = join(legioPath, "agent-manifest.json");
598
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`);
599
+ printCreated(`${LEGIO_DIR}/agent-manifest.json`);
600
+
601
+ // 6. Write hooks.json
602
+ const hooksContent = buildHooksJson();
603
+ const hooksPath = join(legioPath, "hooks.json");
604
+ await writeFile(hooksPath, hooksContent);
605
+ printCreated(`${LEGIO_DIR}/hooks.json`);
606
+
607
+ // 7. Write .legio/.gitignore for runtime state
608
+ await writeLegioGitignore(legioPath);
609
+ printCreated(`${LEGIO_DIR}/.gitignore`);
610
+
611
+ // 8. Migrate existing SQLite databases on --force reinit
612
+ if (force) {
613
+ const migrated = await migrateExistingDatabases(legioPath);
614
+ for (const dbName of migrated) {
615
+ process.stdout.write(` \u2713 Migrated ${LEGIO_DIR}/${dbName} (schema validated)\n`);
616
+ }
617
+ }
618
+
619
+ process.stdout.write("\nDone.\n");
620
+ process.stdout.write(" Next: run `legio hooks install` to enable Claude Code hooks.\n");
621
+ process.stdout.write(" Then: run `legio status` to see the current state.\n");
622
+ }