@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,381 @@
1
+ /**
2
+ * `overstory prime` command.
3
+ *
4
+ * Loads context for the orchestrator or a specific agent and outputs it
5
+ * to stdout for injection into Claude Code's context via hooks.
6
+ *
7
+ * Called by the SessionStart hook.
8
+ */
9
+
10
+ import { join } from "node:path";
11
+ import { loadCheckpoint } from "../agents/checkpoint.ts";
12
+ import { loadIdentity } from "../agents/identity.ts";
13
+ import { createManifestLoader } from "../agents/manifest.ts";
14
+ import { loadConfig } from "../config.ts";
15
+ import { AgentError } from "../errors.ts";
16
+ import { createMetricsStore } from "../metrics/store.ts";
17
+ import { createMulchClient } from "../mulch/client.ts";
18
+ import { openSessionStore } from "../sessions/compat.ts";
19
+ import type { AgentIdentity, AgentManifest, SessionCheckpoint, SessionMetrics } from "../types.ts";
20
+ import { getCurrentSessionName } from "../worktree/tmux.ts";
21
+
22
+ /**
23
+ * Gitignore content for .overstory/.gitignore.
24
+ * TODO: Import from init.ts once it's exported (parallel branch change).
25
+ * Wildcard+whitelist pattern: ignore everything except tracked config files.
26
+ */
27
+ const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
28
+ # Auto-healed by overstory prime on each session start
29
+ *
30
+ !.gitignore
31
+ !config.yaml
32
+ !agent-manifest.json
33
+ !hooks.json
34
+ !groups.json
35
+ !agent-defs/
36
+ `;
37
+
38
+ /**
39
+ * Parse CLI flags from the args array.
40
+ *
41
+ * Supports:
42
+ * - `--agent <name>` — Prime for a specific agent
43
+ * - `--compact` — Output reduced context
44
+ */
45
+ function parseArgs(args: string[]): { agentName: string | null; compact: boolean } {
46
+ let agentName: string | null = null;
47
+ let compact = false;
48
+
49
+ for (let i = 0; i < args.length; i++) {
50
+ const arg = args[i];
51
+ if (arg === "--agent") {
52
+ const next = args[i + 1];
53
+ if (next === undefined || next.startsWith("--")) {
54
+ throw new AgentError("--agent requires a name argument");
55
+ }
56
+ agentName = next;
57
+ i++; // Skip the value
58
+ } else if (arg === "--compact") {
59
+ compact = true;
60
+ }
61
+ }
62
+
63
+ return { agentName, compact };
64
+ }
65
+
66
+ /**
67
+ * Format the agent manifest section for output.
68
+ */
69
+ function formatManifest(manifest: AgentManifest): string {
70
+ const lines: string[] = [];
71
+ for (const [name, def] of Object.entries(manifest.agents)) {
72
+ const caps = def.capabilities.join(", ");
73
+ const spawn = def.canSpawn ? " (can spawn)" : "";
74
+ lines.push(`- **${name}** [${def.model}]: ${caps}${spawn}`);
75
+ }
76
+ return lines.length > 0 ? lines.join("\n") : "No agents registered.";
77
+ }
78
+
79
+ /**
80
+ * Format recent session metrics for output.
81
+ */
82
+ function formatMetrics(sessions: SessionMetrics[]): string {
83
+ if (sessions.length === 0) {
84
+ return "No recent sessions.";
85
+ }
86
+
87
+ const lines: string[] = [];
88
+ for (const s of sessions) {
89
+ const status = s.completedAt !== null ? "completed" : "in-progress";
90
+ const duration = s.durationMs > 0 ? ` (${Math.round(s.durationMs / 1000)}s)` : "";
91
+ const merge = s.mergeResult !== null ? ` [${s.mergeResult}]` : "";
92
+ lines.push(`- ${s.agentName} (${s.capability}): ${s.beadId} — ${status}${duration}${merge}`);
93
+ }
94
+ return lines.join("\n");
95
+ }
96
+
97
+ /**
98
+ * Format agent identity for output.
99
+ */
100
+ function formatIdentity(identity: AgentIdentity): string {
101
+ const lines: string[] = [];
102
+ lines.push(`Name: ${identity.name}`);
103
+ lines.push(`Capability: ${identity.capability}`);
104
+ lines.push(`Sessions completed: ${identity.sessionsCompleted}`);
105
+
106
+ if (identity.expertiseDomains.length > 0) {
107
+ lines.push(`Expertise: ${identity.expertiseDomains.join(", ")}`);
108
+ }
109
+
110
+ if (identity.recentTasks.length > 0) {
111
+ lines.push("Recent tasks:");
112
+ for (const task of identity.recentTasks) {
113
+ lines.push(` - ${task.beadId}: ${task.summary} (${task.completedAt})`);
114
+ }
115
+ }
116
+
117
+ return lines.join("\n");
118
+ }
119
+
120
+ /**
121
+ * Format checkpoint recovery section for compact priming.
122
+ */
123
+ function formatCheckpointRecovery(checkpoint: SessionCheckpoint): string {
124
+ const lines: string[] = [];
125
+ lines.push("\n## Session Recovery");
126
+ lines.push("");
127
+ lines.push("You are resuming from a previous session that was compacted.");
128
+ lines.push("");
129
+ lines.push(`**Progress so far:** ${checkpoint.progressSummary}`);
130
+ lines.push(`**Files modified:** ${checkpoint.filesModified.join(", ") || "none"}`);
131
+ lines.push(`**Pending work:** ${checkpoint.pendingWork}`);
132
+ lines.push(`**Branch:** ${checkpoint.currentBranch}`);
133
+ return lines.join("\n");
134
+ }
135
+
136
+ /**
137
+ * Auto-heal .overstory/.gitignore if its content differs from the template.
138
+ * Ensures existing projects get updated gitignore on session start.
139
+ */
140
+ async function healGitignore(overstoryDir: string): Promise<void> {
141
+ const gitignorePath = join(overstoryDir, ".gitignore");
142
+ try {
143
+ const current = await Bun.file(gitignorePath).text();
144
+ if (current === OVERSTORY_GITIGNORE) {
145
+ return; // Already up to date
146
+ }
147
+ } catch {
148
+ // File does not exist — write it fresh
149
+ }
150
+ await Bun.write(gitignorePath, OVERSTORY_GITIGNORE);
151
+ }
152
+
153
+ /**
154
+ * Prime command entry point.
155
+ *
156
+ * Gathers project state and outputs context to stdout for injection
157
+ * into Claude Code's context.
158
+ *
159
+ * @param args - CLI arguments after "prime" subcommand
160
+ */
161
+ const PRIME_HELP = `overstory prime — Load context for orchestrator/agent
162
+
163
+ Usage: overstory prime [--agent <name>] [--compact]
164
+
165
+ Options:
166
+ --agent <name> Prime for a specific agent (default: orchestrator)
167
+ --compact Output reduced context (for PreCompact hook)
168
+ --help, -h Show this help`;
169
+
170
+ export async function primeCommand(args: string[]): Promise<void> {
171
+ if (args.includes("--help") || args.includes("-h")) {
172
+ process.stdout.write(`${PRIME_HELP}\n`);
173
+ return;
174
+ }
175
+
176
+ const { agentName, compact } = parseArgs(args);
177
+
178
+ // 1. Load config
179
+ const config = await loadConfig(process.cwd());
180
+
181
+ // 2. Auto-heal .overstory/.gitignore
182
+ const overstoryDir = join(config.project.root, ".overstory");
183
+ await healGitignore(overstoryDir);
184
+
185
+ // 3. Load mulch expertise (optional — skip on failure)
186
+ let expertiseOutput: string | null = null;
187
+ if (!compact && config.mulch.enabled) {
188
+ try {
189
+ const mulch = createMulchClient(config.project.root);
190
+ const domains = config.mulch.domains.length > 0 ? config.mulch.domains : undefined;
191
+ expertiseOutput = await mulch.prime(domains, config.mulch.primeFormat);
192
+ } catch {
193
+ // Mulch is optional — silently skip if it fails
194
+ }
195
+ }
196
+
197
+ // 4. Output context (orchestrator or agent)
198
+ if (agentName !== null) {
199
+ // === Agent priming ===
200
+ await outputAgentContext(config, agentName, compact, expertiseOutput);
201
+ } else {
202
+ // === Orchestrator priming ===
203
+ await outputOrchestratorContext(config, compact, expertiseOutput);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Output context for a specific agent.
209
+ */
210
+ async function outputAgentContext(
211
+ config: Awaited<ReturnType<typeof loadConfig>>,
212
+ agentName: string,
213
+ compact: boolean,
214
+ expertiseOutput: string | null,
215
+ ): Promise<void> {
216
+ const sections: string[] = [];
217
+
218
+ sections.push(`# Agent Context: ${agentName}`);
219
+
220
+ // Check if the agent exists in the SessionStore or has an identity file
221
+ const overstoryDir = join(config.project.root, ".overstory");
222
+ const { store } = openSessionStore(overstoryDir);
223
+ let sessionExists = false;
224
+ let boundSession: { beadId: string } | null = null;
225
+ try {
226
+ const agentSession = store.getByName(agentName);
227
+ sessionExists = agentSession !== null;
228
+ if (
229
+ agentSession &&
230
+ agentSession.state !== "completed" &&
231
+ agentSession.state !== "zombie" &&
232
+ agentSession.beadId
233
+ ) {
234
+ boundSession = { beadId: agentSession.beadId };
235
+ }
236
+ } finally {
237
+ store.close();
238
+ }
239
+
240
+ // Identity section
241
+ let identity: AgentIdentity | null = null;
242
+ try {
243
+ const baseDir = join(config.project.root, ".overstory", "agents");
244
+ identity = await loadIdentity(baseDir, agentName);
245
+ } catch {
246
+ // Identity may not exist yet
247
+ }
248
+
249
+ // Warn if agent is completely unknown (no session and no identity)
250
+ if (!sessionExists && identity === null) {
251
+ process.stderr.write(
252
+ `Warning: agent "${agentName}" not found in sessions or identity store.\n`,
253
+ );
254
+ }
255
+
256
+ sections.push("\n## Identity");
257
+ if (identity !== null) {
258
+ sections.push(formatIdentity(identity));
259
+ } else {
260
+ sections.push("New agent - no prior sessions");
261
+ }
262
+
263
+ // Activation context: if agent has a bound task, inject it
264
+ if (boundSession) {
265
+ sections.push("\n## Activation");
266
+ sections.push(`You have a bound task: **${boundSession.beadId}**`);
267
+ sections.push("Read your overlay at `.claude/CLAUDE.md` and begin working immediately.");
268
+ sections.push("Do not wait for dispatch mail. Your assignment was bound at spawn time.");
269
+ }
270
+
271
+ // In compact mode, check for checkpoint recovery
272
+ if (compact) {
273
+ const baseDir = join(config.project.root, ".overstory", "agents");
274
+ const checkpoint = await loadCheckpoint(baseDir, agentName);
275
+ if (checkpoint !== null) {
276
+ sections.push(formatCheckpointRecovery(checkpoint));
277
+ }
278
+ }
279
+
280
+ // In compact mode, skip expertise
281
+ if (!compact && expertiseOutput !== null) {
282
+ sections.push("\n## Expertise");
283
+ sections.push(expertiseOutput.trim());
284
+ }
285
+
286
+ process.stdout.write(`${sections.join("\n")}\n`);
287
+ }
288
+
289
+ /**
290
+ * Output context for the orchestrator.
291
+ */
292
+ async function outputOrchestratorContext(
293
+ config: Awaited<ReturnType<typeof loadConfig>>,
294
+ compact: boolean,
295
+ expertiseOutput: string | null,
296
+ ): Promise<void> {
297
+ // Register orchestrator tmux session for reverse-nudge (agents → orchestrator)
298
+ try {
299
+ const tmuxSession = await getCurrentSessionName();
300
+ if (tmuxSession) {
301
+ const regPath = join(config.project.root, ".overstory", "orchestrator-tmux.json");
302
+ await Bun.write(
303
+ regPath,
304
+ `${JSON.stringify({ tmuxSession, registeredAt: new Date().toISOString() }, null, "\t")}\n`,
305
+ );
306
+ }
307
+ } catch {
308
+ // Tmux detection is optional — silently skip
309
+ }
310
+
311
+ // Record the orchestrator's current branch for merge targeting
312
+ let sessionBranch: string | null = null;
313
+ try {
314
+ const branchProc = Bun.spawn(["git", "symbolic-ref", "--short", "HEAD"], {
315
+ cwd: config.project.root,
316
+ stdout: "pipe",
317
+ stderr: "pipe",
318
+ });
319
+ const branchExit = await branchProc.exited;
320
+ if (branchExit === 0) {
321
+ const branch = (await new Response(branchProc.stdout).text()).trim();
322
+ if (branch) {
323
+ sessionBranch = branch;
324
+ const sessionBranchPath = join(config.project.root, ".overstory", "session-branch.txt");
325
+ await Bun.write(sessionBranchPath, `${branch}\n`);
326
+ }
327
+ }
328
+ } catch {
329
+ // Branch detection is optional — silently skip
330
+ }
331
+
332
+ const sections: string[] = [];
333
+
334
+ // Project section
335
+ sections.push("# Overstory Context");
336
+ sections.push(`\n## Project: ${config.project.name}`);
337
+ sections.push(`Canonical branch: ${config.project.canonicalBranch}`);
338
+ if (sessionBranch && sessionBranch !== config.project.canonicalBranch) {
339
+ sections.push(`Session branch: ${sessionBranch} (merge target)`);
340
+ }
341
+ sections.push(`Max concurrent agents: ${config.agents.maxConcurrent}`);
342
+ sections.push(`Max depth: ${config.agents.maxDepth}`);
343
+
344
+ // Agent manifest section
345
+ sections.push("\n## Agent Manifest");
346
+ try {
347
+ const manifestPath = join(config.project.root, config.agents.manifestPath);
348
+ const baseDir = join(config.project.root, config.agents.baseDir);
349
+ const loader = createManifestLoader(manifestPath, baseDir);
350
+ const manifest = await loader.load();
351
+ sections.push(formatManifest(manifest));
352
+ } catch {
353
+ sections.push("No agent manifest found.");
354
+ }
355
+
356
+ // In compact mode, skip metrics and expertise
357
+ if (!compact) {
358
+ // Recent activity section
359
+ sections.push("\n## Recent Activity");
360
+ try {
361
+ const metricsPath = join(config.project.root, ".overstory", "metrics.db");
362
+ const store = createMetricsStore(metricsPath);
363
+ try {
364
+ const sessions = store.getRecentSessions(5);
365
+ sections.push(formatMetrics(sessions));
366
+ } finally {
367
+ store.close();
368
+ }
369
+ } catch {
370
+ sections.push("No metrics available.");
371
+ }
372
+
373
+ // Expertise section
374
+ if (expertiseOutput !== null) {
375
+ sections.push("\n## Expertise");
376
+ sections.push(expertiseOutput.trim());
377
+ }
378
+ }
379
+
380
+ process.stdout.write(`${sections.join("\n")}\n`);
381
+ }