@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/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";
|
|
@@ -13,6 +14,7 @@ import { createMergeQueue } from "../merge/queue.ts";
|
|
|
13
14
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
14
15
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
15
16
|
import type { AgentSession } from "../types.ts";
|
|
17
|
+
import { evaluateHealth } from "../watchdog/health.ts";
|
|
16
18
|
import { listWorktrees } from "../worktree/manager.ts";
|
|
17
19
|
import { listSessions } from "../worktree/tmux.ts";
|
|
18
20
|
|
|
@@ -64,21 +66,6 @@ export async function getCachedTmuxSessions(
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
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
69
|
/**
|
|
83
70
|
* Format a duration in ms to a human-readable string.
|
|
84
71
|
*/
|
|
@@ -150,23 +137,23 @@ export async function gatherStatus(
|
|
|
150
137
|
|
|
151
138
|
const tmuxSessions = await getCachedTmuxSessions();
|
|
152
139
|
|
|
153
|
-
// Reconcile agent states
|
|
154
|
-
//
|
|
140
|
+
// Reconcile agent states using the same health evaluation as the
|
|
141
|
+
// dashboard and watchdog. This handles:
|
|
142
|
+
// 1. tmux dead -> zombie (regardless of recorded state)
|
|
143
|
+
// 2. persistent capabilities (coordinator, monitor) booting -> working when tmux alive
|
|
144
|
+
// 3. time-based stale/zombie detection for non-persistent agents
|
|
155
145
|
const tmuxSessionNames = new Set(tmuxSessions.map((s) => s.name));
|
|
146
|
+
const healthThresholds = { staleMs: 300_000, zombieMs: 600_000 };
|
|
156
147
|
for (const session of sessions) {
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
session.state = "zombie";
|
|
167
|
-
} catch {
|
|
168
|
-
// Best effort: don't fail status display if update fails
|
|
169
|
-
}
|
|
148
|
+
if (session.state === "completed") continue;
|
|
149
|
+
const tmuxAlive = tmuxSessionNames.has(session.tmuxSession);
|
|
150
|
+
const check = evaluateHealth(session, tmuxAlive, healthThresholds);
|
|
151
|
+
if (check.state !== session.state) {
|
|
152
|
+
try {
|
|
153
|
+
store.updateState(session.agentName, check.state);
|
|
154
|
+
session.state = check.state;
|
|
155
|
+
} catch {
|
|
156
|
+
// Best effort: don't fail status display if update fails
|
|
170
157
|
}
|
|
171
158
|
}
|
|
172
159
|
}
|
|
@@ -287,7 +274,7 @@ export function printStatus(data: StatusData): void {
|
|
|
287
274
|
const tmuxAlive = tmuxSessionNames.has(agent.tmuxSession);
|
|
288
275
|
const aliveMarker = tmuxAlive ? "●" : "○";
|
|
289
276
|
w(` ${aliveMarker} ${agent.agentName} [${agent.capability}] `);
|
|
290
|
-
w(`${agent.state} | ${agent.
|
|
277
|
+
w(`${agent.state} | ${agent.taskId} | ${duration}\n`);
|
|
291
278
|
|
|
292
279
|
const detail = data.verboseDetails?.[agent.agentName];
|
|
293
280
|
if (detail) {
|
|
@@ -323,33 +310,21 @@ export function printStatus(data: StatusData): void {
|
|
|
323
310
|
w(`📈 Sessions recorded: ${data.recentMetricsCount}\n`);
|
|
324
311
|
}
|
|
325
312
|
|
|
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
|
-
}
|
|
313
|
+
interface StatusOpts {
|
|
314
|
+
json?: boolean;
|
|
315
|
+
watch?: boolean;
|
|
316
|
+
verbose?: boolean;
|
|
317
|
+
all?: boolean;
|
|
318
|
+
interval?: string;
|
|
319
|
+
agent?: string;
|
|
320
|
+
}
|
|
347
321
|
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
const
|
|
322
|
+
async function executeStatus(opts: StatusOpts): Promise<void> {
|
|
323
|
+
const json = opts.json ?? false;
|
|
324
|
+
const watch = opts.watch ?? false;
|
|
325
|
+
const verbose = opts.verbose ?? false;
|
|
326
|
+
const all = opts.all ?? false;
|
|
327
|
+
const intervalStr = opts.interval;
|
|
353
328
|
const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 3000;
|
|
354
329
|
|
|
355
330
|
if (Number.isNaN(interval) || interval < 500) {
|
|
@@ -359,7 +334,7 @@ export async function statusCommand(args: string[]): Promise<void> {
|
|
|
359
334
|
});
|
|
360
335
|
}
|
|
361
336
|
|
|
362
|
-
const agentName =
|
|
337
|
+
const agentName = opts.agent ?? "orchestrator";
|
|
363
338
|
|
|
364
339
|
const cwd = process.cwd();
|
|
365
340
|
const config = await loadConfig(cwd);
|
|
@@ -396,3 +371,33 @@ export async function statusCommand(args: string[]): Promise<void> {
|
|
|
396
371
|
}
|
|
397
372
|
}
|
|
398
373
|
}
|
|
374
|
+
|
|
375
|
+
export function createStatusCommand(): Command {
|
|
376
|
+
return new Command("status")
|
|
377
|
+
.description("Show all active agents and project state")
|
|
378
|
+
.option("--json", "Output as JSON")
|
|
379
|
+
.option("--verbose", "Show extra detail per agent (worktree, logs, mail timestamps)")
|
|
380
|
+
.option("--agent <name>", "Show unread mail for this agent (default: orchestrator)")
|
|
381
|
+
.option("--all", "Show sessions from all runs (default: current run only)")
|
|
382
|
+
.option("--watch", "(deprecated) Use 'overstory dashboard' for live monitoring")
|
|
383
|
+
.option("--interval <ms>", "Poll interval for --watch in milliseconds (default: 3000)")
|
|
384
|
+
.action(async (opts: StatusOpts) => {
|
|
385
|
+
await executeStatus(opts);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function statusCommand(args: string[]): Promise<void> {
|
|
390
|
+
const cmd = createStatusCommand();
|
|
391
|
+
cmd.exitOverride();
|
|
392
|
+
try {
|
|
393
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
394
|
+
} catch (err: unknown) {
|
|
395
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
396
|
+
const code = (err as { code: string }).code;
|
|
397
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -137,7 +137,7 @@ function makeAgentSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
|
137
137
|
capability: "builder",
|
|
138
138
|
worktreePath: join(tempDir, ".overstory", "worktrees", "my-builder"),
|
|
139
139
|
branchName: "overstory/my-builder/bead-123",
|
|
140
|
-
|
|
140
|
+
taskId: "bead-123",
|
|
141
141
|
tmuxSession: "overstory-test-project-my-builder",
|
|
142
142
|
state: "working",
|
|
143
143
|
pid: 99999,
|
|
@@ -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
|
|
|
@@ -11,10 +11,10 @@ import { buildSupervisorBeacon, supervisorCommand } from "./supervisor.ts";
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
describe("buildSupervisorBeacon", () => {
|
|
14
|
-
test("contains agent name and
|
|
14
|
+
test("contains agent name and taskId from opts", () => {
|
|
15
15
|
const beacon = buildSupervisorBeacon({
|
|
16
16
|
name: "supervisor-1",
|
|
17
|
-
|
|
17
|
+
taskId: "task-abc123",
|
|
18
18
|
depth: 1,
|
|
19
19
|
parent: "coordinator",
|
|
20
20
|
});
|
|
@@ -26,7 +26,7 @@ describe("buildSupervisorBeacon", () => {
|
|
|
26
26
|
test("contains [OVERSTORY] prefix", () => {
|
|
27
27
|
const beacon = buildSupervisorBeacon({
|
|
28
28
|
name: "supervisor-1",
|
|
29
|
-
|
|
29
|
+
taskId: "task-1",
|
|
30
30
|
depth: 1,
|
|
31
31
|
parent: "coordinator",
|
|
32
32
|
});
|
|
@@ -37,7 +37,7 @@ describe("buildSupervisorBeacon", () => {
|
|
|
37
37
|
test("contains (supervisor) designation", () => {
|
|
38
38
|
const beacon = buildSupervisorBeacon({
|
|
39
39
|
name: "supervisor-1",
|
|
40
|
-
|
|
40
|
+
taskId: "task-1",
|
|
41
41
|
depth: 1,
|
|
42
42
|
parent: "coordinator",
|
|
43
43
|
});
|
|
@@ -48,7 +48,7 @@ describe("buildSupervisorBeacon", () => {
|
|
|
48
48
|
test("contains depth and parent info from opts", () => {
|
|
49
49
|
const beacon = buildSupervisorBeacon({
|
|
50
50
|
name: "supervisor-1",
|
|
51
|
-
|
|
51
|
+
taskId: "task-1",
|
|
52
52
|
depth: 2,
|
|
53
53
|
parent: "lead-cli",
|
|
54
54
|
});
|
|
@@ -60,7 +60,7 @@ describe("buildSupervisorBeacon", () => {
|
|
|
60
60
|
test("contains startup instructions", () => {
|
|
61
61
|
const beacon = buildSupervisorBeacon({
|
|
62
62
|
name: "supervisor-1",
|
|
63
|
-
|
|
63
|
+
taskId: "task-1",
|
|
64
64
|
depth: 1,
|
|
65
65
|
parent: "coordinator",
|
|
66
66
|
});
|
|
@@ -71,7 +71,7 @@ describe("buildSupervisorBeacon", () => {
|
|
|
71
71
|
// Should include mail check with agent name
|
|
72
72
|
expect(beacon).toContain("overstory mail check --agent supervisor-1");
|
|
73
73
|
|
|
74
|
-
// Should include bd show with
|
|
74
|
+
// Should include bd show with taskId
|
|
75
75
|
expect(beacon).toContain("bd show task-1");
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -79,13 +79,13 @@ describe("buildSupervisorBeacon", () => {
|
|
|
79
79
|
const before = new Date();
|
|
80
80
|
const beacon = buildSupervisorBeacon({
|
|
81
81
|
name: "supervisor-1",
|
|
82
|
-
|
|
82
|
+
taskId: "task-1",
|
|
83
83
|
depth: 1,
|
|
84
84
|
parent: "coordinator",
|
|
85
85
|
});
|
|
86
86
|
const after = new Date();
|
|
87
87
|
|
|
88
|
-
// Extract timestamp from beacon (format: [OVERSTORY] {name} (supervisor) {timestamp} task:{
|
|
88
|
+
// Extract timestamp from beacon (format: [OVERSTORY] {name} (supervisor) {timestamp} task:{taskId})
|
|
89
89
|
const timestampMatch = beacon.match(/\(supervisor\)\s+(\S+)\s+task:/);
|
|
90
90
|
expect(timestampMatch).toBeTruthy();
|
|
91
91
|
|
|
@@ -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
|
}
|