@os-eco/overstory-cli 0.6.1

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