@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.
- package/README.md +7 -6
- package/package.json +12 -4
- package/src/agents/hooks-deployer.test.ts +94 -16
- package/src/agents/hooks-deployer.ts +18 -0
- package/src/agents/manifest.test.ts +86 -0
- package/src/commands/agents.test.ts +3 -3
- package/src/commands/agents.ts +59 -88
- package/src/commands/clean.test.ts +31 -46
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +131 -24
- package/src/commands/coordinator.ts +100 -63
- package/src/commands/costs.test.ts +2 -2
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +73 -93
- 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 +2 -2
- package/src/commands/inspect.ts +54 -57
- package/src/commands/log.test.ts +5 -10
- package/src/commands/log.ts +90 -84
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +20 -58
- package/src/commands/merge.ts +13 -43
- package/src/commands/metrics.test.ts +2 -2
- package/src/commands/metrics.ts +33 -34
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +56 -61
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +15 -47
- package/src/commands/prime.ts +7 -44
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +196 -0
- package/src/commands/sling.ts +24 -54
- package/src/commands/spec.test.ts +13 -39
- package/src/commands/spec.ts +30 -99
- package/src/commands/status.ts +46 -42
- package/src/commands/stop.test.ts +21 -39
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +3 -5
- package/src/commands/supervisor.ts +136 -157
- package/src/commands/trace.test.ts +9 -9
- package/src/commands/trace.ts +54 -77
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +8 -8
- package/src/commands/worktree.ts +63 -46
- package/src/config.test.ts +96 -0
- 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/e2e/init-sling-lifecycle.test.ts +6 -6
- 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/merge/queue.test.ts +66 -0
- package/src/merge/queue.ts +15 -0
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.ts +1 -1
- package/src/sessions/store.test.ts +37 -0
- package/src/sessions/store.ts +11 -0
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
package/src/commands/spec.ts
CHANGED
|
@@ -12,42 +12,9 @@ import { mkdir } from "node:fs/promises";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { ValidationError } from "../errors.ts";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Parse a named flag value from args.
|
|
20
|
-
*/
|
|
21
|
-
function getFlag(args: string[], flag: string): string | undefined {
|
|
22
|
-
const idx = args.indexOf(flag);
|
|
23
|
-
if (idx === -1 || idx + 1 >= args.length) {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
return args[idx + 1];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Extract positional arguments, skipping flag-value pairs.
|
|
31
|
-
*/
|
|
32
|
-
function getPositionalArgs(args: string[]): string[] {
|
|
33
|
-
const positional: string[] = [];
|
|
34
|
-
let i = 0;
|
|
35
|
-
while (i < args.length) {
|
|
36
|
-
const arg = args[i];
|
|
37
|
-
if (arg?.startsWith("-")) {
|
|
38
|
-
if (BOOLEAN_FLAGS.has(arg)) {
|
|
39
|
-
i += 1;
|
|
40
|
-
} else {
|
|
41
|
-
i += 2;
|
|
42
|
-
}
|
|
43
|
-
} else {
|
|
44
|
-
if (arg !== undefined) {
|
|
45
|
-
positional.push(arg);
|
|
46
|
-
}
|
|
47
|
-
i += 1;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return positional;
|
|
15
|
+
export interface SpecWriteOptions {
|
|
16
|
+
body?: string;
|
|
17
|
+
agent?: string;
|
|
51
18
|
}
|
|
52
19
|
|
|
53
20
|
/**
|
|
@@ -62,23 +29,6 @@ async function readStdin(): Promise<string> {
|
|
|
62
29
|
return await new Response(Bun.stdin.stream()).text();
|
|
63
30
|
}
|
|
64
31
|
|
|
65
|
-
const SPEC_HELP = `overstory spec -- Manage task specifications
|
|
66
|
-
|
|
67
|
-
Usage: overstory spec <subcommand> [args...]
|
|
68
|
-
|
|
69
|
-
Subcommands:
|
|
70
|
-
write <bead-id> Write a spec file to .overstory/specs/<bead-id>.md
|
|
71
|
-
|
|
72
|
-
Options for 'write':
|
|
73
|
-
--body <content> Spec content (or pipe via stdin)
|
|
74
|
-
--agent <name> Agent writing the spec (for attribution)
|
|
75
|
-
--help, -h Show this help
|
|
76
|
-
|
|
77
|
-
Examples:
|
|
78
|
-
overstory spec write task-abc --body "# Spec\\nDetails here..."
|
|
79
|
-
echo "# Spec" | overstory spec write task-abc
|
|
80
|
-
overstory spec write task-abc --body "..." --agent scout-1`;
|
|
81
|
-
|
|
82
32
|
/**
|
|
83
33
|
* Write a spec file to .overstory/specs/<bead-id>.md.
|
|
84
34
|
*
|
|
@@ -112,57 +62,38 @@ export async function writeSpec(
|
|
|
112
62
|
}
|
|
113
63
|
|
|
114
64
|
/**
|
|
115
|
-
* Entry point for `overstory spec <
|
|
65
|
+
* Entry point for `overstory spec write <bead-id> [flags]`.
|
|
66
|
+
*
|
|
67
|
+
* @param beadId - The bead/task ID for the spec file
|
|
68
|
+
* @param opts - Command options
|
|
116
69
|
*/
|
|
117
|
-
export async function
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
70
|
+
export async function specWriteCommand(beadId: string, opts: SpecWriteOptions): Promise<void> {
|
|
71
|
+
if (!beadId || beadId.trim().length === 0) {
|
|
72
|
+
throw new ValidationError(
|
|
73
|
+
"Bead ID is required: overstory spec write <bead-id> --body <content>",
|
|
74
|
+
{ field: "beadId" },
|
|
75
|
+
);
|
|
121
76
|
}
|
|
122
77
|
|
|
123
|
-
|
|
124
|
-
const subArgs = args.slice(1);
|
|
125
|
-
|
|
126
|
-
switch (subcommand) {
|
|
127
|
-
case "write": {
|
|
128
|
-
const positional = getPositionalArgs(subArgs);
|
|
129
|
-
const beadId = positional[0];
|
|
130
|
-
if (!beadId || beadId.trim().length === 0) {
|
|
131
|
-
throw new ValidationError(
|
|
132
|
-
"Bead ID is required: overstory spec write <bead-id> --body <content>",
|
|
133
|
-
{ field: "beadId" },
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const agent = getFlag(subArgs, "--agent");
|
|
138
|
-
let body = getFlag(subArgs, "--body");
|
|
78
|
+
let body = opts.body;
|
|
139
79
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (body === undefined || body.trim().length === 0) {
|
|
149
|
-
throw new ValidationError("Spec body is required: use --body <content> or pipe via stdin", {
|
|
150
|
-
field: "body",
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const { resolveProjectRoot } = await import("../config.ts");
|
|
155
|
-
const projectRoot = await resolveProjectRoot(process.cwd());
|
|
156
|
-
|
|
157
|
-
const specPath = await writeSpec(projectRoot, beadId, body, agent);
|
|
158
|
-
process.stdout.write(`${specPath}\n`);
|
|
159
|
-
break;
|
|
80
|
+
// If no --body flag, try reading from stdin
|
|
81
|
+
if (body === undefined) {
|
|
82
|
+
const stdinContent = await readStdin();
|
|
83
|
+
if (stdinContent.trim().length > 0) {
|
|
84
|
+
body = stdinContent;
|
|
160
85
|
}
|
|
86
|
+
}
|
|
161
87
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
88
|
+
if (body === undefined || body.trim().length === 0) {
|
|
89
|
+
throw new ValidationError("Spec body is required: use --body <content> or pipe via stdin", {
|
|
90
|
+
field: "body",
|
|
91
|
+
});
|
|
167
92
|
}
|
|
93
|
+
|
|
94
|
+
const { resolveProjectRoot } = await import("../config.ts");
|
|
95
|
+
const projectRoot = await resolveProjectRoot(process.cwd());
|
|
96
|
+
|
|
97
|
+
const specPath = await writeSpec(projectRoot, beadId, body, opts.agent);
|
|
98
|
+
process.stdout.write(`${specPath}\n`);
|
|
168
99
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { Command } from "commander";
|
|
9
10
|
import { loadConfig } from "../config.ts";
|
|
10
11
|
import { ValidationError } from "../errors.ts";
|
|
11
12
|
import { createMailStore } from "../mail/store.ts";
|
|
@@ -64,21 +65,6 @@ export async function getCachedTmuxSessions(
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
-
* Parse a named flag value from args.
|
|
69
|
-
*/
|
|
70
|
-
function getFlag(args: string[], flag: string): string | undefined {
|
|
71
|
-
const idx = args.indexOf(flag);
|
|
72
|
-
if (idx === -1 || idx + 1 >= args.length) {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
return args[idx + 1];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
79
|
-
return args.includes(flag);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
68
|
/**
|
|
83
69
|
* Format a duration in ms to a human-readable string.
|
|
84
70
|
*/
|
|
@@ -323,33 +309,21 @@ export function printStatus(data: StatusData): void {
|
|
|
323
309
|
w(`📈 Sessions recorded: ${data.recentMetricsCount}\n`);
|
|
324
310
|
}
|
|
325
311
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
--json Output as JSON
|
|
335
|
-
--verbose Show extra detail per agent (worktree, logs, mail timestamps)
|
|
336
|
-
--agent <name> Show unread mail for this agent (default: orchestrator)
|
|
337
|
-
--all Show sessions from all runs (default: current run only)
|
|
338
|
-
--watch (deprecated) Use 'overstory dashboard' for live monitoring
|
|
339
|
-
--interval <ms> Poll interval for --watch in milliseconds (default: 3000)
|
|
340
|
-
--help, -h Show this help`;
|
|
341
|
-
|
|
342
|
-
export async function statusCommand(args: string[]): Promise<void> {
|
|
343
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
344
|
-
process.stdout.write(`${STATUS_HELP}\n`);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
312
|
+
interface StatusOpts {
|
|
313
|
+
json?: boolean;
|
|
314
|
+
watch?: boolean;
|
|
315
|
+
verbose?: boolean;
|
|
316
|
+
all?: boolean;
|
|
317
|
+
interval?: string;
|
|
318
|
+
agent?: string;
|
|
319
|
+
}
|
|
347
320
|
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
const
|
|
321
|
+
async function executeStatus(opts: StatusOpts): Promise<void> {
|
|
322
|
+
const json = opts.json ?? false;
|
|
323
|
+
const watch = opts.watch ?? false;
|
|
324
|
+
const verbose = opts.verbose ?? false;
|
|
325
|
+
const all = opts.all ?? false;
|
|
326
|
+
const intervalStr = opts.interval;
|
|
353
327
|
const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 3000;
|
|
354
328
|
|
|
355
329
|
if (Number.isNaN(interval) || interval < 500) {
|
|
@@ -359,7 +333,7 @@ export async function statusCommand(args: string[]): Promise<void> {
|
|
|
359
333
|
});
|
|
360
334
|
}
|
|
361
335
|
|
|
362
|
-
const agentName =
|
|
336
|
+
const agentName = opts.agent ?? "orchestrator";
|
|
363
337
|
|
|
364
338
|
const cwd = process.cwd();
|
|
365
339
|
const config = await loadConfig(cwd);
|
|
@@ -396,3 +370,33 @@ export async function statusCommand(args: string[]): Promise<void> {
|
|
|
396
370
|
}
|
|
397
371
|
}
|
|
398
372
|
}
|
|
373
|
+
|
|
374
|
+
export function createStatusCommand(): Command {
|
|
375
|
+
return new Command("status")
|
|
376
|
+
.description("Show all active agents and project state")
|
|
377
|
+
.option("--json", "Output as JSON")
|
|
378
|
+
.option("--verbose", "Show extra detail per agent (worktree, logs, mail timestamps)")
|
|
379
|
+
.option("--agent <name>", "Show unread mail for this agent (default: orchestrator)")
|
|
380
|
+
.option("--all", "Show sessions from all runs (default: current run only)")
|
|
381
|
+
.option("--watch", "(deprecated) Use 'overstory dashboard' for live monitoring")
|
|
382
|
+
.option("--interval <ms>", "Poll interval for --watch in milliseconds (default: 3000)")
|
|
383
|
+
.action(async (opts: StatusOpts) => {
|
|
384
|
+
await executeStatus(opts);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function statusCommand(args: string[]): Promise<void> {
|
|
389
|
+
const cmd = createStatusCommand();
|
|
390
|
+
cmd.exitOverride();
|
|
391
|
+
try {
|
|
392
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
393
|
+
} catch (err: unknown) {
|
|
394
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
395
|
+
const code = (err as { code: string }).code;
|
|
396
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -207,37 +207,15 @@ function makeDeps(
|
|
|
207
207
|
|
|
208
208
|
// --- Tests ---
|
|
209
209
|
|
|
210
|
-
describe("stopCommand help", () => {
|
|
211
|
-
test("--help outputs help text", async () => {
|
|
212
|
-
const output = await captureStdout(() => stopCommand(["--help"]));
|
|
213
|
-
expect(output).toContain("overstory stop");
|
|
214
|
-
expect(output).toContain("<agent-name>");
|
|
215
|
-
expect(output).toContain("--force");
|
|
216
|
-
expect(output).toContain("--clean-worktree");
|
|
217
|
-
expect(output).toContain("--json");
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("-h outputs help text", async () => {
|
|
221
|
-
const output = await captureStdout(() => stopCommand(["-h"]));
|
|
222
|
-
expect(output).toContain("overstory stop");
|
|
223
|
-
expect(output).toContain("<agent-name>");
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
210
|
describe("stopCommand validation", () => {
|
|
228
|
-
test("throws ValidationError when
|
|
211
|
+
test("throws ValidationError when agent name is empty string", async () => {
|
|
229
212
|
const { deps } = makeDeps();
|
|
230
|
-
await expect(stopCommand(
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("throws ValidationError when only flags are provided (no agent name)", async () => {
|
|
234
|
-
const { deps } = makeDeps();
|
|
235
|
-
await expect(stopCommand(["--json"], deps)).rejects.toThrow(ValidationError);
|
|
213
|
+
await expect(stopCommand("", {}, deps)).rejects.toThrow(ValidationError);
|
|
236
214
|
});
|
|
237
215
|
|
|
238
216
|
test("throws AgentError when agent not found", async () => {
|
|
239
217
|
const { deps } = makeDeps();
|
|
240
|
-
await expect(stopCommand(
|
|
218
|
+
await expect(stopCommand("nonexistent-agent", {}, deps)).rejects.toThrow(AgentError);
|
|
241
219
|
});
|
|
242
220
|
|
|
243
221
|
test("throws AgentError when agent is already completed", async () => {
|
|
@@ -245,8 +223,8 @@ describe("stopCommand validation", () => {
|
|
|
245
223
|
saveSessionsToDb([session]);
|
|
246
224
|
|
|
247
225
|
const { deps } = makeDeps();
|
|
248
|
-
await expect(stopCommand(
|
|
249
|
-
await expect(stopCommand(
|
|
226
|
+
await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(AgentError);
|
|
227
|
+
await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(/already completed/);
|
|
250
228
|
});
|
|
251
229
|
|
|
252
230
|
test("throws AgentError when agent is already zombie", async () => {
|
|
@@ -254,8 +232,8 @@ describe("stopCommand validation", () => {
|
|
|
254
232
|
saveSessionsToDb([session]);
|
|
255
233
|
|
|
256
234
|
const { deps } = makeDeps();
|
|
257
|
-
await expect(stopCommand(
|
|
258
|
-
await expect(stopCommand(
|
|
235
|
+
await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(AgentError);
|
|
236
|
+
await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(/zombie/);
|
|
259
237
|
});
|
|
260
238
|
});
|
|
261
239
|
|
|
@@ -265,7 +243,7 @@ describe("stopCommand stop behavior", () => {
|
|
|
265
243
|
saveSessionsToDb([session]);
|
|
266
244
|
|
|
267
245
|
const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: true });
|
|
268
|
-
const output = await captureStdout(() => stopCommand(
|
|
246
|
+
const output = await captureStdout(() => stopCommand("my-builder", {}, deps));
|
|
269
247
|
|
|
270
248
|
expect(output).toContain(`Agent "my-builder" stopped`);
|
|
271
249
|
expect(output).toContain(`Tmux session killed: ${session.tmuxSession}`);
|
|
@@ -284,7 +262,7 @@ describe("stopCommand stop behavior", () => {
|
|
|
284
262
|
saveSessionsToDb([session]);
|
|
285
263
|
|
|
286
264
|
const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: true });
|
|
287
|
-
await stopCommand(
|
|
265
|
+
await stopCommand("my-builder", {}, deps);
|
|
288
266
|
|
|
289
267
|
expect(tmuxCalls.killSession).toHaveLength(1);
|
|
290
268
|
const { store } = openSessionStore(overstoryDir);
|
|
@@ -298,7 +276,7 @@ describe("stopCommand stop behavior", () => {
|
|
|
298
276
|
saveSessionsToDb([session]);
|
|
299
277
|
|
|
300
278
|
const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: true });
|
|
301
|
-
await stopCommand(
|
|
279
|
+
await stopCommand("my-builder", {}, deps);
|
|
302
280
|
|
|
303
281
|
expect(tmuxCalls.killSession).toHaveLength(1);
|
|
304
282
|
const { store } = openSessionStore(overstoryDir);
|
|
@@ -313,7 +291,7 @@ describe("stopCommand stop behavior", () => {
|
|
|
313
291
|
|
|
314
292
|
// tmux session is NOT alive
|
|
315
293
|
const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: false });
|
|
316
|
-
const output = await captureStdout(() => stopCommand(
|
|
294
|
+
const output = await captureStdout(() => stopCommand("my-builder", {}, deps));
|
|
317
295
|
|
|
318
296
|
expect(output).toContain("Tmux session was already dead");
|
|
319
297
|
expect(tmuxCalls.killSession).toHaveLength(0);
|
|
@@ -332,7 +310,7 @@ describe("stopCommand --json output", () => {
|
|
|
332
310
|
saveSessionsToDb([session]);
|
|
333
311
|
|
|
334
312
|
const { deps } = makeDeps({ [session.tmuxSession]: true });
|
|
335
|
-
const output = await captureStdout(() => stopCommand(
|
|
313
|
+
const output = await captureStdout(() => stopCommand("my-builder", { json: true }, deps));
|
|
336
314
|
|
|
337
315
|
const parsed = JSON.parse(output.trim()) as Record<string, unknown>;
|
|
338
316
|
expect(parsed.stopped).toBe(true);
|
|
@@ -350,7 +328,7 @@ describe("stopCommand --json output", () => {
|
|
|
350
328
|
|
|
351
329
|
const { deps } = makeDeps({ [session.tmuxSession]: true });
|
|
352
330
|
const output = await captureStdout(() =>
|
|
353
|
-
stopCommand(
|
|
331
|
+
stopCommand("my-builder", { json: true, force: true }, deps),
|
|
354
332
|
);
|
|
355
333
|
|
|
356
334
|
const parsed = JSON.parse(output.trim()) as Record<string, unknown>;
|
|
@@ -364,7 +342,9 @@ describe("stopCommand --clean-worktree", () => {
|
|
|
364
342
|
saveSessionsToDb([session]);
|
|
365
343
|
|
|
366
344
|
const { deps, worktreeCalls } = makeDeps({ [session.tmuxSession]: true });
|
|
367
|
-
const output = await captureStdout(() =>
|
|
345
|
+
const output = await captureStdout(() =>
|
|
346
|
+
stopCommand("my-builder", { cleanWorktree: true }, deps),
|
|
347
|
+
);
|
|
368
348
|
|
|
369
349
|
expect(output).toContain(`Worktree removed: ${session.worktreePath}`);
|
|
370
350
|
expect(worktreeCalls.remove).toHaveLength(1);
|
|
@@ -376,7 +356,9 @@ describe("stopCommand --clean-worktree", () => {
|
|
|
376
356
|
saveSessionsToDb([session]);
|
|
377
357
|
|
|
378
358
|
const { deps, worktreeCalls } = makeDeps({ [session.tmuxSession]: true });
|
|
379
|
-
await captureStdout(() =>
|
|
359
|
+
await captureStdout(() =>
|
|
360
|
+
stopCommand("my-builder", { cleanWorktree: true, force: true }, deps),
|
|
361
|
+
);
|
|
380
362
|
|
|
381
363
|
expect(worktreeCalls.remove).toHaveLength(1);
|
|
382
364
|
expect(worktreeCalls.remove[0]?.options?.force).toBe(true);
|
|
@@ -389,7 +371,7 @@ describe("stopCommand --clean-worktree", () => {
|
|
|
389
371
|
|
|
390
372
|
const { deps } = makeDeps({ [session.tmuxSession]: true }, { shouldFail: true });
|
|
391
373
|
const { stderr, stdout } = await captureStderr(() =>
|
|
392
|
-
stopCommand(
|
|
374
|
+
stopCommand("my-builder", { cleanWorktree: true }, deps),
|
|
393
375
|
);
|
|
394
376
|
|
|
395
377
|
// Agent was still stopped
|
|
@@ -410,7 +392,7 @@ describe("stopCommand --clean-worktree", () => {
|
|
|
410
392
|
|
|
411
393
|
const { deps } = makeDeps({ [session.tmuxSession]: true }, { shouldFail: true });
|
|
412
394
|
const { stdout } = await captureStderr(() =>
|
|
413
|
-
stopCommand(
|
|
395
|
+
stopCommand("my-builder", { cleanWorktree: true, json: true }, deps),
|
|
414
396
|
);
|
|
415
397
|
|
|
416
398
|
const parsed = JSON.parse(stdout.trim()) as Record<string, unknown>;
|
package/src/commands/stop.ts
CHANGED
|
@@ -15,6 +15,12 @@ import { openSessionStore } from "../sessions/compat.ts";
|
|
|
15
15
|
import { removeWorktree } from "../worktree/manager.ts";
|
|
16
16
|
import { isSessionAlive, killSession } from "../worktree/tmux.ts";
|
|
17
17
|
|
|
18
|
+
export interface StopOptions {
|
|
19
|
+
force?: boolean;
|
|
20
|
+
cleanWorktree?: boolean;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
/** Dependency injection for testing. Uses real implementations when omitted. */
|
|
19
25
|
export interface StopDeps {
|
|
20
26
|
_tmux?: {
|
|
@@ -30,50 +36,29 @@ export interface StopDeps {
|
|
|
30
36
|
};
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
const STOP_HELP = `overstory stop — Terminate a running agent
|
|
34
|
-
|
|
35
|
-
Usage: overstory stop <agent-name> [flags]
|
|
36
|
-
|
|
37
|
-
Arguments:
|
|
38
|
-
<agent-name> Name of the agent to stop
|
|
39
|
-
|
|
40
|
-
Options:
|
|
41
|
-
--force Force kill and force-delete branch when cleaning worktree
|
|
42
|
-
--clean-worktree Remove the agent's worktree after stopping
|
|
43
|
-
--json Output as JSON
|
|
44
|
-
--help, -h Show this help
|
|
45
|
-
|
|
46
|
-
Examples:
|
|
47
|
-
overstory stop my-builder
|
|
48
|
-
overstory stop my-builder --clean-worktree
|
|
49
|
-
overstory stop my-builder --clean-worktree --force
|
|
50
|
-
overstory stop my-builder --json`;
|
|
51
|
-
|
|
52
39
|
/**
|
|
53
40
|
* Entry point for `overstory stop <agent-name>`.
|
|
54
41
|
*
|
|
55
|
-
* @param
|
|
42
|
+
* @param agentName - Name of the agent to stop
|
|
43
|
+
* @param opts - Command options
|
|
56
44
|
* @param deps - Optional dependency injection for testing (tmux, worktree)
|
|
57
45
|
*/
|
|
58
|
-
export async function stopCommand(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const json = args.includes("--json");
|
|
65
|
-
const force = args.includes("--force");
|
|
66
|
-
const cleanWorktree = args.includes("--clean-worktree");
|
|
67
|
-
|
|
68
|
-
// First non-flag arg is the agent name
|
|
69
|
-
const agentName = args.find((a) => !a.startsWith("-"));
|
|
70
|
-
if (!agentName) {
|
|
46
|
+
export async function stopCommand(
|
|
47
|
+
agentName: string,
|
|
48
|
+
opts: StopOptions,
|
|
49
|
+
deps: StopDeps = {},
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
if (!agentName || agentName.trim().length === 0) {
|
|
71
52
|
throw new ValidationError("Missing required argument: <agent-name>", {
|
|
72
53
|
field: "agentName",
|
|
73
54
|
value: "",
|
|
74
55
|
});
|
|
75
56
|
}
|
|
76
57
|
|
|
58
|
+
const json = opts.json ?? false;
|
|
59
|
+
const force = opts.force ?? false;
|
|
60
|
+
const cleanWorktree = opts.cleanWorktree ?? false;
|
|
61
|
+
|
|
77
62
|
const tmux = deps._tmux ?? { isSessionAlive, killSession };
|
|
78
63
|
const worktree = deps._worktree ?? { remove: removeWorktree };
|
|
79
64
|
|
|
@@ -111,12 +111,10 @@ describe("supervisorCommand", () => {
|
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
113
|
await supervisorCommand(["--help"]);
|
|
114
|
-
expect(output).toContain("
|
|
114
|
+
expect(output).toContain("supervisor");
|
|
115
115
|
expect(output).toContain("start");
|
|
116
116
|
expect(output).toContain("stop");
|
|
117
117
|
expect(output).toContain("status");
|
|
118
|
-
expect(output).toContain("--task");
|
|
119
|
-
expect(output).toContain("--name");
|
|
120
118
|
} finally {
|
|
121
119
|
process.stdout.write = originalWrite;
|
|
122
120
|
}
|
|
@@ -132,7 +130,7 @@ describe("supervisorCommand", () => {
|
|
|
132
130
|
|
|
133
131
|
try {
|
|
134
132
|
await supervisorCommand(["-h"]);
|
|
135
|
-
expect(output).toContain("
|
|
133
|
+
expect(output).toContain("supervisor");
|
|
136
134
|
} finally {
|
|
137
135
|
process.stdout.write = originalWrite;
|
|
138
136
|
}
|
|
@@ -148,7 +146,7 @@ describe("supervisorCommand", () => {
|
|
|
148
146
|
|
|
149
147
|
try {
|
|
150
148
|
await supervisorCommand([]);
|
|
151
|
-
expect(output).toContain("
|
|
149
|
+
expect(output).toContain("supervisor");
|
|
152
150
|
} finally {
|
|
153
151
|
process.stdout.write = originalWrite;
|
|
154
152
|
}
|