@os-eco/overstory-cli 0.6.1 → 0.6.4

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 (80) hide show
  1. package/README.md +7 -6
  2. package/package.json +12 -4
  3. package/src/agents/hooks-deployer.test.ts +94 -16
  4. package/src/agents/hooks-deployer.ts +18 -0
  5. package/src/agents/manifest.test.ts +86 -0
  6. package/src/commands/agents.test.ts +3 -3
  7. package/src/commands/agents.ts +59 -88
  8. package/src/commands/clean.test.ts +31 -46
  9. package/src/commands/clean.ts +28 -49
  10. package/src/commands/completions.ts +14 -0
  11. package/src/commands/coordinator.test.ts +131 -24
  12. package/src/commands/coordinator.ts +100 -63
  13. package/src/commands/costs.test.ts +2 -2
  14. package/src/commands/costs.ts +96 -75
  15. package/src/commands/dashboard.test.ts +2 -2
  16. package/src/commands/dashboard.ts +73 -93
  17. package/src/commands/doctor.test.ts +2 -2
  18. package/src/commands/doctor.ts +92 -79
  19. package/src/commands/errors.test.ts +2 -2
  20. package/src/commands/errors.ts +56 -50
  21. package/src/commands/feed.test.ts +2 -2
  22. package/src/commands/feed.ts +86 -83
  23. package/src/commands/group.ts +167 -177
  24. package/src/commands/hooks.test.ts +2 -2
  25. package/src/commands/hooks.ts +52 -42
  26. package/src/commands/init.test.ts +19 -19
  27. package/src/commands/init.ts +7 -16
  28. package/src/commands/inspect.test.ts +2 -2
  29. package/src/commands/inspect.ts +54 -57
  30. package/src/commands/log.test.ts +5 -10
  31. package/src/commands/log.ts +90 -84
  32. package/src/commands/logs.test.ts +1 -1
  33. package/src/commands/logs.ts +101 -104
  34. package/src/commands/mail.ts +157 -169
  35. package/src/commands/merge.test.ts +20 -58
  36. package/src/commands/merge.ts +13 -43
  37. package/src/commands/metrics.test.ts +2 -2
  38. package/src/commands/metrics.ts +33 -34
  39. package/src/commands/monitor.test.ts +3 -3
  40. package/src/commands/monitor.ts +56 -61
  41. package/src/commands/nudge.ts +41 -89
  42. package/src/commands/prime.test.ts +15 -47
  43. package/src/commands/prime.ts +7 -44
  44. package/src/commands/replay.test.ts +2 -2
  45. package/src/commands/replay.ts +79 -86
  46. package/src/commands/run.ts +97 -77
  47. package/src/commands/sling.test.ts +196 -0
  48. package/src/commands/sling.ts +24 -54
  49. package/src/commands/spec.test.ts +13 -39
  50. package/src/commands/spec.ts +30 -99
  51. package/src/commands/status.ts +46 -42
  52. package/src/commands/stop.test.ts +21 -39
  53. package/src/commands/stop.ts +18 -33
  54. package/src/commands/supervisor.test.ts +3 -5
  55. package/src/commands/supervisor.ts +136 -157
  56. package/src/commands/trace.test.ts +9 -9
  57. package/src/commands/trace.ts +54 -77
  58. package/src/commands/watch.test.ts +2 -2
  59. package/src/commands/watch.ts +38 -45
  60. package/src/commands/worktree.test.ts +8 -8
  61. package/src/commands/worktree.ts +63 -46
  62. package/src/config.test.ts +96 -0
  63. package/src/doctor/databases.test.ts +22 -2
  64. package/src/doctor/databases.ts +16 -0
  65. package/src/doctor/dependencies.test.ts +55 -1
  66. package/src/doctor/dependencies.ts +113 -18
  67. package/src/e2e/init-sling-lifecycle.test.ts +6 -6
  68. package/src/index.ts +223 -213
  69. package/src/logging/color.test.ts +74 -91
  70. package/src/logging/color.ts +52 -46
  71. package/src/logging/reporter.test.ts +10 -10
  72. package/src/logging/reporter.ts +6 -5
  73. package/src/merge/queue.test.ts +66 -0
  74. package/src/merge/queue.ts +15 -0
  75. package/src/schema-consistency.test.ts +239 -0
  76. package/src/sessions/compat.ts +1 -1
  77. package/src/sessions/store.test.ts +37 -0
  78. package/src/sessions/store.ts +11 -0
  79. package/src/worktree/tmux.test.ts +98 -9
  80. package/src/worktree/tmux.ts +18 -0
