@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,311 @@
1
+ /**
2
+ * CLI command: overstory worktree list | clean [--completed] [--all]
3
+ *
4
+ * List shows worktrees with agent status.
5
+ * Clean removes worktree dirs, branch refs (if merged), and tmux sessions.
6
+ * Logs are never auto-deleted.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { loadConfig } from "../config.ts";
11
+ import { ValidationError } from "../errors.ts";
12
+ import { createMailStore } from "../mail/store.ts";
13
+ import { openSessionStore } from "../sessions/compat.ts";
14
+ import type { AgentSession } from "../types.ts";
15
+ import { isBranchMerged, listWorktrees, removeWorktree } from "../worktree/manager.ts";
16
+ import { isSessionAlive, killSession } from "../worktree/tmux.ts";
17
+
18
+ function hasFlag(args: string[], flag: string): boolean {
19
+ return args.includes(flag);
20
+ }
21
+
22
+ /**
23
+ * Handle `overstory worktree list`.
24
+ */
25
+ async function handleList(root: string, json: boolean): Promise<void> {
26
+ const worktrees = await listWorktrees(root);
27
+ const overstoryDir = join(root, ".overstory");
28
+ const { store } = openSessionStore(overstoryDir);
29
+ let sessions: AgentSession[];
30
+ try {
31
+ sessions = store.getAll();
32
+ } finally {
33
+ store.close();
34
+ }
35
+
36
+ const overstoryWts = worktrees.filter((wt) => wt.branch.startsWith("overstory/"));
37
+
38
+ if (json) {
39
+ const entries = overstoryWts.map((wt) => {
40
+ const session = sessions.find((s) => s.worktreePath === wt.path);
41
+ return {
42
+ path: wt.path,
43
+ branch: wt.branch,
44
+ head: wt.head,
45
+ agentName: session?.agentName ?? null,
46
+ state: session?.state ?? null,
47
+ beadId: session?.beadId ?? null,
48
+ };
49
+ });
50
+ process.stdout.write(`${JSON.stringify(entries, null, "\t")}\n`);
51
+ return;
52
+ }
53
+
54
+ if (overstoryWts.length === 0) {
55
+ process.stdout.write("No agent worktrees found.\n");
56
+ return;
57
+ }
58
+
59
+ process.stdout.write(`🌳 Agent worktrees: ${overstoryWts.length}\n\n`);
60
+ for (const wt of overstoryWts) {
61
+ const session = sessions.find((s) => s.worktreePath === wt.path);
62
+ const state = session?.state ?? "unknown";
63
+ const agent = session?.agentName ?? "?";
64
+ const bead = session?.beadId ?? "?";
65
+ process.stdout.write(` ${wt.branch}\n`);
66
+ process.stdout.write(` Agent: ${agent} | State: ${state} | Task: ${bead}\n`);
67
+ process.stdout.write(` Path: ${wt.path}\n\n`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Handle `overstory worktree clean [--completed] [--all] [--force]`.
73
+ */
74
+ async function handleClean(
75
+ args: string[],
76
+ root: string,
77
+ json: boolean,
78
+ canonicalBranch: string,
79
+ ): Promise<void> {
80
+ const all = hasFlag(args, "--all");
81
+ const force = hasFlag(args, "--force");
82
+ const completedOnly = hasFlag(args, "--completed") || !all;
83
+
84
+ const worktrees = await listWorktrees(root);
85
+ const overstoryDir = join(root, ".overstory");
86
+ const { store } = openSessionStore(overstoryDir);
87
+
88
+ let sessions: AgentSession[];
89
+ try {
90
+ sessions = store.getAll();
91
+ } catch {
92
+ store.close();
93
+ return;
94
+ }
95
+
96
+ const overstoryWts = worktrees.filter((wt) => wt.branch.startsWith("overstory/"));
97
+ const cleaned: string[] = [];
98
+ const failed: string[] = [];
99
+ const skipped: string[] = [];
100
+
101
+ try {
102
+ for (const wt of overstoryWts) {
103
+ const session = sessions.find((s) => s.worktreePath === wt.path);
104
+
105
+ // If --completed (default), only clean worktrees whose agent is done/zombie
106
+ if (completedOnly && session && session.state !== "completed" && session.state !== "zombie") {
107
+ continue;
108
+ }
109
+
110
+ // Check if the branch has been merged into the canonical branch (unless --force)
111
+ if (!force && wt.branch.length > 0) {
112
+ let merged = false;
113
+ try {
114
+ merged = await isBranchMerged(root, wt.branch, canonicalBranch);
115
+ } catch {
116
+ // If we can't determine merge status, treat as unmerged (safe default)
117
+ merged = false;
118
+ }
119
+
120
+ if (!merged) {
121
+ skipped.push(wt.branch);
122
+ continue;
123
+ }
124
+ }
125
+
126
+ // If --all, clean everything
127
+ // Kill tmux session if still alive
128
+ if (session?.tmuxSession) {
129
+ const alive = await isSessionAlive(session.tmuxSession);
130
+ if (alive) {
131
+ try {
132
+ await killSession(session.tmuxSession);
133
+ } catch {
134
+ // Best effort
135
+ }
136
+ }
137
+ }
138
+
139
+ // Warn about force-deleting unmerged branch
140
+ if (force && wt.branch.length > 0) {
141
+ let merged = false;
142
+ try {
143
+ merged = await isBranchMerged(root, wt.branch, canonicalBranch);
144
+ } catch {
145
+ merged = false;
146
+ }
147
+ if (!merged && !json) {
148
+ process.stdout.write(`⚠️ Force-deleting unmerged branch: ${wt.branch}\n`);
149
+ }
150
+ }
151
+
152
+ // Remove worktree and its branch.
153
+ // Always force worktree removal since deployed .claude/ files create untracked
154
+ // files that cause non-forced removal to fail.
155
+ // Always force-delete the branch since we're cleaning up finished/zombie agents
156
+ // whose branches are typically unmerged.
157
+ try {
158
+ await removeWorktree(root, wt.path, { force: true, forceBranch: true });
159
+ cleaned.push(wt.branch);
160
+
161
+ if (!json) {
162
+ process.stdout.write(`🗑️ Removed: ${wt.branch}\n`);
163
+ }
164
+ } catch (err) {
165
+ failed.push(wt.branch);
166
+ if (!json) {
167
+ const msg = err instanceof Error ? err.message : String(err);
168
+ process.stderr.write(`⚠️ Failed to remove ${wt.branch}: ${msg}\n`);
169
+ }
170
+ }
171
+ }
172
+
173
+ // Purge mail for cleaned agents
174
+ let mailPurged = 0;
175
+ if (cleaned.length > 0) {
176
+ const mailDbPath = join(root, ".overstory", "mail.db");
177
+ const mailDbFile = Bun.file(mailDbPath);
178
+ if (await mailDbFile.exists()) {
179
+ const mailStore = createMailStore(mailDbPath);
180
+ try {
181
+ for (const branch of cleaned) {
182
+ const session = sessions.find((s) => s.branchName === branch);
183
+ if (session) {
184
+ mailPurged += mailStore.purge({ agent: session.agentName });
185
+ }
186
+ }
187
+ } finally {
188
+ mailStore.close();
189
+ }
190
+ }
191
+ }
192
+
193
+ // Mark cleaned sessions as zombie in the SessionStore
194
+ for (const branch of cleaned) {
195
+ const session = sessions.find((s) => s.branchName === branch);
196
+ if (session) {
197
+ store.updateState(session.agentName, "zombie");
198
+ }
199
+ }
200
+
201
+ // Prune zombie entries whose worktree paths no longer exist on disk.
202
+ // This prevents the session store from growing unbounded with stale entries.
203
+ const remainingWorktrees = await listWorktrees(root);
204
+ const worktreePaths = new Set(remainingWorktrees.map((wt) => wt.path));
205
+ let pruneCount = 0;
206
+
207
+ // Re-read sessions after state updates to get current zombie list
208
+ const currentSessions = store.getAll();
209
+ for (const session of currentSessions) {
210
+ if (session.state === "zombie" && !worktreePaths.has(session.worktreePath)) {
211
+ store.remove(session.agentName);
212
+ pruneCount++;
213
+ }
214
+ }
215
+
216
+ if (json) {
217
+ process.stdout.write(
218
+ `${JSON.stringify({ cleaned, failed, skipped, pruned: pruneCount, mailPurged })}\n`,
219
+ );
220
+ } else if (
221
+ cleaned.length === 0 &&
222
+ pruneCount === 0 &&
223
+ failed.length === 0 &&
224
+ skipped.length === 0
225
+ ) {
226
+ process.stdout.write("No worktrees to clean.\n");
227
+ } else {
228
+ if (cleaned.length > 0) {
229
+ process.stdout.write(
230
+ `\nCleaned ${cleaned.length} worktree${cleaned.length === 1 ? "" : "s"}.\n`,
231
+ );
232
+ }
233
+ if (failed.length > 0) {
234
+ process.stdout.write(
235
+ `Failed to clean ${failed.length} worktree${failed.length === 1 ? "" : "s"}.\n`,
236
+ );
237
+ }
238
+ if (mailPurged > 0) {
239
+ process.stdout.write(
240
+ `Purged ${mailPurged} mail message${mailPurged === 1 ? "" : "s"} from cleaned agents.\n`,
241
+ );
242
+ }
243
+ if (pruneCount > 0) {
244
+ process.stdout.write(
245
+ `Pruned ${pruneCount} zombie session${pruneCount === 1 ? "" : "s"} from store.\n`,
246
+ );
247
+ }
248
+ if (skipped.length > 0) {
249
+ process.stdout.write(
250
+ `\n⚠️ Skipped ${skipped.length} worktree${skipped.length === 1 ? "" : "s"} with unmerged branches:\n`,
251
+ );
252
+ for (const branch of skipped) {
253
+ process.stdout.write(` ${branch}\n`);
254
+ }
255
+ process.stdout.write("Use --force to delete unmerged branches.\n");
256
+ }
257
+ }
258
+ } finally {
259
+ store.close();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Entry point for `overstory worktree <subcommand> [flags]`.
265
+ *
266
+ * Subcommands: list, clean.
267
+ */
268
+ const WORKTREE_HELP = `overstory worktree — Manage agent worktrees
269
+
270
+ Usage: overstory worktree <subcommand> [flags]
271
+
272
+ Subcommands:
273
+ list List worktrees with agent status
274
+ clean Remove completed worktrees
275
+ [--completed] Only finished agents (default)
276
+ [--all] Force remove all
277
+ [--force] Delete even if branches are unmerged
278
+
279
+ Options:
280
+ --json Output as JSON
281
+ --help, -h Show this help`;
282
+
283
+ export async function worktreeCommand(args: string[]): Promise<void> {
284
+ if (args.includes("--help") || args.includes("-h")) {
285
+ process.stdout.write(`${WORKTREE_HELP}\n`);
286
+ return;
287
+ }
288
+
289
+ const subcommand = args[0];
290
+ const subArgs = args.slice(1);
291
+ const jsonFlag = hasFlag(args, "--json");
292
+
293
+ const cwd = process.cwd();
294
+ const config = await loadConfig(cwd);
295
+ const root = config.project.root;
296
+ const canonicalBranch = config.project.canonicalBranch;
297
+
298
+ switch (subcommand) {
299
+ case "list":
300
+ await handleList(root, jsonFlag);
301
+ break;
302
+ case "clean":
303
+ await handleClean(subArgs, root, jsonFlag, canonicalBranch);
304
+ break;
305
+ default:
306
+ throw new ValidationError(
307
+ `Unknown worktree subcommand: ${subcommand ?? "(none)"}. Use: list, clean`,
308
+ { field: "subcommand" },
309
+ );
310
+ }
311
+ }