@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,355 @@
1
+ /**
2
+ * CLI command: overstory merge
3
+ *
4
+ * Merges agent branches back to the canonical branch using
5
+ * the merge queue and tiered conflict resolver.
6
+ *
7
+ * Usage:
8
+ * overstory merge --branch <name> Merge a specific branch
9
+ * overstory merge --all Merge all pending branches
10
+ * overstory merge --dry-run Check for conflicts without merging
11
+ * overstory merge --json Output results as JSON
12
+ */
13
+
14
+ import { join } from "node:path";
15
+ import { loadConfig } from "../config.ts";
16
+ import { MergeError, ValidationError } from "../errors.ts";
17
+ import { createMergeQueue } from "../merge/queue.ts";
18
+ import { createMergeResolver } from "../merge/resolver.ts";
19
+ import { createMulchClient } from "../mulch/client.ts";
20
+ import type { MergeEntry, MergeResult } from "../types.ts";
21
+
22
+ /**
23
+ * Parse a named flag value from an args array.
24
+ * Returns the value after the flag, or undefined if not present.
25
+ */
26
+ function getFlag(args: string[], flag: string): string | undefined {
27
+ const idx = args.indexOf(flag);
28
+ if (idx === -1 || idx + 1 >= args.length) {
29
+ return undefined;
30
+ }
31
+ return args[idx + 1];
32
+ }
33
+
34
+ /** Check if a boolean flag is present in the args. */
35
+ function hasFlag(args: string[], flag: string): boolean {
36
+ return args.includes(flag);
37
+ }
38
+
39
+ /**
40
+ * Extract agent name from a branch following the overstory naming convention.
41
+ * Pattern: overstory/{agentName}/{beadId}
42
+ * Falls back to "unknown" if the pattern does not match.
43
+ */
44
+ function parseAgentName(branchName: string): string {
45
+ const parts = branchName.split("/");
46
+ if (parts[0] === "overstory" && parts[1] !== undefined) {
47
+ return parts[1];
48
+ }
49
+ return "unknown";
50
+ }
51
+
52
+ /**
53
+ * Extract bead ID from a branch following the overstory naming convention.
54
+ * Pattern: overstory/{agentName}/{beadId}
55
+ * Falls back to "unknown" if the pattern does not match.
56
+ */
57
+ function parseBeadId(branchName: string): string {
58
+ const parts = branchName.split("/");
59
+ if (parts[0] === "overstory" && parts[2] !== undefined) {
60
+ return parts[2];
61
+ }
62
+ return "unknown";
63
+ }
64
+
65
+ /**
66
+ * Detect modified files between a branch and the canonical branch using git diff.
67
+ * Returns an array of file paths that differ.
68
+ */
69
+ async function detectModifiedFiles(
70
+ repoRoot: string,
71
+ canonicalBranch: string,
72
+ branchName: string,
73
+ ): Promise<string[]> {
74
+ const proc = Bun.spawn(["git", "diff", "--name-only", `${canonicalBranch}...${branchName}`], {
75
+ cwd: repoRoot,
76
+ stdout: "pipe",
77
+ stderr: "pipe",
78
+ });
79
+ const exitCode = await proc.exited;
80
+
81
+ if (exitCode !== 0) {
82
+ const stderr = await new Response(proc.stderr).text();
83
+ throw new MergeError(
84
+ `Failed to detect modified files for branch "${branchName}": ${stderr.trim()}`,
85
+ { branchName },
86
+ );
87
+ }
88
+
89
+ const stdout = await new Response(proc.stdout).text();
90
+ return stdout
91
+ .trim()
92
+ .split("\n")
93
+ .filter((line) => line.length > 0);
94
+ }
95
+
96
+ /** Format a single merge result for human-readable output. */
97
+ function formatResult(result: MergeResult): string {
98
+ const statusIcon = result.success ? "Merged" : "Failed";
99
+ const lines: string[] = [
100
+ `Merging branch: ${result.entry.branchName}`,
101
+ ` Agent: ${result.entry.agentName} | Task: ${result.entry.beadId}`,
102
+ ` Files: ${result.entry.filesModified.length} modified`,
103
+ ` Result: ${statusIcon} (tier: ${result.tier})`,
104
+ ];
105
+
106
+ if (result.conflictFiles.length > 0) {
107
+ lines.push(` Conflicts: ${result.conflictFiles.join(", ")}`);
108
+ }
109
+
110
+ if (result.errorMessage) {
111
+ lines.push(` Error: ${result.errorMessage}`);
112
+ }
113
+
114
+ return lines.join("\n");
115
+ }
116
+
117
+ /** Format a dry-run report for a merge entry. */
118
+ function formatDryRun(entry: MergeEntry): string {
119
+ const lines: string[] = [
120
+ `[dry-run] Branch: ${entry.branchName}`,
121
+ ` Agent: ${entry.agentName} | Task: ${entry.beadId}`,
122
+ ` Status: ${entry.status}`,
123
+ ` Files: ${entry.filesModified.length} modified`,
124
+ ];
125
+
126
+ if (entry.filesModified.length > 0) {
127
+ for (const f of entry.filesModified) {
128
+ lines.push(` - ${f}`);
129
+ }
130
+ }
131
+
132
+ return lines.join("\n");
133
+ }
134
+
135
+ /**
136
+ * Entry point for `overstory merge [flags]`.
137
+ *
138
+ * Flags:
139
+ * --branch <name> Merge a specific branch
140
+ * --all Merge all pending branches in the queue
141
+ * --dry-run Check for conflicts without actually merging
142
+ * --json Output results as JSON
143
+ */
144
+ const MERGE_HELP = `overstory merge — Merge agent branches into canonical
145
+
146
+ Usage: overstory merge --branch <name> | --all [--into <branch>] [--dry-run] [--json]
147
+
148
+ Options:
149
+ --branch <name> Merge a specific branch
150
+ --all Merge all pending branches in the queue
151
+ --into <branch> Target branch to merge into (default: config canonicalBranch)
152
+ --dry-run Check for conflicts without actually merging
153
+ --json Output results as JSON
154
+ --help, -h Show this help`;
155
+
156
+ export async function mergeCommand(args: string[]): Promise<void> {
157
+ if (args.includes("--help") || args.includes("-h")) {
158
+ process.stdout.write(`${MERGE_HELP}\n`);
159
+ return;
160
+ }
161
+
162
+ const branchName = getFlag(args, "--branch");
163
+ const all = hasFlag(args, "--all");
164
+ const into = getFlag(args, "--into");
165
+ const dryRun = hasFlag(args, "--dry-run");
166
+ const json = hasFlag(args, "--json");
167
+
168
+ if (!branchName && !all) {
169
+ throw new ValidationError("Either --branch <name> or --all is required for overstory merge", {
170
+ field: "branch|all",
171
+ });
172
+ }
173
+
174
+ const cwd = process.cwd();
175
+ const config = await loadConfig(cwd);
176
+
177
+ // Resolution chain: --into flag > session-start branch > config canonicalBranch
178
+ let sessionBranch: string | null = null;
179
+ if (into === undefined) {
180
+ const sessionBranchPath = join(config.project.root, ".overstory", "session-branch.txt");
181
+ const sessionBranchFile = Bun.file(sessionBranchPath);
182
+ if (await sessionBranchFile.exists()) {
183
+ const content = (await sessionBranchFile.text()).trim();
184
+ if (content) {
185
+ sessionBranch = content;
186
+ }
187
+ }
188
+ }
189
+ const targetBranch = into ?? sessionBranch ?? config.project.canonicalBranch;
190
+ const queuePath = join(config.project.root, ".overstory", "merge-queue.db");
191
+ const queue = createMergeQueue(queuePath);
192
+ const mulchClient = createMulchClient(config.project.root);
193
+ const resolver = createMergeResolver({
194
+ aiResolveEnabled: config.merge.aiResolveEnabled,
195
+ reimagineEnabled: config.merge.reimagineEnabled,
196
+ mulchClient,
197
+ });
198
+
199
+ if (branchName) {
200
+ await handleBranch(branchName, queue, resolver, config, targetBranch, dryRun, json);
201
+ } else {
202
+ await handleAll(queue, resolver, config, targetBranch, dryRun, json);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Handle merging a specific branch.
208
+ * If the branch is not in the queue, creates a new entry by detecting
209
+ * agent name, bead ID, and modified files from git.
210
+ */
211
+ async function handleBranch(
212
+ branchName: string,
213
+ queue: ReturnType<typeof createMergeQueue>,
214
+ resolver: ReturnType<typeof createMergeResolver>,
215
+ config: Awaited<ReturnType<typeof loadConfig>>,
216
+ targetBranch: string,
217
+ dryRun: boolean,
218
+ json: boolean,
219
+ ): Promise<void> {
220
+ const canonicalBranch = targetBranch;
221
+ const repoRoot = config.project.root;
222
+
223
+ // Look for existing entry in the queue
224
+ const allEntries = queue.list();
225
+ let entry = allEntries.find((e) => e.branchName === branchName) ?? null;
226
+
227
+ // If not in queue, create one by detecting info from the branch
228
+ if (entry === null) {
229
+ // Validate that the branch exists before attempting any git operations
230
+ const verifyProc = Bun.spawn(["git", "rev-parse", "--verify", `refs/heads/${branchName}`], {
231
+ cwd: repoRoot,
232
+ stdout: "pipe",
233
+ stderr: "pipe",
234
+ });
235
+ const verifyExit = await verifyProc.exited;
236
+ if (verifyExit !== 0) {
237
+ throw new ValidationError(`Branch "${branchName}" not found`, {
238
+ field: "branch",
239
+ value: branchName,
240
+ });
241
+ }
242
+
243
+ const agentName = parseAgentName(branchName);
244
+ const beadId = parseBeadId(branchName);
245
+ const filesModified = await detectModifiedFiles(repoRoot, canonicalBranch, branchName);
246
+
247
+ entry = queue.enqueue({
248
+ branchName,
249
+ beadId,
250
+ agentName,
251
+ filesModified,
252
+ });
253
+ }
254
+
255
+ if (dryRun) {
256
+ if (json) {
257
+ process.stdout.write(`${JSON.stringify(entry)}\n`);
258
+ } else {
259
+ process.stdout.write(`${formatDryRun(entry)}\n`);
260
+ }
261
+ return;
262
+ }
263
+
264
+ // Perform the actual merge
265
+ const result = await resolver.resolve(entry, canonicalBranch, repoRoot);
266
+
267
+ // Update queue status based on result
268
+ queue.updateStatus(branchName, result.success ? "merged" : "conflict", result.tier);
269
+
270
+ if (json) {
271
+ process.stdout.write(`${JSON.stringify(result)}\n`);
272
+ } else {
273
+ process.stdout.write(`${formatResult(result)}\n`);
274
+ }
275
+
276
+ if (!result.success) {
277
+ throw new MergeError(result.errorMessage ?? `Merge failed for branch "${branchName}"`, {
278
+ branchName,
279
+ conflictFiles: result.conflictFiles,
280
+ });
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Handle merging all pending branches in the queue.
286
+ * Processes entries sequentially in FIFO order.
287
+ */
288
+ async function handleAll(
289
+ queue: ReturnType<typeof createMergeQueue>,
290
+ resolver: ReturnType<typeof createMergeResolver>,
291
+ config: Awaited<ReturnType<typeof loadConfig>>,
292
+ targetBranch: string,
293
+ dryRun: boolean,
294
+ json: boolean,
295
+ ): Promise<void> {
296
+ const canonicalBranch = targetBranch;
297
+ const repoRoot = config.project.root;
298
+
299
+ const pendingEntries = queue.list("pending");
300
+
301
+ if (pendingEntries.length === 0) {
302
+ if (json) {
303
+ process.stdout.write(`${JSON.stringify({ results: [], count: 0 })}\n`);
304
+ } else {
305
+ process.stdout.write("No pending branches to merge.\n");
306
+ }
307
+ return;
308
+ }
309
+
310
+ if (dryRun) {
311
+ if (json) {
312
+ process.stdout.write(`${JSON.stringify(pendingEntries)}\n`);
313
+ } else {
314
+ process.stdout.write(
315
+ `${pendingEntries.length} pending branch${pendingEntries.length === 1 ? "" : "es"}:\n\n`,
316
+ );
317
+ for (const entry of pendingEntries) {
318
+ process.stdout.write(`${formatDryRun(entry)}\n\n`);
319
+ }
320
+ }
321
+ return;
322
+ }
323
+
324
+ const results: MergeResult[] = [];
325
+ let successCount = 0;
326
+ let failCount = 0;
327
+
328
+ for (const entry of pendingEntries) {
329
+ const result = await resolver.resolve(entry, canonicalBranch, repoRoot);
330
+
331
+ queue.updateStatus(entry.branchName, result.success ? "merged" : "conflict", result.tier);
332
+
333
+ results.push(result);
334
+
335
+ if (result.success) {
336
+ successCount++;
337
+ } else {
338
+ failCount++;
339
+ }
340
+
341
+ if (!json) {
342
+ process.stdout.write(`${formatResult(result)}\n\n`);
343
+ }
344
+ }
345
+
346
+ if (json) {
347
+ process.stdout.write(
348
+ `${JSON.stringify({ results, count: results.length, successCount, failCount })}\n`,
349
+ );
350
+ } else {
351
+ process.stdout.write(
352
+ `Done: ${successCount} merged, ${failCount} failed out of ${results.length} total.\n`,
353
+ );
354
+ }
355
+ }