@@ -51,13 +51,15 @@ describe("checkDatabases", () => {
51
51
  test("fails when database files do not exist", () => {
52
52
  const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
53
53
 
54
- expect(checks).toHaveLength(3);
54
+ expect(checks).toHaveLength(4);
55
55
  expect(checks[0]?.status).toBe("fail");
56
56
  expect(checks[0]?.name).toBe("mail.db exists");
57
57
  expect(checks[1]?.status).toBe("fail");
58
58
  expect(checks[1]?.name).toBe("metrics.db exists");
59
59
  expect(checks[2]?.status).toBe("fail");
60
60
  expect(checks[2]?.name).toBe("sessions.db exists");
61
+ expect(checks[3]?.status).toBe("fail");
62
+ expect(checks[3]?.name).toBe("merge-queue.db exists");
61
63
  });
62
64
 
63
65
  test("passes when databases exist with correct schema", () => {
@@ -141,13 +143,31 @@ describe("checkDatabases", () => {
141
143
  `);
142
144
  sessionsDb.close();
143
145
 
146
+ // Create merge-queue.db
147
+ const mergeDb = new Database(join(tempDir, "merge-queue.db"));
148
+ mergeDb.exec("PRAGMA journal_mode=WAL");
149
+ mergeDb.exec(`
150
+ CREATE TABLE merge_queue (
151
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
152
+ branch_name TEXT NOT NULL,
153
+ task_id TEXT NOT NULL,
154
+ agent_name TEXT NOT NULL,
155
+ files_modified TEXT NOT NULL DEFAULT '[]',
156
+ enqueued_at TEXT NOT NULL,
157
+ status TEXT NOT NULL DEFAULT 'pending',
158
+ resolved_tier TEXT
159
+ )
160
+ `);
161
+ mergeDb.close();
162
+
144
163
  const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
145
164
 
146
- expect(checks).toHaveLength(3);
165
+ expect(checks).toHaveLength(4);
147
166
  expect(checks.every((c) => c?.status === "pass")).toBe(true);
148
167
  expect(checks[0]?.name).toBe("mail.db health");
149
168
  expect(checks[1]?.name).toBe("metrics.db health");
150
169
  expect(checks[2]?.name).toBe("sessions.db health");
170
+ expect(checks[3]?.name).toBe("merge-queue.db health");
151
171
  });
152
172
 
153
173
  test("fails when table is missing", () => {
@@ -85,6 +85,22 @@ export const checkDatabases: DoctorCheckFn = (_config, overstoryDir): DoctorChec
85
85
  ],
86
86
  },
87
87
  },
88
+ {
89
+ name: "merge-queue.db",
90
+ tables: ["merge_queue"],
91
+ requiredColumns: {
92
+ merge_queue: [
93
+ "id",
94
+ "branch_name",
95
+ "task_id",
96
+ "agent_name",
97
+ "files_modified",
98
+ "enqueued_at",
99
+ "status",
100
+ "resolved_tier",
101
+ ],
102
+ },
103
+ },
88
104
  ];
89
105
 
90
106
  for (const dbSpec of databases) {
@@ -57,7 +57,7 @@ describe("checkDependencies", () => {
57
57
  const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
58
58
 
59
59
  expect(checks).toBeArray();
60
- expect(checks.length).toBeGreaterThanOrEqual(5);
60
+ expect(checks.length).toBeGreaterThanOrEqual(7);
61
61
 
62
62
  // Verify we have checks for each required tool
63
63
  const toolNames = checks.map((c) => c.name);
@@ -66,6 +66,8 @@ describe("checkDependencies", () => {
66
66
  expect(toolNames).toContain("tmux availability");
67
67
  expect(toolNames).toContain("sd availability");
68
68
  expect(toolNames).toContain("mulch availability");
69
+ expect(toolNames).toContain("overstory availability");
70
+ expect(toolNames).toContain("cn availability");
69
71
  });
70
72
 
71
73
  test("includes bd CGO support check when bd is available", async () => {
@@ -181,4 +183,56 @@ describe("checkDependencies", () => {
181
183
  const cgoCheck = checks.find((c) => c.name === "bd CGO support");
182
184
  expect(cgoCheck).toBeUndefined();
183
185
  });
186
+
187
+ test("cn check is warn (not fail) when missing", async () => {
188
+ const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
189
+ const cnCheck = checks.find((c) => c.name === "cn availability");
190
+ expect(cnCheck).toBeDefined();
191
+ // cn is optional — should never be "fail", only "pass" or "warn"
192
+ expect(cnCheck?.status).not.toBe("fail");
193
+ });
194
+
195
+ test("checks short aliases for available tools", async () => {
196
+ const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
197
+ const mulchCheck = checks.find((c) => c.name === "mulch availability");
198
+ if (mulchCheck?.status === "pass") {
199
+ const mlAlias = checks.find((c) => c.name === "ml alias");
200
+ expect(mlAlias).toBeDefined();
201
+ expect(mlAlias?.category).toBe("dependencies");
202
+ expect(["pass", "warn"]).toContain(mlAlias?.status ?? "");
203
+ }
204
+ const ovCheck = checks.find((c) => c.name === "overstory availability");
205
+ if (ovCheck?.status === "pass") {
206
+ const ovAlias = checks.find((c) => c.name === "ov alias");
207
+ expect(ovAlias).toBeDefined();
208
+ expect(["pass", "warn"]).toContain(ovAlias?.status ?? "");
209
+ }
210
+ });
211
+
212
+ test("alias checks are only run when primary tool passes", async () => {
213
+ const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
214
+ // If mulch failed, ml alias should NOT be present
215
+ const mulchCheck = checks.find((c) => c.name === "mulch availability");
216
+ const mlAlias = checks.find((c) => c.name === "ml alias");
217
+ if (mulchCheck?.status !== "pass") {
218
+ expect(mlAlias).toBeUndefined();
219
+ }
220
+ });
221
+
222
+ test("install hints appear in details for missing tools", async () => {
223
+ const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
224
+ // Check any failing/warning check with an installHint has npm install guidance
225
+ const cnCheck = checks.find((c) => c.name === "cn availability");
226
+ if (cnCheck?.status === "warn" || cnCheck?.status === "fail") {
227
+ const hasInstallHint = cnCheck.details?.some((d) => d.includes("npm install -g"));
228
+ expect(hasInstallHint).toBe(true);
229
+ }
230
+ });
231
+
232
+ test("includes overstory availability check", async () => {
233
+ const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
234
+ const ovCheck = checks.find((c) => c.name === "overstory availability");
235
+ expect(ovCheck).toBeDefined();
236
+ expect(ovCheck?.category).toBe("dependencies");
237
+ });
184
238
  });
@@ -1,10 +1,20 @@
1
1
  import { resolveBackend, trackerCliName } from "../tracker/factory.ts";
2
2
  import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
3
3
 
4
+ interface ToolSpec {
5
+ name: string;
6
+ versionFlag: string;
7
+ required: boolean;
8
+ /** Short alias to check if the primary tool is available. */
9
+ alias?: string;
10
+ /** npm package name for install hint (e.g. "@os-eco/mulch-cli"). */
11
+ installHint?: string;
12
+ }
13
+
4
14
  /**
5
15
  * External dependency checks.
6
- * Validates that required CLI tools (git, bun, tmux, bd, mulch) are available
7
- * and that bd has functional CGO support for its Dolt database backend.
16
+ * Validates that required CLI tools (git, bun, tmux, tracker, mulch, overstory)
17
+ * and optional tools (cn) are available, including short alias availability.
8
18
  */
9
19
  export const checkDependencies: DoctorCheckFn = async (
10
20
  config,
@@ -12,30 +22,56 @@ export const checkDependencies: DoctorCheckFn = async (
12
22
  ): Promise<DoctorCheck[]> => {
13
23
  // Determine which tracker CLI to check based on config backend (resolve "auto")
14
24
  const resolvedBackend = await resolveBackend(config.taskTracker.backend, config.project.root);
15
- const trackerTool = {
16
- name: trackerCliName(resolvedBackend),
17
- versionFlag: "--version",
18
- required: true,
19
- };
25
+ const trackerName = trackerCliName(resolvedBackend);
20
26
 
21
- const requiredTools = [
27
+ const tools: ToolSpec[] = [
22
28
  { name: "git", versionFlag: "--version", required: true },
23
29
  { name: "bun", versionFlag: "--version", required: true },
24
30
  { name: "tmux", versionFlag: "-V", required: true },
25
- trackerTool,
26
- { name: "mulch", versionFlag: "--version", required: true },
31
+ {
32
+ name: trackerName,
33
+ versionFlag: "--version",
34
+ required: true,
35
+ installHint: trackerName === "sd" ? "@os-eco/seeds-cli" : undefined,
36
+ },
37
+ {
38
+ name: "mulch",
39
+ versionFlag: "--version",
40
+ required: true,
41
+ alias: "ml",
42
+ installHint: "@os-eco/mulch-cli",
43
+ },
44
+ {
45
+ name: "overstory",
46
+ versionFlag: "--version",
47
+ required: true,
48
+ alias: "ov",
49
+ installHint: "@os-eco/overstory-cli",
50
+ },
51
+ {
52
+ name: "cn",
53
+ versionFlag: "--version",
54
+ required: false,
55
+ installHint: "@os-eco/canopy-cli",
56
+ },
27
57
  ];
28
58
 
29
59
  const checks: DoctorCheck[] = [];
30
60
 
31
- for (const tool of requiredTools) {
32
- const check = await checkTool(tool.name, tool.versionFlag, tool.required);
61
+ for (const tool of tools) {
62
+ const check = await checkTool(tool.name, tool.versionFlag, tool.required, tool.installHint);
33
63
  checks.push(check);
64
+
65
+ // Check short alias availability if the main tool is available
66
+ if (tool.alias && check.status === "pass") {
67
+ const aliasCheck = await checkAlias(tool.name, tool.alias, tool.installHint);
68
+ checks.push(aliasCheck);
69
+ }
34
70
  }
35
71
 
36
72
  // If bd is available, probe for CGO/Dolt backend functionality.
37
73
  // Only run for beads backend (CGO check is beads-specific).
38
- if (trackerTool.name === "bd") {
74
+ if (trackerName === "bd") {
39
75
  const bdCheck = checks.find((c) => c.name === "bd availability");
40
76
  if (bdCheck?.status === "pass") {
41
77
  const cgoCheck = await checkBdCgoSupport();
@@ -119,6 +155,57 @@ async function checkBdCgoSupport(): Promise<DoctorCheck> {
119
155
  }
120
156
  }
121
157
 
158
+ /**
159
+ * Check if a short alias for a CLI tool is available.
160
+ */
161
+ async function checkAlias(
162
+ toolName: string,
163
+ alias: string,
164
+ installHint?: string,
165
+ ): Promise<DoctorCheck> {
166
+ try {
167
+ const proc = Bun.spawn([alias, "--version"], {
168
+ stdout: "pipe",
169
+ stderr: "pipe",
170
+ });
171
+ const exitCode = await proc.exited;
172
+
173
+ if (exitCode === 0) {
174
+ return {
175
+ name: `${alias} alias`,
176
+ category: "dependencies",
177
+ status: "pass",
178
+ message: `${alias} alias for ${toolName} is available`,
179
+ details: [`Short alias '${alias}' is configured`],
180
+ };
181
+ }
182
+
183
+ const hint = installHint
184
+ ? `Reinstall ${installHint} to get the '${alias}' alias.`
185
+ : `Ensure '${alias}' alias is in your PATH.`;
186
+ return {
187
+ name: `${alias} alias`,
188
+ category: "dependencies",
189
+ status: "warn",
190
+ message: `${alias} alias for ${toolName} not working`,
191
+ details: [hint],
192
+ fixable: true,
193
+ };
194
+ } catch {
195
+ const hint = installHint
196
+ ? `Reinstall ${installHint} to get the '${alias}' alias.`
197
+ : `Ensure '${alias}' alias is in your PATH.`;
198
+ return {
199
+ name: `${alias} alias`,
200
+ category: "dependencies",
201
+ status: "warn",
202
+ message: `${alias} alias for ${toolName} is not available`,
203
+ details: [`'${toolName}' works but short alias '${alias}' was not found.`, hint],
204
+ fixable: true,
205
+ };
206
+ }
207
+ }
208
+
122
209
  /**
123
210
  * Check if a CLI tool is available by attempting to run it with a version flag.
124
211
  */
@@ -126,6 +213,7 @@ async function checkTool(
126
213
  name: string,
127
214
  versionFlag: string,
128
215
  required: boolean,
216
+ installHint?: string,
129
217
  ): Promise<DoctorCheck> {
130
218
  try {
131
219
  const proc = Bun.spawn([name, versionFlag], {
@@ -150,25 +238,32 @@ async function checkTool(
150
238
 
151
239
  // Non-zero exit code
152
240
  const stderr = await new Response(proc.stderr).text();
241
+ const details: string[] = [];
242
+ if (stderr) details.push(stderr.trim());
243
+ if (installHint) details.push(`Install: npm install -g ${installHint}`);
153
244
  return {
154
245
  name: `${name} availability`,
155
246
  category: "dependencies",
156
247
  status: required ? "fail" : "warn",
157
248
  message: `${name} command failed (exit code ${exitCode})`,
158
- details: stderr ? [stderr.trim()] : undefined,
249
+ details: details.length > 0 ? details : undefined,
159
250
  fixable: true,
160
251
  };
161
252
  } catch (error) {
162
253
  // Command not found or spawn failed
254
+ const details: string[] = [];
255
+ if (installHint) {
256
+ details.push(`Install: npm install -g ${installHint}`);
257
+ } else {
258
+ details.push(`Install ${name} or ensure it is in your PATH`);
259
+ }
260
+ details.push(error instanceof Error ? error.message : String(error));
163
261
  return {
164
262
  name: `${name} availability`,
165
263
  category: "dependencies",
166
264
  status: required ? "fail" : "warn",
167
265
  message: `${name} is not installed or not in PATH`,
168
- details: [
169
- `Install ${name} or ensure it is in your PATH`,
170
- error instanceof Error ? error.message : String(error),
171
- ],
266
+ details,
172
267
  fixable: true,
173
268
  };
174
269
  }
@@ -52,7 +52,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
52
52
  });
53
53
 
54
54
  test("init creates all expected artifacts", async () => {
55
- await initCommand([]);
55
+ await initCommand({});
56
56
 
57
57
  const overstoryDir = join(tempDir, ".overstory");
58
58
 
@@ -92,7 +92,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
92
92
  });
93
93
 
94
94
  test("loadConfig returns valid config pointing to temp dir", async () => {
95
- await initCommand([]);
95
+ await initCommand({});
96
96
 
97
97
  const config = await loadConfig(tempDir);
98
98
 
@@ -110,7 +110,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
110
110
  });
111
111
 
112
112
  test("manifest loads successfully with all 8 agents", async () => {
113
- await initCommand([]);
113
+ await initCommand({});
114
114
 
115
115
  const manifestPath = join(tempDir, ".overstory", "agent-manifest.json");
116
116
  const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
@@ -145,7 +145,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
145
145
  });
146
146
 
147
147
  test("manifest capability index is consistent", async () => {
148
- await initCommand([]);
148
+ await initCommand({});
149
149
 
150
150
  const manifestPath = join(tempDir, ".overstory", "agent-manifest.json");
151
151
  const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
@@ -167,7 +167,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
167
167
  });
168
168
 
169
169
  test("overlay generation works for external project", async () => {
170
- await initCommand([]);
170
+ await initCommand({});
171
171
 
172
172
  const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
173
173
  const baseDefinition = await Bun.file(join(agentDefsDir, "builder.md")).text();
@@ -215,7 +215,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
215
215
  // init → load config → load manifest → generate overlay
216
216
 
217
217
  // Step 1: Init
218
- await initCommand([]);
218
+ await initCommand({});
219
219
 
220
220
  // Step 2: Load config
221
221
  const config = await loadConfig(tempDir);