@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,216 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { createMergeQueue } from "../merge/queue.ts";
7
+ import type { OverstoryConfig } from "../types.ts";
8
+ import { checkMergeQueue } from "./merge-queue.ts";
9
+ import type { DoctorCheck } from "./types.ts";
10
+
11
+ describe("checkMergeQueue", () => {
12
+ let tempDir: string;
13
+ let mockConfig: OverstoryConfig;
14
+
15
+ beforeEach(() => {
16
+ tempDir = mkdtempSync(join(tmpdir(), "overstory-test-"));
17
+ mockConfig = {
18
+ project: { name: "test", root: tempDir, canonicalBranch: "main" },
19
+ agents: {
20
+ manifestPath: "",
21
+ baseDir: "",
22
+ maxConcurrent: 5,
23
+ staggerDelayMs: 100,
24
+ maxDepth: 2,
25
+ maxSessionsPerRun: 0,
26
+ },
27
+ worktrees: { baseDir: "" },
28
+ taskTracker: { backend: "auto", enabled: true },
29
+ mulch: { enabled: true, domains: [], primeFormat: "markdown" },
30
+ merge: { aiResolveEnabled: false, reimagineEnabled: false },
31
+ providers: {
32
+ anthropic: { type: "native" },
33
+ },
34
+ watchdog: {
35
+ tier0Enabled: true,
36
+ tier0IntervalMs: 30000,
37
+ tier1Enabled: false,
38
+ tier2Enabled: false,
39
+ staleThresholdMs: 300000,
40
+ zombieThresholdMs: 600000,
41
+ nudgeIntervalMs: 60000,
42
+ },
43
+ models: {},
44
+ logging: { verbose: false, redactSecrets: true },
45
+ };
46
+ });
47
+
48
+ afterEach(() => {
49
+ rmSync(tempDir, { recursive: true, force: true });
50
+ });
51
+
52
+ test("passes when merge queue db does not exist", () => {
53
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
54
+
55
+ expect(checks).toHaveLength(1);
56
+ expect(checks[0]?.status).toBe("pass");
57
+ expect(checks[0]?.name).toBe("merge-queue.db exists");
58
+ expect(checks[0]?.message).toContain("normal for new installations");
59
+ });
60
+
61
+ test("passes when merge queue is empty", () => {
62
+ const dbPath = join(tempDir, "merge-queue.db");
63
+ // Create empty queue
64
+ const queue = createMergeQueue(dbPath);
65
+ queue.close();
66
+
67
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
68
+
69
+ expect(checks).toHaveLength(1);
70
+ expect(checks[0]?.status).toBe("pass");
71
+ expect(checks[0]?.name).toBe("merge-queue.db schema");
72
+ expect(checks[0]?.message).toBe("Merge queue has 0 entries");
73
+ });
74
+
75
+ test("passes with valid queue entries", () => {
76
+ const dbPath = join(tempDir, "merge-queue.db");
77
+ const queue = createMergeQueue(dbPath);
78
+ queue.enqueue({
79
+ branchName: "feature/test",
80
+ beadId: "beads-abc",
81
+ agentName: "test-agent",
82
+ filesModified: ["src/test.ts"],
83
+ });
84
+ queue.enqueue({
85
+ branchName: "feature/another",
86
+ beadId: "beads-def",
87
+ agentName: "another-agent",
88
+ filesModified: ["src/another.ts"],
89
+ });
90
+ // Mark second entry as merged
91
+ const entry = queue.list()[1];
92
+ if (entry) {
93
+ queue.updateStatus(entry.branchName, "merged", "clean-merge");
94
+ }
95
+ queue.close();
96
+
97
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
98
+
99
+ expect(checks).toHaveLength(1);
100
+ expect(checks[0]?.status).toBe("pass");
101
+ expect(checks[0]?.name).toBe("merge-queue.db schema");
102
+ expect(checks[0]?.message).toBe("Merge queue has 2 entries");
103
+ });
104
+
105
+ test("fails when db is corrupted", () => {
106
+ const dbPath = join(tempDir, "merge-queue.db");
107
+ // Write invalid data to db file
108
+ const fs = require("node:fs");
109
+ fs.writeFileSync(dbPath, "not a valid sqlite database");
110
+
111
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
112
+
113
+ expect(checks).toHaveLength(1);
114
+ expect(checks[0]?.status).toBe("fail");
115
+ expect(checks[0]?.name).toBe("merge-queue.db readable");
116
+ expect(checks[0]?.message).toContain("Failed to read merge-queue.db");
117
+ });
118
+
119
+ test("fails when table does not exist", () => {
120
+ const dbPath = join(tempDir, "merge-queue.db");
121
+ // Create a db but without the merge_queue table
122
+ const db = new Database(dbPath);
123
+ db.exec("CREATE TABLE other_table (id INTEGER PRIMARY KEY)");
124
+ db.close();
125
+
126
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
127
+
128
+ const schemaCheck = checks.find((c) => c?.name === "merge-queue.db schema");
129
+ expect(schemaCheck?.status).toBe("fail");
130
+ expect(schemaCheck?.message).toContain("merge_queue table not found");
131
+ });
132
+
133
+ test("warns about stale pending entries", () => {
134
+ const dbPath = join(tempDir, "merge-queue.db");
135
+ // Create queue and manually insert stale entry (2 days old)
136
+ const queue = createMergeQueue(dbPath);
137
+ queue.close();
138
+
139
+ const staleDate = new Date();
140
+ staleDate.setDate(staleDate.getDate() - 2); // 2 days ago
141
+
142
+ const db = new Database(dbPath);
143
+ db.prepare(
144
+ "INSERT INTO merge_queue (branch_name, task_id, agent_name, files_modified, status, enqueued_at) VALUES (?, ?, ?, ?, ?, ?)",
145
+ ).run(
146
+ "feature/stale",
147
+ "beads-abc",
148
+ "test-agent",
149
+ JSON.stringify(["src/test.ts"]),
150
+ "pending",
151
+ staleDate.toISOString(),
152
+ );
153
+ db.close();
154
+
155
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
156
+
157
+ const staleCheck = checks.find((c) => c?.name === "merge-queue.db staleness");
158
+ expect(staleCheck?.status).toBe("warn");
159
+ expect(staleCheck?.message).toContain("potentially stale");
160
+ expect(staleCheck?.details?.[0]).toContain("feature/stale");
161
+ });
162
+
163
+ test("does not warn about old completed entries", () => {
164
+ const dbPath = join(tempDir, "merge-queue.db");
165
+ // Create queue and manually insert old merged entry (2 days ago)
166
+ const queue = createMergeQueue(dbPath);
167
+ queue.close();
168
+
169
+ const oldDate = new Date();
170
+ oldDate.setDate(oldDate.getDate() - 2); // 2 days ago
171
+
172
+ const db = new Database(dbPath);
173
+ db.prepare(
174
+ "INSERT INTO merge_queue (branch_name, task_id, agent_name, files_modified, status, enqueued_at, resolved_tier) VALUES (?, ?, ?, ?, ?, ?, ?)",
175
+ ).run(
176
+ "feature/old-merged",
177
+ "beads-abc",
178
+ "test-agent",
179
+ JSON.stringify(["src/test.ts"]),
180
+ "merged",
181
+ oldDate.toISOString(),
182
+ "clean-merge",
183
+ );
184
+ db.close();
185
+
186
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
187
+
188
+ const staleCheck = checks.find((c) => c?.name === "merge-queue.db staleness");
189
+ expect(staleCheck).toBeUndefined();
190
+ });
191
+
192
+ test("warns about duplicate branches", () => {
193
+ const dbPath = join(tempDir, "merge-queue.db");
194
+ const queue = createMergeQueue(dbPath);
195
+ queue.enqueue({
196
+ branchName: "feature/duplicate",
197
+ beadId: "beads-abc",
198
+ agentName: "test-agent",
199
+ filesModified: ["src/test.ts"],
200
+ });
201
+ queue.enqueue({
202
+ branchName: "feature/duplicate",
203
+ beadId: "beads-def",
204
+ agentName: "another-agent",
205
+ filesModified: ["src/another.ts"],
206
+ });
207
+ queue.close();
208
+
209
+ const checks = checkMergeQueue(mockConfig, tempDir) as DoctorCheck[];
210
+
211
+ const duplicateCheck = checks.find((c) => c?.name === "merge-queue.db duplicates");
212
+ expect(duplicateCheck?.status).toBe("warn");
213
+ expect(duplicateCheck?.message).toContain("duplicate branch entries");
214
+ expect(duplicateCheck?.details?.[0]).toContain("feature/duplicate");
215
+ });
216
+ });
@@ -0,0 +1,144 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
5
+
6
+ /**
7
+ * Merge queue health checks.
8
+ * Validates merge-queue.db schema and detects stale entries.
9
+ */
10
+ export const checkMergeQueue: DoctorCheckFn = (_config, overstoryDir): DoctorCheck[] => {
11
+ const checks: DoctorCheck[] = [];
12
+ const dbPath = join(overstoryDir, "merge-queue.db");
13
+
14
+ if (!existsSync(dbPath)) {
15
+ checks.push({
16
+ name: "merge-queue.db exists",
17
+ category: "merge",
18
+ status: "pass",
19
+ message: "No merge queue database (normal for new installations or no merges yet)",
20
+ });
21
+ return checks;
22
+ }
23
+
24
+ let db: Database;
25
+ try {
26
+ db = new Database(dbPath, { readonly: true });
27
+ } catch (err) {
28
+ checks.push({
29
+ name: "merge-queue.db readable",
30
+ category: "merge",
31
+ status: "fail",
32
+ message: "Failed to open merge-queue.db",
33
+ details: [err instanceof Error ? err.message : String(err)],
34
+ fixable: true,
35
+ });
36
+ return checks;
37
+ }
38
+
39
+ try {
40
+ // Check table exists
41
+ let tableCheck: unknown;
42
+ try {
43
+ tableCheck = db
44
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='merge_queue'")
45
+ .get();
46
+ } catch (err) {
47
+ // Database is corrupted or not a valid SQLite file
48
+ checks.push({
49
+ name: "merge-queue.db readable",
50
+ category: "merge",
51
+ status: "fail",
52
+ message: "Failed to read merge-queue.db",
53
+ details: [err instanceof Error ? err.message : String(err)],
54
+ fixable: true,
55
+ });
56
+ db.close();
57
+ return checks;
58
+ }
59
+
60
+ if (!tableCheck) {
61
+ checks.push({
62
+ name: "merge-queue.db schema",
63
+ category: "merge",
64
+ status: "fail",
65
+ message: "merge_queue table not found in database",
66
+ fixable: true,
67
+ });
68
+ db.close();
69
+ return checks;
70
+ }
71
+
72
+ // Read all entries
73
+ const rows = db.prepare("SELECT * FROM merge_queue ORDER BY id ASC").all() as Array<{
74
+ branch_name: string;
75
+ agent_name: string;
76
+ status: string;
77
+ enqueued_at: string;
78
+ resolved_tier: string | null;
79
+ }>;
80
+
81
+ checks.push({
82
+ name: "merge-queue.db schema",
83
+ category: "merge",
84
+ status: "pass",
85
+ message: `Merge queue has ${rows.length} entries`,
86
+ });
87
+
88
+ // Check for stale entries (pending/merging older than 24h)
89
+ const now = new Date();
90
+ const staleThresholdMs = 24 * 60 * 60 * 1000;
91
+ const staleEntries: string[] = [];
92
+ for (const row of rows) {
93
+ if (row.status === "pending" || row.status === "merging") {
94
+ try {
95
+ const enqueuedAt = new Date(row.enqueued_at);
96
+ const ageMs = now.getTime() - enqueuedAt.getTime();
97
+ if (ageMs > staleThresholdMs) {
98
+ const ageHours = Math.floor(ageMs / (60 * 60 * 1000));
99
+ staleEntries.push(
100
+ `${row.branch_name} (${row.status}, ${ageHours}h old) - may be stuck`,
101
+ );
102
+ }
103
+ } catch {
104
+ /* invalid date */
105
+ }
106
+ }
107
+ }
108
+
109
+ if (staleEntries.length > 0) {
110
+ checks.push({
111
+ name: "merge-queue.db staleness",
112
+ category: "merge",
113
+ status: "warn",
114
+ message: `Found ${staleEntries.length} potentially stale queue entries`,
115
+ details: staleEntries,
116
+ fixable: true,
117
+ });
118
+ }
119
+
120
+ // Check for duplicate branches
121
+ const branchCounts = new Map<string, number>();
122
+ for (const row of rows) {
123
+ branchCounts.set(row.branch_name, (branchCounts.get(row.branch_name) ?? 0) + 1);
124
+ }
125
+ const duplicates: string[] = [];
126
+ for (const [branch, count] of branchCounts) {
127
+ if (count > 1) duplicates.push(`${branch} (appears ${count} times)`);
128
+ }
129
+ if (duplicates.length > 0) {
130
+ checks.push({
131
+ name: "merge-queue.db duplicates",
132
+ category: "merge",
133
+ status: "warn",
134
+ message: "Found duplicate branch entries in queue",
135
+ details: duplicates,
136
+ fixable: true,
137
+ });
138
+ }
139
+ } finally {
140
+ db.close();
141
+ }
142
+
143
+ return checks;
144
+ };
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Tests for structure doctor checks.
3
+ *
4
+ * Uses temp directories with real filesystem operations.
5
+ * No mocks needed -- all operations are cheap and local.
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import type { OverstoryConfig } from "../types.ts";
13
+ import { checkStructure } from "./structure.ts";
14
+
15
+ describe("checkStructure", () => {
16
+ let tempDir: string;
17
+ let overstoryDir: string;
18
+ let mockConfig: OverstoryConfig;
19
+
20
+ beforeEach(async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), "structure-test-"));
22
+ overstoryDir = join(tempDir, ".overstory");
23
+
24
+ mockConfig = {
25
+ project: {
26
+ name: "test-project",
27
+ root: tempDir,
28
+ canonicalBranch: "main",
29
+ },
30
+ agents: {
31
+ manifestPath: ".overstory/agent-manifest.json",
32
+ baseDir: ".overstory/agent-defs",
33
+ maxConcurrent: 5,
34
+ staggerDelayMs: 1000,
35
+ maxDepth: 2,
36
+ maxSessionsPerRun: 0,
37
+ },
38
+ worktrees: {
39
+ baseDir: ".overstory/worktrees",
40
+ },
41
+ taskTracker: {
42
+ backend: "auto",
43
+ enabled: true,
44
+ },
45
+ mulch: {
46
+ enabled: true,
47
+ domains: [],
48
+ primeFormat: "markdown",
49
+ },
50
+ merge: {
51
+ aiResolveEnabled: false,
52
+ reimagineEnabled: false,
53
+ },
54
+ providers: {
55
+ anthropic: { type: "native" },
56
+ },
57
+ watchdog: {
58
+ tier0Enabled: true,
59
+ tier0IntervalMs: 30000,
60
+ tier1Enabled: false,
61
+ tier2Enabled: false,
62
+ staleThresholdMs: 300000,
63
+ zombieThresholdMs: 600000,
64
+ nudgeIntervalMs: 60000,
65
+ },
66
+ models: {},
67
+ logging: {
68
+ verbose: false,
69
+ redactSecrets: true,
70
+ },
71
+ };
72
+ });
73
+
74
+ afterEach(async () => {
75
+ await rm(tempDir, { recursive: true, force: true });
76
+ });
77
+
78
+ test("fails when .overstory/ directory does not exist", async () => {
79
+ const checks = await checkStructure(mockConfig, overstoryDir);
80
+
81
+ expect(checks.length).toBeGreaterThan(0);
82
+ const dirCheck = checks.find((c) => c.name === ".overstory/ directory");
83
+ expect(dirCheck).toBeDefined();
84
+ expect(dirCheck?.status).toBe("fail");
85
+ expect(dirCheck?.message).toContain("missing");
86
+ expect(dirCheck?.fixable).toBe(true);
87
+ });
88
+
89
+ test("passes when all required files and directories exist", async () => {
90
+ // Create .overstory/ and all required structure
91
+ await mkdir(overstoryDir, { recursive: true });
92
+ await mkdir(join(overstoryDir, "agent-defs"), { recursive: true });
93
+ await mkdir(join(overstoryDir, "agents"), { recursive: true });
94
+ await mkdir(join(overstoryDir, "worktrees"), { recursive: true });
95
+ await mkdir(join(overstoryDir, "specs"), { recursive: true });
96
+ await mkdir(join(overstoryDir, "logs"), { recursive: true });
97
+
98
+ await Bun.write(join(overstoryDir, "config.yaml"), "project:\n name: test\n");
99
+ await Bun.write(
100
+ join(overstoryDir, "agent-manifest.json"),
101
+ JSON.stringify({ version: "1.0", agents: {}, capabilityIndex: {} }, null, 2),
102
+ );
103
+ await Bun.write(join(overstoryDir, "hooks.json"), "{}");
104
+ await Bun.write(
105
+ join(overstoryDir, ".gitignore"),
106
+ `# Wildcard+whitelist: ignore everything, whitelist tracked files
107
+ # Auto-healed by overstory prime on each session start
108
+ *
109
+ !.gitignore
110
+ !config.yaml
111
+ !agent-manifest.json
112
+ !hooks.json
113
+ !groups.json
114
+ !agent-defs/
115
+ `,
116
+ );
117
+
118
+ const checks = await checkStructure(mockConfig, overstoryDir);
119
+
120
+ // All checks should pass
121
+ const failedChecks = checks.filter((c) => c.status === "fail");
122
+ expect(failedChecks).toHaveLength(0);
123
+
124
+ const dirCheck = checks.find((c) => c.name === ".overstory/ directory");
125
+ expect(dirCheck?.status).toBe("pass");
126
+
127
+ const filesCheck = checks.find((c) => c.name === "Required files");
128
+ expect(filesCheck?.status).toBe("pass");
129
+
130
+ const dirsCheck = checks.find((c) => c.name === "Required subdirectories");
131
+ expect(dirsCheck?.status).toBe("pass");
132
+
133
+ const gitignoreCheck = checks.find((c) => c.name === ".gitignore entries");
134
+ expect(gitignoreCheck?.status).toBe("pass");
135
+ });
136
+
137
+ test("reports missing required files", async () => {
138
+ await mkdir(overstoryDir, { recursive: true });
139
+ await Bun.write(join(overstoryDir, "config.yaml"), "project:\n name: test\n");
140
+ // Missing: agent-manifest.json, hooks.json, .gitignore
141
+
142
+ const checks = await checkStructure(mockConfig, overstoryDir);
143
+
144
+ const filesCheck = checks.find((c) => c.name === "Required files");
145
+ expect(filesCheck).toBeDefined();
146
+ expect(filesCheck?.status).toBe("fail");
147
+ expect(filesCheck?.details).toContain("agent-manifest.json");
148
+ expect(filesCheck?.details).toContain("hooks.json");
149
+ expect(filesCheck?.details).toContain(".gitignore");
150
+ expect(filesCheck?.fixable).toBe(true);
151
+ });
152
+
153
+ test("reports missing required subdirectories", async () => {
154
+ await mkdir(overstoryDir, { recursive: true });
155
+ await mkdir(join(overstoryDir, "agent-defs"), { recursive: true });
156
+ // Missing: agents/, worktrees/, specs/, logs/
157
+
158
+ const checks = await checkStructure(mockConfig, overstoryDir);
159
+
160
+ const dirsCheck = checks.find((c) => c.name === "Required subdirectories");
161
+ expect(dirsCheck).toBeDefined();
162
+ expect(dirsCheck?.status).toBe("fail");
163
+ expect(dirsCheck?.details).toContain("agents/");
164
+ expect(dirsCheck?.details).toContain("worktrees/");
165
+ expect(dirsCheck?.details).toContain("specs/");
166
+ expect(dirsCheck?.details).toContain("logs/");
167
+ expect(dirsCheck?.fixable).toBe(true);
168
+ });
169
+
170
+ test("warns when .gitignore is missing entries", async () => {
171
+ await mkdir(overstoryDir, { recursive: true });
172
+ await Bun.write(
173
+ join(overstoryDir, ".gitignore"),
174
+ `# Incomplete gitignore
175
+ *
176
+ !.gitignore
177
+ !config.yaml
178
+ `,
179
+ );
180
+
181
+ const checks = await checkStructure(mockConfig, overstoryDir);
182
+
183
+ const gitignoreCheck = checks.find((c) => c.name === ".gitignore entries");
184
+ expect(gitignoreCheck).toBeDefined();
185
+ expect(gitignoreCheck?.status).toBe("warn");
186
+ expect(gitignoreCheck?.details).toBeDefined();
187
+ expect(gitignoreCheck?.details?.length).toBeGreaterThan(0);
188
+ expect(gitignoreCheck?.fixable).toBe(true);
189
+ });
190
+
191
+ test("validates agent-defs files against manifest", async () => {
192
+ await mkdir(overstoryDir, { recursive: true });
193
+ await mkdir(join(overstoryDir, "agent-defs"), { recursive: true });
194
+
195
+ const manifest = {
196
+ version: "1.0",
197
+ agents: {
198
+ scout: { file: "scout.md", model: "haiku", tools: [], capabilities: [], canSpawn: false },
199
+ builder: {
200
+ file: "builder.md",
201
+ model: "sonnet",
202
+ tools: [],
203
+ capabilities: [],
204
+ canSpawn: false,
205
+ },
206
+ },
207
+ capabilityIndex: {},
208
+ };
209
+
210
+ await Bun.write(join(overstoryDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
211
+ await Bun.write(join(overstoryDir, "agent-defs", "scout.md"), "# Scout");
212
+ // Missing: builder.md
213
+
214
+ const checks = await checkStructure(mockConfig, overstoryDir);
215
+
216
+ const agentDefsCheck = checks.find((c) => c.name === "Agent definition files");
217
+ expect(agentDefsCheck).toBeDefined();
218
+ expect(agentDefsCheck?.status).toBe("fail");
219
+ expect(agentDefsCheck?.details).toContain("builder.md");
220
+ expect(agentDefsCheck?.fixable).toBe(true);
221
+ });
222
+
223
+ test("passes when all agent-defs files are present", async () => {
224
+ await mkdir(overstoryDir, { recursive: true });
225
+ await mkdir(join(overstoryDir, "agent-defs"), { recursive: true });
226
+
227
+ const manifest = {
228
+ version: "1.0",
229
+ agents: {
230
+ scout: { file: "scout.md", model: "haiku", tools: [], capabilities: [], canSpawn: false },
231
+ builder: {
232
+ file: "builder.md",
233
+ model: "sonnet",
234
+ tools: [],
235
+ capabilities: [],
236
+ canSpawn: false,
237
+ },
238
+ },
239
+ capabilityIndex: {},
240
+ };
241
+
242
+ await Bun.write(join(overstoryDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
243
+ await Bun.write(join(overstoryDir, "agent-defs", "scout.md"), "# Scout");
244
+ await Bun.write(join(overstoryDir, "agent-defs", "builder.md"), "# Builder");
245
+
246
+ const checks = await checkStructure(mockConfig, overstoryDir);
247
+
248
+ const agentDefsCheck = checks.find((c) => c.name === "Agent definition files");
249
+ expect(agentDefsCheck).toBeDefined();
250
+ expect(agentDefsCheck?.status).toBe("pass");
251
+ });
252
+
253
+ test("fails gracefully when manifest is malformed", async () => {
254
+ await mkdir(overstoryDir, { recursive: true });
255
+ await Bun.write(join(overstoryDir, "agent-manifest.json"), "invalid json{");
256
+
257
+ const checks = await checkStructure(mockConfig, overstoryDir);
258
+
259
+ const agentDefsCheck = checks.find((c) => c.name === "Agent definition files");
260
+ expect(agentDefsCheck).toBeDefined();
261
+ expect(agentDefsCheck?.status).toBe("fail");
262
+ expect(agentDefsCheck?.message).toContain("Cannot validate");
263
+ expect(agentDefsCheck?.fixable).toBe(false);
264
+ });
265
+
266
+ test("detects leftover temp files", async () => {
267
+ await mkdir(overstoryDir, { recursive: true });
268
+ await Bun.write(join(overstoryDir, "config.yaml.tmp"), "temp");
269
+ await Bun.write(join(overstoryDir, "old-file.bak"), "backup");
270
+
271
+ const checks = await checkStructure(mockConfig, overstoryDir);
272
+
273
+ const tempFilesCheck = checks.find((c) => c.name === "Leftover temp files");
274
+ expect(tempFilesCheck).toBeDefined();
275
+ expect(tempFilesCheck?.status).toBe("warn");
276
+ expect(tempFilesCheck?.details).toContain("config.yaml.tmp");
277
+ expect(tempFilesCheck?.details).toContain("old-file.bak");
278
+ expect(tempFilesCheck?.fixable).toBe(true);
279
+ });
280
+
281
+ test("passes when no temp files exist", async () => {
282
+ await mkdir(overstoryDir, { recursive: true });
283
+ await Bun.write(join(overstoryDir, "config.yaml"), "project:\n name: test\n");
284
+
285
+ const checks = await checkStructure(mockConfig, overstoryDir);
286
+
287
+ const tempFilesCheck = checks.find((c) => c.name === "Leftover temp files");
288
+ expect(tempFilesCheck).toBeDefined();
289
+ expect(tempFilesCheck?.status).toBe("pass");
290
+ });
291
+ });