@os-eco/overstory-cli 0.6.1 → 0.6.5
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.
- package/README.md +8 -7
- package/package.json +12 -4
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +131 -16
- package/src/agents/hooks-deployer.ts +33 -1
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/manifest.test.ts +86 -0
- package/src/agents/overlay.test.ts +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +8 -8
- package/src/commands/agents.ts +62 -91
- package/src/commands/clean.test.ts +36 -51
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +133 -26
- package/src/commands/coordinator.ts +101 -64
- package/src/commands/costs.test.ts +47 -47
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +75 -95
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +18 -18
- package/src/commands/inspect.ts +55 -58
- package/src/commands/log.test.ts +26 -31
- package/src/commands/log.ts +97 -91
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +28 -66
- package/src/commands/merge.ts +21 -51
- package/src/commands/metrics.test.ts +8 -8
- package/src/commands/metrics.ts +34 -35
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +57 -62
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +19 -51
- package/src/commands/prime.ts +13 -50
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +201 -5
- package/src/commands/sling.ts +37 -64
- package/src/commands/spec.test.ts +14 -40
- package/src/commands/spec.ts +32 -101
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +63 -58
- package/src/commands/stop.test.ts +22 -40
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +12 -14
- package/src/commands/supervisor.ts +144 -165
- package/src/commands/trace.test.ts +15 -15
- package/src/commands/trace.ts +59 -82
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +213 -37
- package/src/commands/worktree.ts +110 -55
- package/src/config.test.ts +96 -0
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +8 -8
- package/src/errors.ts +1 -1
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +73 -7
- package/src/merge/queue.ts +17 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +2 -2
- package/src/sessions/store.test.ts +41 -4
- package/src/sessions/store.ts +13 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
package/src/commands/worktree.ts
CHANGED
|
@@ -7,18 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import { Command } from "commander";
|
|
10
11
|
import { loadConfig } from "../config.ts";
|
|
11
12
|
import { ValidationError } from "../errors.ts";
|
|
12
13
|
import { createMailStore } from "../mail/store.ts";
|
|
13
14
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
14
15
|
import type { AgentSession } from "../types.ts";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
isBranchMerged,
|
|
18
|
+
listWorktrees,
|
|
19
|
+
preserveSeedsChanges,
|
|
20
|
+
removeWorktree,
|
|
21
|
+
} from "../worktree/manager.ts";
|
|
16
22
|
import { isSessionAlive, killSession } from "../worktree/tmux.ts";
|
|
17
23
|
|
|
18
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
19
|
-
return args.includes(flag);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
24
|
/**
|
|
23
25
|
* Handle `overstory worktree list`.
|
|
24
26
|
*/
|
|
@@ -44,7 +46,7 @@ async function handleList(root: string, json: boolean): Promise<void> {
|
|
|
44
46
|
head: wt.head,
|
|
45
47
|
agentName: session?.agentName ?? null,
|
|
46
48
|
state: session?.state ?? null,
|
|
47
|
-
|
|
49
|
+
taskId: session?.taskId ?? null,
|
|
48
50
|
};
|
|
49
51
|
});
|
|
50
52
|
process.stdout.write(`${JSON.stringify(entries, null, "\t")}\n`);
|
|
@@ -61,7 +63,7 @@ async function handleList(root: string, json: boolean): Promise<void> {
|
|
|
61
63
|
const session = sessions.find((s) => s.worktreePath === wt.path);
|
|
62
64
|
const state = session?.state ?? "unknown";
|
|
63
65
|
const agent = session?.agentName ?? "?";
|
|
64
|
-
const bead = session?.
|
|
66
|
+
const bead = session?.taskId ?? "?";
|
|
65
67
|
process.stdout.write(` ${wt.branch}\n`);
|
|
66
68
|
process.stdout.write(` Agent: ${agent} | State: ${state} | Task: ${bead}\n`);
|
|
67
69
|
process.stdout.write(` Path: ${wt.path}\n\n`);
|
|
@@ -72,14 +74,12 @@ async function handleList(root: string, json: boolean): Promise<void> {
|
|
|
72
74
|
* Handle `overstory worktree clean [--completed] [--all] [--force]`.
|
|
73
75
|
*/
|
|
74
76
|
async function handleClean(
|
|
75
|
-
|
|
77
|
+
opts: { all: boolean; force: boolean; completedOnly: boolean },
|
|
76
78
|
root: string,
|
|
77
79
|
json: boolean,
|
|
78
80
|
canonicalBranch: string,
|
|
79
81
|
): Promise<void> {
|
|
80
|
-
const
|
|
81
|
-
const force = hasFlag(args, "--force");
|
|
82
|
-
const completedOnly = hasFlag(args, "--completed") || !all;
|
|
82
|
+
const { force, completedOnly } = opts;
|
|
83
83
|
|
|
84
84
|
const worktrees = await listWorktrees(root);
|
|
85
85
|
const overstoryDir = join(root, ".overstory");
|
|
@@ -97,6 +97,7 @@ async function handleClean(
|
|
|
97
97
|
const cleaned: string[] = [];
|
|
98
98
|
const failed: string[] = [];
|
|
99
99
|
const skipped: string[] = [];
|
|
100
|
+
const seedsPreserved: string[] = [];
|
|
100
101
|
|
|
101
102
|
try {
|
|
102
103
|
for (const wt of overstoryWts) {
|
|
@@ -107,8 +108,11 @@ async function handleClean(
|
|
|
107
108
|
continue;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
//
|
|
111
|
-
|
|
111
|
+
// Lead branches are never merged via the normal pipeline — skip merge check for leads.
|
|
112
|
+
const isLead = session?.capability === "lead";
|
|
113
|
+
|
|
114
|
+
// Check if the branch has been merged into the canonical branch (unless --force or lead)
|
|
115
|
+
if (!force && !isLead && wt.branch.length > 0) {
|
|
112
116
|
let merged = false;
|
|
113
117
|
try {
|
|
114
118
|
merged = await isBranchMerged(root, wt.branch, canonicalBranch);
|
|
@@ -136,8 +140,8 @@ async function handleClean(
|
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
// Warn about force-deleting unmerged branch
|
|
140
|
-
if (force && wt.branch.length > 0) {
|
|
143
|
+
// Warn about force-deleting unmerged branch (non-lead only)
|
|
144
|
+
if (force && !isLead && wt.branch.length > 0) {
|
|
141
145
|
let merged = false;
|
|
142
146
|
try {
|
|
143
147
|
merged = await isBranchMerged(root, wt.branch, canonicalBranch);
|
|
@@ -149,6 +153,29 @@ async function handleClean(
|
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
// Preserve .seeds/ changes from lead worktrees before removal.
|
|
157
|
+
// Lead branches are never merged, so .seeds/ files would otherwise be lost.
|
|
158
|
+
if (isLead && wt.branch.length > 0) {
|
|
159
|
+
const result = await preserveSeedsChanges(
|
|
160
|
+
root,
|
|
161
|
+
wt.branch,
|
|
162
|
+
canonicalBranch,
|
|
163
|
+
session?.agentName ?? "unknown-lead",
|
|
164
|
+
);
|
|
165
|
+
if (result.preserved) {
|
|
166
|
+
seedsPreserved.push(wt.branch);
|
|
167
|
+
if (!json) {
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
`🌱 Preserved .seeds/ changes from lead ${session?.agentName ?? "unknown-lead"}\n`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
} else if (result.error) {
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
`⚠️ Failed to preserve .seeds/ from ${wt.branch}: ${result.error}\n`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
152
179
|
// Remove worktree and its branch.
|
|
153
180
|
// Always force worktree removal since deployed .claude/ files create untracked
|
|
154
181
|
// files that cause non-forced removal to fail.
|
|
@@ -215,13 +242,14 @@ async function handleClean(
|
|
|
215
242
|
|
|
216
243
|
if (json) {
|
|
217
244
|
process.stdout.write(
|
|
218
|
-
`${JSON.stringify({ cleaned, failed, skipped, pruned: pruneCount, mailPurged })}\n`,
|
|
245
|
+
`${JSON.stringify({ cleaned, failed, skipped, pruned: pruneCount, mailPurged, seedsPreserved })}\n`,
|
|
219
246
|
);
|
|
220
247
|
} else if (
|
|
221
248
|
cleaned.length === 0 &&
|
|
222
249
|
pruneCount === 0 &&
|
|
223
250
|
failed.length === 0 &&
|
|
224
|
-
skipped.length === 0
|
|
251
|
+
skipped.length === 0 &&
|
|
252
|
+
seedsPreserved.length === 0
|
|
225
253
|
) {
|
|
226
254
|
process.stdout.write("No worktrees to clean.\n");
|
|
227
255
|
} else {
|
|
@@ -245,6 +273,11 @@ async function handleClean(
|
|
|
245
273
|
`Pruned ${pruneCount} zombie session${pruneCount === 1 ? "" : "s"} from store.\n`,
|
|
246
274
|
);
|
|
247
275
|
}
|
|
276
|
+
if (seedsPreserved.length > 0) {
|
|
277
|
+
process.stdout.write(
|
|
278
|
+
`Preserved .seeds/ changes from ${seedsPreserved.length} lead${seedsPreserved.length === 1 ? "" : "s"}.\n`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
248
281
|
if (skipped.length > 0) {
|
|
249
282
|
process.stdout.write(
|
|
250
283
|
`\n⚠️ Skipped ${skipped.length} worktree${skipped.length === 1 ? "" : "s"} with unmerged branches:\n`,
|
|
@@ -260,52 +293,74 @@ async function handleClean(
|
|
|
260
293
|
}
|
|
261
294
|
}
|
|
262
295
|
|
|
296
|
+
export function createWorktreeCommand(): Command {
|
|
297
|
+
const cmd = new Command("worktree").description("Manage agent worktrees");
|
|
298
|
+
|
|
299
|
+
cmd
|
|
300
|
+
.command("list")
|
|
301
|
+
.description("List worktrees with agent status")
|
|
302
|
+
.option("--json", "Output as JSON")
|
|
303
|
+
.action(async (opts: { json?: boolean }) => {
|
|
304
|
+
const cwd = process.cwd();
|
|
305
|
+
const config = await loadConfig(cwd);
|
|
306
|
+
await handleList(config.project.root, opts.json ?? false);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
cmd
|
|
310
|
+
.command("clean")
|
|
311
|
+
.description("Remove completed worktrees")
|
|
312
|
+
.option("--completed", "Only finished agents (default)")
|
|
313
|
+
.option("--all", "Force remove all worktrees")
|
|
314
|
+
.option("--force", "Delete even if branches are unmerged")
|
|
315
|
+
.option("--json", "Output as JSON")
|
|
316
|
+
.action(
|
|
317
|
+
async (opts: { completed?: boolean; all?: boolean; force?: boolean; json?: boolean }) => {
|
|
318
|
+
const cwd = process.cwd();
|
|
319
|
+
const config = await loadConfig(cwd);
|
|
320
|
+
const all = opts.all ?? false;
|
|
321
|
+
await handleClean(
|
|
322
|
+
{
|
|
323
|
+
all,
|
|
324
|
+
force: opts.force ?? false,
|
|
325
|
+
completedOnly: opts.completed ?? !all,
|
|
326
|
+
},
|
|
327
|
+
config.project.root,
|
|
328
|
+
opts.json ?? false,
|
|
329
|
+
config.project.canonicalBranch,
|
|
330
|
+
);
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return cmd;
|
|
335
|
+
}
|
|
336
|
+
|
|
263
337
|
/**
|
|
264
338
|
* Entry point for `overstory worktree <subcommand> [flags]`.
|
|
265
339
|
*
|
|
266
340
|
* Subcommands: list, clean.
|
|
267
341
|
*/
|
|
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
342
|
export async function worktreeCommand(args: string[]): Promise<void> {
|
|
284
|
-
|
|
285
|
-
|
|
343
|
+
const cmd = createWorktreeCommand();
|
|
344
|
+
cmd.exitOverride();
|
|
345
|
+
|
|
346
|
+
if (args.length === 0) {
|
|
347
|
+
process.stdout.write(cmd.helpInformation());
|
|
286
348
|
return;
|
|
287
349
|
}
|
|
288
350
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
);
|
|
351
|
+
try {
|
|
352
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
353
|
+
} catch (err: unknown) {
|
|
354
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
355
|
+
const code = (err as { code: string }).code;
|
|
356
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (code === "commander.unknownCommand") {
|
|
360
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
361
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
throw err;
|
|
310
365
|
}
|
|
311
366
|
}
|
package/src/config.test.ts
CHANGED
|
@@ -265,6 +265,80 @@ providers:
|
|
|
265
265
|
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
+
test("multiple providers parsed correctly", async () => {
|
|
269
|
+
await ensureOverstoryDir();
|
|
270
|
+
await writeConfig(`
|
|
271
|
+
providers:
|
|
272
|
+
anthropic:
|
|
273
|
+
type: native
|
|
274
|
+
openrouter:
|
|
275
|
+
type: gateway
|
|
276
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
277
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
278
|
+
litellm:
|
|
279
|
+
type: gateway
|
|
280
|
+
baseUrl: http://localhost:4000
|
|
281
|
+
authTokenEnv: LITELLM_API_KEY
|
|
282
|
+
`);
|
|
283
|
+
const config = await loadConfig(tempDir);
|
|
284
|
+
expect(Object.keys(config.providers).length).toBe(3);
|
|
285
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
286
|
+
expect(config.providers.openrouter).toEqual({
|
|
287
|
+
type: "gateway",
|
|
288
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
289
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
290
|
+
});
|
|
291
|
+
expect(config.providers.litellm).toEqual({
|
|
292
|
+
type: "gateway",
|
|
293
|
+
baseUrl: "http://localhost:4000",
|
|
294
|
+
authTokenEnv: "LITELLM_API_KEY",
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("config.local.yaml adds new provider alongside config.yaml providers", async () => {
|
|
299
|
+
await ensureOverstoryDir();
|
|
300
|
+
await writeConfig(`
|
|
301
|
+
providers:
|
|
302
|
+
openrouter:
|
|
303
|
+
type: gateway
|
|
304
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
305
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
306
|
+
`);
|
|
307
|
+
await Bun.write(
|
|
308
|
+
join(tempDir, ".overstory", "config.local.yaml"),
|
|
309
|
+
`providers:\n litellm:\n type: gateway\n baseUrl: http://localhost:4000\n authTokenEnv: LITELLM_API_KEY\n`,
|
|
310
|
+
);
|
|
311
|
+
const config = await loadConfig(tempDir);
|
|
312
|
+
// All three providers present: default anthropic + openrouter from config.yaml + litellm from config.local.yaml
|
|
313
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
314
|
+
expect(config.providers.openrouter).toEqual({
|
|
315
|
+
type: "gateway",
|
|
316
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
317
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
318
|
+
});
|
|
319
|
+
expect(config.providers.litellm).toEqual({
|
|
320
|
+
type: "gateway",
|
|
321
|
+
baseUrl: "http://localhost:4000",
|
|
322
|
+
authTokenEnv: "LITELLM_API_KEY",
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("simple model strings still work without providers section", async () => {
|
|
327
|
+
await ensureOverstoryDir();
|
|
328
|
+
await writeConfig(`
|
|
329
|
+
models:
|
|
330
|
+
coordinator: sonnet
|
|
331
|
+
builder: opus
|
|
332
|
+
monitor: haiku
|
|
333
|
+
`);
|
|
334
|
+
const config = await loadConfig(tempDir);
|
|
335
|
+
expect(config.models.coordinator).toBe("sonnet");
|
|
336
|
+
expect(config.models.builder).toBe("opus");
|
|
337
|
+
expect(config.models.monitor).toBe("haiku");
|
|
338
|
+
// Default anthropic provider still present even without explicit providers section
|
|
339
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
340
|
+
});
|
|
341
|
+
|
|
268
342
|
test("migrates deprecated watchdog tier1/tier2 keys to tier0/tier1", async () => {
|
|
269
343
|
await ensureOverstoryDir();
|
|
270
344
|
await writeConfig(`
|
|
@@ -556,6 +630,28 @@ models:
|
|
|
556
630
|
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
557
631
|
});
|
|
558
632
|
|
|
633
|
+
test("rejects model ref with deeply nested slashes when provider unknown", async () => {
|
|
634
|
+
await writeConfig(`
|
|
635
|
+
models:
|
|
636
|
+
coordinator: unknown/openai/gpt-5.3/latest
|
|
637
|
+
`);
|
|
638
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("accepts model ref with deeply nested slashes when provider exists", async () => {
|
|
642
|
+
await writeConfig(`
|
|
643
|
+
providers:
|
|
644
|
+
openrouter:
|
|
645
|
+
type: gateway
|
|
646
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
647
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
648
|
+
models:
|
|
649
|
+
coordinator: openrouter/openai/gpt-5.3/variant
|
|
650
|
+
`);
|
|
651
|
+
const config = await loadConfig(tempDir);
|
|
652
|
+
expect(config.models.coordinator).toBe("openrouter/openai/gpt-5.3/variant");
|
|
653
|
+
});
|
|
654
|
+
|
|
559
655
|
test("rejects bare invalid model name", async () => {
|
|
560
656
|
await writeConfig(`
|
|
561
657
|
models:
|
|
@@ -195,7 +195,7 @@ describe("checkConsistency", () => {
|
|
|
195
195
|
capability: "builder",
|
|
196
196
|
worktreePath: join(overstoryDir, "worktrees", "dead-agent"),
|
|
197
197
|
branchName: "overstory/dead-agent/test-123",
|
|
198
|
-
|
|
198
|
+
taskId: "test-123",
|
|
199
199
|
tmuxSession: "overstory-testproject-dead-agent",
|
|
200
200
|
state: "working",
|
|
201
201
|
pid: 99999, // Non-existent PID
|
|
@@ -231,7 +231,7 @@ describe("checkConsistency", () => {
|
|
|
231
231
|
capability: "builder",
|
|
232
232
|
worktreePath: join(overstoryDir, "worktrees", "live-agent"),
|
|
233
233
|
branchName: "overstory/live-agent/test-123",
|
|
234
|
-
|
|
234
|
+
taskId: "test-123",
|
|
235
235
|
tmuxSession: "overstory-testproject-live-agent",
|
|
236
236
|
state: "working",
|
|
237
237
|
pid: 12345,
|
|
@@ -266,7 +266,7 @@ describe("checkConsistency", () => {
|
|
|
266
266
|
capability: "builder",
|
|
267
267
|
worktreePath: missingWorktreePath,
|
|
268
268
|
branchName: "overstory/missing-agent/test-123",
|
|
269
|
-
|
|
269
|
+
taskId: "test-123",
|
|
270
270
|
tmuxSession: "overstory-testproject-missing-agent",
|
|
271
271
|
state: "working",
|
|
272
272
|
pid: null,
|
|
@@ -302,7 +302,7 @@ describe("checkConsistency", () => {
|
|
|
302
302
|
capability: "builder",
|
|
303
303
|
worktreePath,
|
|
304
304
|
branchName: "overstory/agent-without-tmux/test-123",
|
|
305
|
-
|
|
305
|
+
taskId: "test-123",
|
|
306
306
|
tmuxSession: "overstory-testproject-agent-without-tmux",
|
|
307
307
|
state: "working",
|
|
308
308
|
pid: null,
|
|
@@ -341,7 +341,7 @@ describe("checkConsistency", () => {
|
|
|
341
341
|
capability: "builder",
|
|
342
342
|
worktreePath,
|
|
343
343
|
branchName: "overstory/consistent-agent/test-123",
|
|
344
|
-
|
|
344
|
+
taskId: "test-123",
|
|
345
345
|
tmuxSession: "overstory-testproject-consistent-agent",
|
|
346
346
|
state: "working",
|
|
347
347
|
pid: 12345,
|
|
@@ -414,7 +414,7 @@ describe("checkConsistency", () => {
|
|
|
414
414
|
capability: "builder",
|
|
415
415
|
worktreePath: join(overstoryDir, "worktrees", "builder-1"),
|
|
416
416
|
branchName: "overstory/builder-1/test-123",
|
|
417
|
-
|
|
417
|
+
taskId: "test-123",
|
|
418
418
|
tmuxSession: "overstory-testproject-builder-1",
|
|
419
419
|
state: "working",
|
|
420
420
|
pid: null,
|
|
@@ -433,7 +433,7 @@ describe("checkConsistency", () => {
|
|
|
433
433
|
capability: "builder",
|
|
434
434
|
worktreePath: join(overstoryDir, "worktrees", "builder-2"),
|
|
435
435
|
branchName: "overstory/builder-2/test-456",
|
|
436
|
-
|
|
436
|
+
taskId: "test-456",
|
|
437
437
|
tmuxSession: "overstory-testproject-builder-2",
|
|
438
438
|
state: "working",
|
|
439
439
|
pid: null,
|
|
@@ -469,7 +469,7 @@ describe("checkConsistency", () => {
|
|
|
469
469
|
capability: "builder",
|
|
470
470
|
worktreePath: join(overstoryDir, "worktrees", `builder-${i}`),
|
|
471
471
|
branchName: `overstory/builder-${i}/test-${i}`,
|
|
472
|
-
|
|
472
|
+
taskId: `test-${i}`,
|
|
473
473
|
tmuxSession: `overstory-testproject-builder-${i}`,
|
|
474
474
|
state: "working",
|
|
475
475
|
pid: null,
|
|
@@ -489,7 +489,7 @@ describe("checkConsistency", () => {
|
|
|
489
489
|
capability: "reviewer",
|
|
490
490
|
worktreePath: join(overstoryDir, "worktrees", "reviewer-1"),
|
|
491
491
|
branchName: "overstory/reviewer-1/test-r1",
|
|
492
|
-
|
|
492
|
+
taskId: "test-r1",
|
|
493
493
|
tmuxSession: "overstory-testproject-reviewer-1",
|
|
494
494
|
state: "working",
|
|
495
495
|
pid: null,
|
|
@@ -523,7 +523,7 @@ describe("checkConsistency", () => {
|
|
|
523
523
|
capability: "builder",
|
|
524
524
|
worktreePath: join(overstoryDir, "worktrees", `builder-${i}`),
|
|
525
525
|
branchName: `overstory/builder-${i}/test-${i}`,
|
|
526
|
-
|
|
526
|
+
taskId: `test-${i}`,
|
|
527
527
|
tmuxSession: `overstory-testproject-builder-${i}`,
|
|
528
528
|
state: "working",
|
|
529
529
|
pid: null,
|
|
@@ -542,7 +542,7 @@ describe("checkConsistency", () => {
|
|
|
542
542
|
capability: "reviewer",
|
|
543
543
|
worktreePath: join(overstoryDir, "worktrees", `reviewer-${i}`),
|
|
544
544
|
branchName: `overstory/reviewer-${i}/test-r${i}`,
|
|
545
|
-
|
|
545
|
+
taskId: `test-r${i}`,
|
|
546
546
|
tmuxSession: `overstory-testproject-reviewer-${i}`,
|
|
547
547
|
state: "working",
|
|
548
548
|
pid: null,
|
|
@@ -585,7 +585,7 @@ describe("checkConsistency", () => {
|
|
|
585
585
|
capability: "builder",
|
|
586
586
|
worktreePath: join(overstoryDir, "worktrees", "builder-1"),
|
|
587
587
|
branchName: "overstory/builder-1/test-1",
|
|
588
|
-
|
|
588
|
+
taskId: "test-1",
|
|
589
589
|
tmuxSession: "overstory-testproject-builder-1",
|
|
590
590
|
state: "working",
|
|
591
591
|
pid: null,
|
|
@@ -604,7 +604,7 @@ describe("checkConsistency", () => {
|
|
|
604
604
|
capability: "reviewer",
|
|
605
605
|
worktreePath: join(overstoryDir, "worktrees", "reviewer-1"),
|
|
606
606
|
branchName: "overstory/reviewer-1/test-r1",
|
|
607
|
-
|
|
607
|
+
taskId: "test-r1",
|
|
608
608
|
tmuxSession: "overstory-testproject-reviewer-1",
|
|
609
609
|
state: "working",
|
|
610
610
|
pid: null,
|
|
@@ -624,7 +624,7 @@ describe("checkConsistency", () => {
|
|
|
624
624
|
capability: "builder",
|
|
625
625
|
worktreePath: join(overstoryDir, "worktrees", "builder-2"),
|
|
626
626
|
branchName: "overstory/builder-2/test-2",
|
|
627
|
-
|
|
627
|
+
taskId: "test-2",
|
|
628
628
|
tmuxSession: "overstory-testproject-builder-2",
|
|
629
629
|
state: "working",
|
|
630
630
|
pid: null,
|
|
@@ -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(
|
|
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(
|
|
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", () => {
|
package/src/doctor/databases.ts
CHANGED
|
@@ -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(
|
|
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
|
});
|