@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,198 @@
1
+ import { access, constants } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { AgentManifest } from "../types.ts";
4
+ import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
5
+
6
+ /**
7
+ * Check if a path exists.
8
+ */
9
+ async function pathExists(path: string): Promise<boolean> {
10
+ try {
11
+ await access(path, constants.F_OK);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Directory structure checks.
20
+ * Validates that .overstory/ and its subdirectories exist with correct permissions.
21
+ */
22
+ export const checkStructure: DoctorCheckFn = async (
23
+ _config,
24
+ overstoryDir,
25
+ ): Promise<DoctorCheck[]> => {
26
+ const checks: DoctorCheck[] = [];
27
+
28
+ // Check 1: .overstory/ directory exists
29
+ const overstoryDirExists = await pathExists(overstoryDir);
30
+ checks.push({
31
+ name: ".overstory/ directory",
32
+ category: "structure",
33
+ status: overstoryDirExists ? "pass" : "fail",
34
+ message: overstoryDirExists ? "Directory exists" : "Directory missing",
35
+ details: overstoryDirExists ? undefined : ["Run 'overstory init' to create it"],
36
+ fixable: !overstoryDirExists,
37
+ });
38
+
39
+ // If .overstory/ doesn't exist, bail early
40
+ if (!overstoryDirExists) {
41
+ return checks;
42
+ }
43
+
44
+ // Check 2: Required files
45
+ const requiredFiles = ["config.yaml", "agent-manifest.json", "hooks.json", ".gitignore"];
46
+ const missingFiles: string[] = [];
47
+
48
+ for (const fileName of requiredFiles) {
49
+ const filePath = join(overstoryDir, fileName);
50
+ const exists = await pathExists(filePath);
51
+ if (!exists) {
52
+ missingFiles.push(fileName);
53
+ }
54
+ }
55
+
56
+ checks.push({
57
+ name: "Required files",
58
+ category: "structure",
59
+ status: missingFiles.length === 0 ? "pass" : "fail",
60
+ message:
61
+ missingFiles.length === 0
62
+ ? "All required files present"
63
+ : `Missing ${missingFiles.length} file(s)`,
64
+ details: missingFiles.length > 0 ? missingFiles : undefined,
65
+ fixable: missingFiles.length > 0,
66
+ });
67
+
68
+ // Check 3: Required subdirectories
69
+ const requiredDirs = ["agent-defs", "agents", "worktrees", "specs", "logs"];
70
+ const missingDirs: string[] = [];
71
+
72
+ for (const dirName of requiredDirs) {
73
+ const dirPath = join(overstoryDir, dirName);
74
+ const exists = await pathExists(dirPath);
75
+ if (!exists) {
76
+ missingDirs.push(`${dirName}/`);
77
+ }
78
+ }
79
+
80
+ checks.push({
81
+ name: "Required subdirectories",
82
+ category: "structure",
83
+ status: missingDirs.length === 0 ? "pass" : "fail",
84
+ message:
85
+ missingDirs.length === 0
86
+ ? "All required subdirectories present"
87
+ : `Missing ${missingDirs.length} subdirectory(ies)`,
88
+ details: missingDirs.length > 0 ? missingDirs : undefined,
89
+ fixable: missingDirs.length > 0,
90
+ });
91
+
92
+ // Check 4: .gitignore contents — validate wildcard+whitelist model
93
+ const gitignorePath = join(overstoryDir, ".gitignore");
94
+ const expectedEntries = [
95
+ "*",
96
+ "!.gitignore",
97
+ "!config.yaml",
98
+ "!agent-manifest.json",
99
+ "!hooks.json",
100
+ "!groups.json",
101
+ "!agent-defs/",
102
+ ];
103
+
104
+ try {
105
+ const gitignoreContent = await Bun.file(gitignorePath).text();
106
+ const missingEntries = expectedEntries.filter((entry) => !gitignoreContent.includes(entry));
107
+
108
+ checks.push({
109
+ name: ".gitignore entries",
110
+ category: "structure",
111
+ status: missingEntries.length === 0 ? "pass" : "warn",
112
+ message:
113
+ missingEntries.length === 0
114
+ ? "All expected entries present"
115
+ : `Missing ${missingEntries.length} entry(ies)`,
116
+ details: missingEntries.length > 0 ? missingEntries : undefined,
117
+ fixable: missingEntries.length > 0,
118
+ });
119
+ } catch {
120
+ // .gitignore doesn't exist, already reported in required files check
121
+ checks.push({
122
+ name: ".gitignore entries",
123
+ category: "structure",
124
+ status: "fail",
125
+ message: "Cannot read .gitignore",
126
+ details: ["File is missing or unreadable"],
127
+ fixable: true,
128
+ });
129
+ }
130
+
131
+ // Check 5: agent-defs/ contains .md files referenced by agent-manifest.json
132
+ try {
133
+ const manifestPath = join(overstoryDir, "agent-manifest.json");
134
+ const manifestContent = await Bun.file(manifestPath).text();
135
+ const manifest = JSON.parse(manifestContent) as AgentManifest;
136
+
137
+ const referencedFiles = new Set<string>();
138
+ for (const agentDef of Object.values(manifest.agents)) {
139
+ referencedFiles.add(agentDef.file);
140
+ }
141
+
142
+ const agentDefsDir = join(overstoryDir, "agent-defs");
143
+ const missingDefFiles: string[] = [];
144
+
145
+ for (const fileName of referencedFiles) {
146
+ const filePath = join(agentDefsDir, fileName);
147
+ const exists = await pathExists(filePath);
148
+ if (!exists) {
149
+ missingDefFiles.push(fileName);
150
+ }
151
+ }
152
+
153
+ checks.push({
154
+ name: "Agent definition files",
155
+ category: "structure",
156
+ status: missingDefFiles.length === 0 ? "pass" : "fail",
157
+ message:
158
+ missingDefFiles.length === 0
159
+ ? "All referenced .md files present"
160
+ : `Missing ${missingDefFiles.length} agent definition(s)`,
161
+ details: missingDefFiles.length > 0 ? missingDefFiles : undefined,
162
+ fixable: missingDefFiles.length > 0,
163
+ });
164
+ } catch (error) {
165
+ // Manifest missing or malformed, already reported or will be in config checks
166
+ checks.push({
167
+ name: "Agent definition files",
168
+ category: "structure",
169
+ status: "fail",
170
+ message: "Cannot validate agent definitions",
171
+ details: [
172
+ error instanceof Error ? error.message : "agent-manifest.json is missing or malformed",
173
+ ],
174
+ fixable: false,
175
+ });
176
+ }
177
+
178
+ // Check 6: No leftover files from failed init attempts
179
+ // Common temp files: .tmp, .bak, config.yaml~, etc.
180
+ try {
181
+ const entries = await Array.fromAsync(new Bun.Glob("*.{tmp,bak}").scan({ cwd: overstoryDir }));
182
+ const tempFiles = entries.filter((name) => name.endsWith(".tmp") || name.endsWith(".bak"));
183
+
184
+ checks.push({
185
+ name: "Leftover temp files",
186
+ category: "structure",
187
+ status: tempFiles.length === 0 ? "pass" : "warn",
188
+ message:
189
+ tempFiles.length === 0 ? "No temp files found" : `Found ${tempFiles.length} temp file(s)`,
190
+ details: tempFiles.length > 0 ? tempFiles : undefined,
191
+ fixable: tempFiles.length > 0,
192
+ });
193
+ } catch {
194
+ // Ignore errors scanning for temp files
195
+ }
196
+
197
+ return checks;
198
+ };
@@ -0,0 +1,37 @@
1
+ import type { OverstoryConfig } from "../types.ts";
2
+
3
+ // === Doctor (Health Checks) ===
4
+
5
+ /** Categories for doctor health checks. */
6
+ export type DoctorCategory =
7
+ | "dependencies"
8
+ | "structure"
9
+ | "config"
10
+ | "databases"
11
+ | "consistency"
12
+ | "agents"
13
+ | "merge"
14
+ | "logs"
15
+ | "version";
16
+
17
+ /** Result of a single doctor health check. */
18
+ export interface DoctorCheck {
19
+ name: string;
20
+ category: DoctorCategory;
21
+ status: "pass" | "warn" | "fail";
22
+ message: string;
23
+ details?: string[];
24
+ /** Whether this check issues can be auto-fixed (future --fix flag). */
25
+ fixable?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Signature for a doctor check function.
30
+ * Each check module exports a function matching this signature.
31
+ * Receives the loaded config and the absolute path to .overstory/.
32
+ * Returns one or more check results.
33
+ */
34
+ export type DoctorCheckFn = (
35
+ config: OverstoryConfig,
36
+ overstoryDir: string,
37
+ ) => DoctorCheck[] | Promise<DoctorCheck[]>;
@@ -0,0 +1,136 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { OverstoryConfig } from "../types.ts";
3
+ import { checkVersion } from "./version.ts";
4
+
5
+ // Minimal config for testing
6
+ const mockConfig: OverstoryConfig = {
7
+ project: {
8
+ name: "test-project",
9
+ root: "/tmp/test",
10
+ canonicalBranch: "main",
11
+ },
12
+ agents: {
13
+ manifestPath: "/tmp/.overstory/agent-manifest.json",
14
+ baseDir: "/tmp/.overstory/agents",
15
+ maxConcurrent: 5,
16
+ staggerDelayMs: 1000,
17
+ maxDepth: 2,
18
+ maxSessionsPerRun: 0,
19
+ },
20
+ worktrees: {
21
+ baseDir: "/tmp/.overstory/worktrees",
22
+ },
23
+ taskTracker: {
24
+ backend: "auto",
25
+ enabled: false,
26
+ },
27
+ mulch: {
28
+ enabled: false,
29
+ domains: [],
30
+ primeFormat: "markdown",
31
+ },
32
+ merge: {
33
+ aiResolveEnabled: false,
34
+ reimagineEnabled: false,
35
+ },
36
+ providers: {
37
+ anthropic: { type: "native" },
38
+ },
39
+ watchdog: {
40
+ tier0Enabled: false,
41
+ tier0IntervalMs: 30000,
42
+ tier1Enabled: false,
43
+ tier2Enabled: false,
44
+ staleThresholdMs: 300000,
45
+ zombieThresholdMs: 600000,
46
+ nudgeIntervalMs: 60000,
47
+ },
48
+ models: {},
49
+ logging: {
50
+ verbose: false,
51
+ redactSecrets: true,
52
+ },
53
+ };
54
+
55
+ describe("checkVersion", () => {
56
+ test("returns checks with category version", async () => {
57
+ const checks = await checkVersion(mockConfig, "/tmp/.overstory");
58
+
59
+ expect(checks).toBeArray();
60
+ expect(checks.length).toBeGreaterThan(0);
61
+
62
+ for (const check of checks) {
63
+ expect(check.category).toBe("version");
64
+ }
65
+ });
66
+
67
+ test("includes version-current check", async () => {
68
+ const checks = await checkVersion(mockConfig, "/tmp/.overstory");
69
+
70
+ const versionCheck = checks.find((c) => c.name === "version-current");
71
+ expect(versionCheck).toBeDefined();
72
+ expect(versionCheck?.status).toBeOneOf(["pass", "warn", "fail"]);
73
+ expect(versionCheck?.message).toContain("overstory");
74
+ });
75
+
76
+ test("includes package-json-sync check", async () => {
77
+ const checks = await checkVersion(mockConfig, "/tmp/.overstory");
78
+
79
+ const syncCheck = checks.find((c) => c.name === "package-json-sync");
80
+ expect(syncCheck).toBeDefined();
81
+ expect(syncCheck?.status).toBeOneOf(["pass", "warn", "fail"]);
82
+ });
83
+
84
+ test("version-current check reports version string", async () => {
85
+ const checks = await checkVersion(mockConfig, "/tmp/.overstory");
86
+
87
+ const versionCheck = checks.find((c) => c.name === "version-current");
88
+ expect(versionCheck).toBeDefined();
89
+
90
+ if (versionCheck?.status === "pass") {
91
+ // Message should contain version in format "overstory vX.Y.Z"
92
+ expect(versionCheck.message).toMatch(/overstory v\d+\.\d+\.\d+/);
93
+ }
94
+ });
95
+
96
+ test("package-json-sync check provides details", async () => {
97
+ const checks = await checkVersion(mockConfig, "/tmp/.overstory");
98
+
99
+ const syncCheck = checks.find((c) => c.name === "package-json-sync");
100
+ expect(syncCheck).toBeDefined();
101
+
102
+ if (syncCheck?.status === "pass") {
103
+ // Should include version details
104
+ expect(syncCheck.details).toBeDefined();
105
+ expect(syncCheck.details?.length).toBeGreaterThan(0);
106
+
107
+ // Details should mention both package.json and src/index.ts
108
+ const detailsText = syncCheck.details?.join(" ");
109
+ expect(detailsText).toContain("package.json");
110
+ expect(detailsText).toContain("src/index.ts");
111
+ }
112
+ });
113
+
114
+ test("all checks have required DoctorCheck fields", async () => {
115
+ const checks = await checkVersion(mockConfig, "/tmp/.overstory");
116
+
117
+ for (const check of checks) {
118
+ expect(check).toHaveProperty("name");
119
+ expect(check).toHaveProperty("category");
120
+ expect(check).toHaveProperty("status");
121
+ expect(check).toHaveProperty("message");
122
+
123
+ expect(typeof check.name).toBe("string");
124
+ expect(typeof check.message).toBe("string");
125
+ expect(["pass", "warn", "fail"]).toContain(check.status);
126
+
127
+ if (check.details !== undefined) {
128
+ expect(check.details).toBeArray();
129
+ }
130
+
131
+ if (check.fixable !== undefined) {
132
+ expect(typeof check.fixable).toBe("boolean");
133
+ }
134
+ }
135
+ });
136
+ });
@@ -0,0 +1,129 @@
1
+ import { join } from "node:path";
2
+ import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
3
+
4
+ /**
5
+ * Version compatibility checks.
6
+ * Validates overstory CLI version, config schema version, database schema versions.
7
+ */
8
+ export const checkVersion: DoctorCheckFn = async (
9
+ _config,
10
+ _overstoryDir,
11
+ ): Promise<DoctorCheck[]> => {
12
+ const checks: DoctorCheck[] = [];
13
+
14
+ // Determine overstory tool root (not the target project)
15
+ // import.meta.dir is src/doctor/, so go up two levels to repo root
16
+ const toolRoot = join(import.meta.dir, "..", "..");
17
+
18
+ // Check 1: version-current (read package.json)
19
+ const versionCheck = await checkCurrentVersion(toolRoot);
20
+ checks.push(versionCheck);
21
+
22
+ // Check 2: package-json-sync (compare package.json and src/index.ts)
23
+ const syncCheck = await checkVersionSync(toolRoot);
24
+ checks.push(syncCheck);
25
+
26
+ return checks;
27
+ };
28
+
29
+ /**
30
+ * Check that the current version can be determined from package.json.
31
+ */
32
+ async function checkCurrentVersion(toolRoot: string): Promise<DoctorCheck> {
33
+ try {
34
+ const packageJsonPath = join(toolRoot, "package.json");
35
+ const packageJson = (await Bun.file(packageJsonPath).json()) as { version?: string };
36
+
37
+ if (!packageJson.version) {
38
+ return {
39
+ name: "version-current",
40
+ category: "version",
41
+ status: "fail",
42
+ message: "Cannot determine version (package.json has no version field)",
43
+ };
44
+ }
45
+
46
+ return {
47
+ name: "version-current",
48
+ category: "version",
49
+ status: "pass",
50
+ message: `overstory v${packageJson.version}`,
51
+ };
52
+ } catch (error) {
53
+ return {
54
+ name: "version-current",
55
+ category: "version",
56
+ status: "fail",
57
+ message: "Cannot determine version",
58
+ details: [error instanceof Error ? error.message : String(error)],
59
+ };
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Check that package.json version matches src/index.ts VERSION constant.
65
+ */
66
+ async function checkVersionSync(toolRoot: string): Promise<DoctorCheck> {
67
+ try {
68
+ // Read package.json version
69
+ const packageJsonPath = join(toolRoot, "package.json");
70
+ const packageJson = (await Bun.file(packageJsonPath).json()) as { version?: string };
71
+ const pkgVersion = packageJson.version;
72
+
73
+ if (!pkgVersion) {
74
+ return {
75
+ name: "package-json-sync",
76
+ category: "version",
77
+ status: "warn",
78
+ message: "Cannot verify version sync (package.json has no version)",
79
+ };
80
+ }
81
+
82
+ // Read src/index.ts and extract VERSION constant
83
+ const indexPath = join(toolRoot, "src", "index.ts");
84
+ const indexContent = await Bun.file(indexPath).text();
85
+
86
+ // Regex to find: const VERSION = "x.y.z"
87
+ const versionRegex = /const\s+VERSION\s*=\s*["']([^"']+)["']/;
88
+ const match = versionRegex.exec(indexContent);
89
+
90
+ if (!match?.[1]) {
91
+ return {
92
+ name: "package-json-sync",
93
+ category: "version",
94
+ status: "warn",
95
+ message: "Cannot find VERSION constant in src/index.ts",
96
+ details: [`package.json version: ${pkgVersion}`],
97
+ };
98
+ }
99
+
100
+ const indexVersion = match[1];
101
+
102
+ if (pkgVersion === indexVersion) {
103
+ return {
104
+ name: "package-json-sync",
105
+ category: "version",
106
+ status: "pass",
107
+ message: "Versions are synchronized",
108
+ details: [`package.json: ${pkgVersion}`, `src/index.ts: ${indexVersion}`],
109
+ };
110
+ }
111
+
112
+ return {
113
+ name: "package-json-sync",
114
+ category: "version",
115
+ status: "warn",
116
+ message: "Version mismatch between package.json and src/index.ts",
117
+ details: [`package.json: ${pkgVersion}`, `src/index.ts: ${indexVersion}`],
118
+ fixable: true,
119
+ };
120
+ } catch (error) {
121
+ return {
122
+ name: "package-json-sync",
123
+ category: "version",
124
+ status: "warn",
125
+ message: "Cannot verify version sync",
126
+ details: [error instanceof Error ? error.message : String(error)],
127
+ };
128
+ }
129
+ }