@os-eco/overstory-cli 0.6.7 → 0.6.9
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 +5 -2
- package/agents/coordinator.md +5 -5
- package/agents/lead.md +1 -6
- package/agents/merger.md +3 -3
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +3 -3
- package/agents/supervisor.md +16 -16
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +180 -0
- package/src/agents/hooks-deployer.ts +32 -1
- package/src/commands/agents.ts +9 -6
- package/src/commands/clean.ts +5 -3
- package/src/commands/completions.ts +3 -4
- package/src/commands/coordinator.test.ts +15 -12
- package/src/commands/coordinator.ts +28 -25
- package/src/commands/costs.test.ts +48 -38
- package/src/commands/costs.ts +48 -38
- package/src/commands/dashboard.ts +7 -7
- package/src/commands/doctor.test.ts +8 -0
- package/src/commands/doctor.ts +2 -6
- package/src/commands/errors.test.ts +47 -40
- package/src/commands/errors.ts +5 -4
- package/src/commands/feed.test.ts +40 -33
- package/src/commands/feed.ts +3 -2
- package/src/commands/group.ts +28 -18
- package/src/commands/hooks.test.ts +1 -1
- package/src/commands/hooks.ts +9 -9
- package/src/commands/init.test.ts +105 -5
- package/src/commands/init.ts +17 -12
- package/src/commands/inspect.test.ts +2 -0
- package/src/commands/inspect.ts +9 -8
- package/src/commands/logs.test.ts +5 -6
- package/src/commands/logs.ts +2 -1
- package/src/commands/mail.test.ts +17 -16
- package/src/commands/mail.ts +17 -17
- package/src/commands/merge.ts +12 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +9 -7
- package/src/commands/nudge.ts +4 -4
- package/src/commands/prime.test.ts +1 -6
- package/src/commands/prime.ts +2 -3
- package/src/commands/replay.test.ts +62 -55
- package/src/commands/replay.ts +3 -2
- package/src/commands/run.ts +24 -26
- package/src/commands/sling.ts +4 -2
- package/src/commands/spec.test.ts +10 -8
- package/src/commands/spec.ts +3 -2
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +10 -6
- package/src/commands/stop.ts +13 -13
- package/src/commands/supervisor.ts +12 -10
- package/src/commands/trace.test.ts +52 -44
- package/src/commands/trace.ts +5 -4
- package/src/commands/watch.ts +8 -10
- package/src/commands/worktree.test.ts +27 -20
- package/src/commands/worktree.ts +29 -30
- package/src/doctor/version.ts +2 -2
- package/src/e2e/init-sling-lifecycle.test.ts +1 -5
- package/src/index.ts +99 -14
- package/src/json.test.ts +72 -0
- package/src/json.ts +24 -0
- package/src/logging/color.test.ts +127 -0
- package/src/logging/color.ts +28 -0
- package/src/mulch/client.test.ts +22 -22
- package/src/worktree/tmux.test.ts +123 -5
- package/src/worktree/tmux.ts +38 -8
- package/agents/issue-reviews.md +0 -71
- package/agents/pr-reviews.md +0 -60
- package/agents/prioritize.md +0 -110
- package/agents/release.md +0 -56
package/src/commands/spec.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { mkdir } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { ValidationError } from "../errors.ts";
|
|
14
|
+
import { printSuccess } from "../logging/color.ts";
|
|
14
15
|
|
|
15
16
|
export interface SpecWriteOptions {
|
|
16
17
|
body?: string;
|
|
@@ -93,6 +94,6 @@ export async function specWriteCommand(taskId: string, opts: SpecWriteOptions):
|
|
|
93
94
|
const { resolveProjectRoot } = await import("../config.ts");
|
|
94
95
|
const projectRoot = await resolveProjectRoot(process.cwd());
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
await writeSpec(projectRoot, taskId, body, opts.agent);
|
|
98
|
+
printSuccess("Spec written", taskId);
|
|
98
99
|
}
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { stripAnsi } from "../logging/color.ts";
|
|
5
6
|
import { createSessionStore } from "../sessions/store.ts";
|
|
6
7
|
import { createTempGitRepo } from "../test-helpers.ts";
|
|
7
8
|
import type { AgentSession } from "../types.ts";
|
|
@@ -258,7 +259,7 @@ describe("run scoping", () => {
|
|
|
258
259
|
test("printStatus shows run ID when currentRunId is set", () => {
|
|
259
260
|
const data = makeStatusData({ currentRunId: "run-123" });
|
|
260
261
|
printStatus(data);
|
|
261
|
-
expect(output()).toContain("Run: run-123");
|
|
262
|
+
expect(stripAnsi(output())).toContain("Run: run-123");
|
|
262
263
|
});
|
|
263
264
|
|
|
264
265
|
test("printStatus does not show run line when currentRunId is undefined", () => {
|
package/src/commands/status.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { join } from "node:path";
|
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { loadConfig } from "../config.ts";
|
|
11
11
|
import { ValidationError } from "../errors.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { jsonOutput } from "../json.ts";
|
|
13
|
+
import { accent, color } from "../logging/color.ts";
|
|
13
14
|
import { createMailStore } from "../mail/store.ts";
|
|
14
15
|
import { createMergeQueue } from "../merge/queue.ts";
|
|
15
16
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
@@ -258,7 +259,7 @@ export function printStatus(data: StatusData): void {
|
|
|
258
259
|
w("Overstory Status\n");
|
|
259
260
|
w(`${"═".repeat(60)}\n\n`);
|
|
260
261
|
if (data.currentRunId) {
|
|
261
|
-
w(`Run: ${data.currentRunId}\n`);
|
|
262
|
+
w(`Run: ${accent(data.currentRunId)}\n`);
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
// Active agents
|
|
@@ -274,8 +275,8 @@ export function printStatus(data: StatusData): void {
|
|
|
274
275
|
const duration = formatDuration(endTime - new Date(agent.startedAt).getTime());
|
|
275
276
|
const tmuxAlive = tmuxSessionNames.has(agent.tmuxSession);
|
|
276
277
|
const aliveMarker = tmuxAlive ? color.green(">") : color.red("x");
|
|
277
|
-
w(` ${aliveMarker} ${agent.agentName} [${agent.capability}] `);
|
|
278
|
-
w(`${agent.state} | ${agent.taskId} | ${duration}\n`);
|
|
278
|
+
w(` ${aliveMarker} ${accent(agent.agentName)} [${agent.capability}] `);
|
|
279
|
+
w(`${agent.state} | ${accent(agent.taskId)} | ${duration}\n`);
|
|
279
280
|
|
|
280
281
|
const detail = data.verboseDetails?.[agent.agentName];
|
|
281
282
|
if (detail) {
|
|
@@ -357,7 +358,7 @@ async function executeStatus(opts: StatusOpts): Promise<void> {
|
|
|
357
358
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
358
359
|
const data = await gatherStatus(root, agentName, verbose, runId);
|
|
359
360
|
if (json) {
|
|
360
|
-
|
|
361
|
+
jsonOutput("status", data as unknown as Record<string, unknown>);
|
|
361
362
|
} else {
|
|
362
363
|
printStatus(data);
|
|
363
364
|
}
|
|
@@ -366,7 +367,7 @@ async function executeStatus(opts: StatusOpts): Promise<void> {
|
|
|
366
367
|
} else {
|
|
367
368
|
const data = await gatherStatus(root, agentName, verbose, runId);
|
|
368
369
|
if (json) {
|
|
369
|
-
|
|
370
|
+
jsonOutput("status", data as unknown as Record<string, unknown>);
|
|
370
371
|
} else {
|
|
371
372
|
printStatus(data);
|
|
372
373
|
}
|
|
@@ -245,7 +245,8 @@ describe("stopCommand stop behavior", () => {
|
|
|
245
245
|
const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: true });
|
|
246
246
|
const output = await captureStdout(() => stopCommand("my-builder", {}, deps));
|
|
247
247
|
|
|
248
|
-
expect(output).toContain(
|
|
248
|
+
expect(output).toContain("Agent stopped");
|
|
249
|
+
expect(output).toContain("my-builder");
|
|
249
250
|
expect(output).toContain(`Tmux session killed: ${session.tmuxSession}`);
|
|
250
251
|
expect(tmuxCalls.killSession).toHaveLength(1);
|
|
251
252
|
expect(tmuxCalls.killSession[0]?.name).toBe(session.tmuxSession);
|
|
@@ -313,6 +314,8 @@ describe("stopCommand --json output", () => {
|
|
|
313
314
|
const output = await captureStdout(() => stopCommand("my-builder", { json: true }, deps));
|
|
314
315
|
|
|
315
316
|
const parsed = JSON.parse(output.trim()) as Record<string, unknown>;
|
|
317
|
+
expect(parsed.success).toBe(true);
|
|
318
|
+
expect(parsed.command).toBe("stop");
|
|
316
319
|
expect(parsed.stopped).toBe(true);
|
|
317
320
|
expect(parsed.agentName).toBe("my-builder");
|
|
318
321
|
expect(parsed.sessionId).toBe(session.id);
|
|
@@ -365,19 +368,20 @@ describe("stopCommand --clean-worktree", () => {
|
|
|
365
368
|
expect(worktreeCalls.remove[0]?.options?.forceBranch).toBe(true);
|
|
366
369
|
});
|
|
367
370
|
|
|
368
|
-
test("--clean-worktree failure is non-fatal (agent still stopped, warning on
|
|
371
|
+
test("--clean-worktree failure is non-fatal (agent still stopped, warning on stdout)", async () => {
|
|
369
372
|
const session = makeAgentSession({ state: "working" });
|
|
370
373
|
saveSessionsToDb([session]);
|
|
371
374
|
|
|
372
375
|
const { deps } = makeDeps({ [session.tmuxSession]: true }, { shouldFail: true });
|
|
373
|
-
const {
|
|
376
|
+
const { stdout } = await captureStderr(() =>
|
|
374
377
|
stopCommand("my-builder", { cleanWorktree: true }, deps),
|
|
375
378
|
);
|
|
376
379
|
|
|
377
380
|
// Agent was still stopped
|
|
378
|
-
expect(stdout).toContain(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
+
expect(stdout).toContain("Agent stopped");
|
|
382
|
+
expect(stdout).toContain("my-builder");
|
|
383
|
+
// Warning written to stdout (via printWarning)
|
|
384
|
+
expect(stdout).toContain("Failed to remove worktree");
|
|
381
385
|
|
|
382
386
|
// Session is marked completed despite worktree failure
|
|
383
387
|
const { store } = openSessionStore(overstoryDir);
|
package/src/commands/stop.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { loadConfig } from "../config.ts";
|
|
13
13
|
import { AgentError, ValidationError } from "../errors.ts";
|
|
14
|
+
import { jsonOutput } from "../json.ts";
|
|
15
|
+
import { printSuccess, printWarning } from "../logging/color.ts";
|
|
14
16
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
15
17
|
import { removeWorktree } from "../worktree/manager.ts";
|
|
16
18
|
import { isSessionAlive, killSession } from "../worktree/tmux.ts";
|
|
@@ -103,24 +105,22 @@ export async function stopCommand(
|
|
|
103
105
|
worktreeRemoved = true;
|
|
104
106
|
} catch (err) {
|
|
105
107
|
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
-
|
|
108
|
+
if (!json) printWarning("Failed to remove worktree", msg);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
if (json) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
})}\n`,
|
|
121
|
-
);
|
|
113
|
+
jsonOutput("stop", {
|
|
114
|
+
stopped: true,
|
|
115
|
+
agentName,
|
|
116
|
+
sessionId: session.id,
|
|
117
|
+
capability: session.capability,
|
|
118
|
+
tmuxKilled: alive,
|
|
119
|
+
worktreeRemoved,
|
|
120
|
+
force,
|
|
121
|
+
});
|
|
122
122
|
} else {
|
|
123
|
-
|
|
123
|
+
printSuccess("Agent stopped", agentName);
|
|
124
124
|
if (alive) {
|
|
125
125
|
process.stdout.write(` Tmux session killed: ${session.tmuxSession}\n`);
|
|
126
126
|
} else {
|
|
@@ -20,6 +20,8 @@ import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
|
20
20
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
21
21
|
import { loadConfig } from "../config.ts";
|
|
22
22
|
import { AgentError, ValidationError } from "../errors.ts";
|
|
23
|
+
import { jsonOutput } from "../json.ts";
|
|
24
|
+
import { printHint, printSuccess } from "../logging/color.ts";
|
|
23
25
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
24
26
|
import { createTrackerClient, resolveBackend, trackerCliName } from "../tracker/factory.ts";
|
|
25
27
|
import type { AgentSession } from "../types.ts";
|
|
@@ -229,9 +231,9 @@ async function startSupervisor(opts: {
|
|
|
229
231
|
};
|
|
230
232
|
|
|
231
233
|
if (opts.json) {
|
|
232
|
-
|
|
234
|
+
jsonOutput("supervisor start", output);
|
|
233
235
|
} else {
|
|
234
|
-
|
|
236
|
+
printSuccess("Supervisor started", opts.name);
|
|
235
237
|
process.stdout.write(` Tmux: ${tmuxSession}\n`);
|
|
236
238
|
process.stdout.write(` Root: ${projectRoot}\n`);
|
|
237
239
|
process.stdout.write(` Task: ${opts.task}\n`);
|
|
@@ -290,9 +292,9 @@ async function stopSupervisor(opts: { name: string; json: boolean }): Promise<vo
|
|
|
290
292
|
store.updateLastActivity(opts.name);
|
|
291
293
|
|
|
292
294
|
if (opts.json) {
|
|
293
|
-
|
|
295
|
+
jsonOutput("supervisor stop", { stopped: true, sessionId: session.id });
|
|
294
296
|
} else {
|
|
295
|
-
|
|
297
|
+
printSuccess("Supervisor stopped", opts.name);
|
|
296
298
|
}
|
|
297
299
|
} finally {
|
|
298
300
|
store.close();
|
|
@@ -324,9 +326,9 @@ async function statusSupervisor(opts: { name?: string; json: boolean }): Promise
|
|
|
324
326
|
session.state === "zombie"
|
|
325
327
|
) {
|
|
326
328
|
if (opts.json) {
|
|
327
|
-
|
|
329
|
+
jsonOutput("supervisor status", { running: false });
|
|
328
330
|
} else {
|
|
329
|
-
|
|
331
|
+
printHint("Supervisor not running");
|
|
330
332
|
}
|
|
331
333
|
return;
|
|
332
334
|
}
|
|
@@ -356,7 +358,7 @@ async function statusSupervisor(opts: { name?: string; json: boolean }): Promise
|
|
|
356
358
|
};
|
|
357
359
|
|
|
358
360
|
if (opts.json) {
|
|
359
|
-
|
|
361
|
+
jsonOutput("supervisor status", status);
|
|
360
362
|
} else {
|
|
361
363
|
const stateLabel = alive ? "running" : session.state;
|
|
362
364
|
process.stdout.write(`Supervisor '${opts.name}': ${stateLabel}\n`);
|
|
@@ -376,9 +378,9 @@ async function statusSupervisor(opts: { name?: string; json: boolean }): Promise
|
|
|
376
378
|
|
|
377
379
|
if (supervisors.length === 0) {
|
|
378
380
|
if (opts.json) {
|
|
379
|
-
|
|
381
|
+
jsonOutput("supervisor status", { supervisors: [] });
|
|
380
382
|
} else {
|
|
381
|
-
|
|
383
|
+
printHint("No supervisor sessions found");
|
|
382
384
|
}
|
|
383
385
|
return;
|
|
384
386
|
}
|
|
@@ -410,7 +412,7 @@ async function statusSupervisor(opts: { name?: string; json: boolean }): Promise
|
|
|
410
412
|
);
|
|
411
413
|
|
|
412
414
|
if (opts.json) {
|
|
413
|
-
|
|
415
|
+
jsonOutput("supervisor status", { supervisors: statuses });
|
|
414
416
|
} else {
|
|
415
417
|
process.stdout.write("Supervisor sessions:\n");
|
|
416
418
|
for (const status of statuses) {
|
|
@@ -14,6 +14,7 @@ import { tmpdir } from "node:os";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { ValidationError } from "../errors.ts";
|
|
16
16
|
import { createEventStore } from "../events/store.ts";
|
|
17
|
+
import { stripAnsi } from "../logging/color.ts";
|
|
17
18
|
import { createSessionStore } from "../sessions/store.ts";
|
|
18
19
|
import type { InsertEvent } from "../types.ts";
|
|
19
20
|
import { traceCommand } from "./trace.ts";
|
|
@@ -149,8 +150,8 @@ describe("traceCommand", () => {
|
|
|
149
150
|
|
|
150
151
|
await traceCommand(["--json", "--limit", "50", "my-agent"]);
|
|
151
152
|
const out = output();
|
|
152
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
153
|
-
expect(parsed).toHaveLength(1);
|
|
153
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
154
|
+
expect(parsed.events).toHaveLength(1);
|
|
154
155
|
});
|
|
155
156
|
|
|
156
157
|
test("target is extracted correctly when flags come after", async () => {
|
|
@@ -161,8 +162,8 @@ describe("traceCommand", () => {
|
|
|
161
162
|
|
|
162
163
|
await traceCommand(["my-agent", "--json", "--limit", "50"]);
|
|
163
164
|
const out = output();
|
|
164
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
165
|
-
expect(parsed).toHaveLength(1);
|
|
165
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
166
|
+
expect(parsed.events).toHaveLength(1);
|
|
166
167
|
});
|
|
167
168
|
});
|
|
168
169
|
|
|
@@ -180,7 +181,14 @@ describe("traceCommand", () => {
|
|
|
180
181
|
await traceCommand(["builder-1", "--json"]);
|
|
181
182
|
const out = output();
|
|
182
183
|
|
|
183
|
-
|
|
184
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
185
|
+
success: boolean;
|
|
186
|
+
command: string;
|
|
187
|
+
events: unknown[];
|
|
188
|
+
};
|
|
189
|
+
expect(parsed.success).toBe(true);
|
|
190
|
+
expect(parsed.command).toBe("trace");
|
|
191
|
+
expect(parsed.events).toEqual([]);
|
|
184
192
|
});
|
|
185
193
|
});
|
|
186
194
|
|
|
@@ -198,9 +206,9 @@ describe("traceCommand", () => {
|
|
|
198
206
|
await traceCommand(["builder-1", "--json"]);
|
|
199
207
|
const out = output();
|
|
200
208
|
|
|
201
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
202
|
-
expect(parsed).toHaveLength(3);
|
|
203
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
209
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
210
|
+
expect(parsed.events).toHaveLength(3);
|
|
211
|
+
expect(Array.isArray(parsed.events)).toBe(true);
|
|
204
212
|
});
|
|
205
213
|
|
|
206
214
|
test("JSON output includes expected fields", async () => {
|
|
@@ -219,9 +227,9 @@ describe("traceCommand", () => {
|
|
|
219
227
|
await traceCommand(["builder-1", "--json"]);
|
|
220
228
|
const out = output();
|
|
221
229
|
|
|
222
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
223
|
-
expect(parsed).toHaveLength(1);
|
|
224
|
-
const event = parsed[0];
|
|
230
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
231
|
+
expect(parsed.events).toHaveLength(1);
|
|
232
|
+
const event = parsed.events[0];
|
|
225
233
|
expect(event).toBeDefined();
|
|
226
234
|
expect(event?.agentName).toBe("builder-1");
|
|
227
235
|
expect(event?.eventType).toBe("tool_start");
|
|
@@ -239,8 +247,8 @@ describe("traceCommand", () => {
|
|
|
239
247
|
await traceCommand(["builder-1", "--json"]);
|
|
240
248
|
const out = output();
|
|
241
249
|
|
|
242
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
243
|
-
expect(parsed).toEqual([]);
|
|
250
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
251
|
+
expect(parsed.events).toEqual([]);
|
|
244
252
|
});
|
|
245
253
|
});
|
|
246
254
|
|
|
@@ -256,7 +264,7 @@ describe("traceCommand", () => {
|
|
|
256
264
|
await traceCommand(["builder-1"]);
|
|
257
265
|
const out = output();
|
|
258
266
|
|
|
259
|
-
expect(out).toContain("Timeline for builder-1");
|
|
267
|
+
expect(stripAnsi(out)).toContain("Timeline for builder-1");
|
|
260
268
|
});
|
|
261
269
|
|
|
262
270
|
test("shows event count", async () => {
|
|
@@ -413,8 +421,8 @@ describe("traceCommand", () => {
|
|
|
413
421
|
await traceCommand(["builder-1", "--json", "--limit", "3"]);
|
|
414
422
|
const out = output();
|
|
415
423
|
|
|
416
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
417
|
-
expect(parsed).toHaveLength(3);
|
|
424
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
425
|
+
expect(parsed.events).toHaveLength(3);
|
|
418
426
|
});
|
|
419
427
|
|
|
420
428
|
test("default limit is 100", async () => {
|
|
@@ -428,8 +436,8 @@ describe("traceCommand", () => {
|
|
|
428
436
|
await traceCommand(["builder-1", "--json"]);
|
|
429
437
|
const out = output();
|
|
430
438
|
|
|
431
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
432
|
-
expect(parsed).toHaveLength(100);
|
|
439
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
440
|
+
expect(parsed.events).toHaveLength(100);
|
|
433
441
|
});
|
|
434
442
|
});
|
|
435
443
|
|
|
@@ -448,8 +456,8 @@ describe("traceCommand", () => {
|
|
|
448
456
|
await traceCommand(["builder-1", "--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
449
457
|
const out = output();
|
|
450
458
|
|
|
451
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
452
|
-
expect(parsed).toEqual([]);
|
|
459
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
460
|
+
expect(parsed.events).toEqual([]);
|
|
453
461
|
});
|
|
454
462
|
|
|
455
463
|
test("--since with past timestamp returns all events", async () => {
|
|
@@ -462,8 +470,8 @@ describe("traceCommand", () => {
|
|
|
462
470
|
await traceCommand(["builder-1", "--json", "--since", "2020-01-01T00:00:00Z"]);
|
|
463
471
|
const out = output();
|
|
464
472
|
|
|
465
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
466
|
-
expect(parsed).toHaveLength(2);
|
|
473
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
474
|
+
expect(parsed.events).toHaveLength(2);
|
|
467
475
|
});
|
|
468
476
|
|
|
469
477
|
test("--until with past timestamp returns no events", async () => {
|
|
@@ -475,8 +483,8 @@ describe("traceCommand", () => {
|
|
|
475
483
|
await traceCommand(["builder-1", "--json", "--until", "2000-01-01T00:00:00Z"]);
|
|
476
484
|
const out = output();
|
|
477
485
|
|
|
478
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
479
|
-
expect(parsed).toEqual([]);
|
|
486
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
487
|
+
expect(parsed.events).toEqual([]);
|
|
480
488
|
});
|
|
481
489
|
|
|
482
490
|
test("--since causes absolute timestamps in text mode", async () => {
|
|
@@ -518,9 +526,9 @@ describe("traceCommand", () => {
|
|
|
518
526
|
await traceCommand(["my-custom-agent", "--json"]);
|
|
519
527
|
const out = output();
|
|
520
528
|
|
|
521
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
522
|
-
expect(parsed).toHaveLength(1);
|
|
523
|
-
expect(parsed[0]?.agentName).toBe("my-custom-agent");
|
|
529
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
530
|
+
expect(parsed.events).toHaveLength(1);
|
|
531
|
+
expect(parsed.events[0]?.agentName).toBe("my-custom-agent");
|
|
524
532
|
});
|
|
525
533
|
|
|
526
534
|
test("task ID pattern is detected and resolved to agent name via SessionStore", async () => {
|
|
@@ -556,9 +564,9 @@ describe("traceCommand", () => {
|
|
|
556
564
|
await traceCommand(["overstory-rj1k", "--json"]);
|
|
557
565
|
const out = output();
|
|
558
566
|
|
|
559
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
560
|
-
expect(parsed).toHaveLength(1);
|
|
561
|
-
expect(parsed[0]?.agentName).toBe("builder-for-task");
|
|
567
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
568
|
+
expect(parsed.events).toHaveLength(1);
|
|
569
|
+
expect(parsed.events[0]?.agentName).toBe("builder-for-task");
|
|
562
570
|
});
|
|
563
571
|
|
|
564
572
|
test("unresolved task ID falls back to using task ID as agent name", async () => {
|
|
@@ -575,8 +583,8 @@ describe("traceCommand", () => {
|
|
|
575
583
|
await traceCommand(["myproj-abc1", "--json"]);
|
|
576
584
|
const out = output();
|
|
577
585
|
|
|
578
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
579
|
-
expect(parsed).toEqual([]);
|
|
586
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
587
|
+
expect(parsed.events).toEqual([]);
|
|
580
588
|
});
|
|
581
589
|
|
|
582
590
|
test("short agent names without task pattern are not resolved as task IDs", async () => {
|
|
@@ -589,9 +597,9 @@ describe("traceCommand", () => {
|
|
|
589
597
|
await traceCommand(["scout", "--json"]);
|
|
590
598
|
const out = output();
|
|
591
599
|
|
|
592
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
593
|
-
expect(parsed).toHaveLength(1);
|
|
594
|
-
expect(parsed[0]?.agentName).toBe("scout");
|
|
600
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
601
|
+
expect(parsed.events).toHaveLength(1);
|
|
602
|
+
expect(parsed.events[0]?.agentName).toBe("scout");
|
|
595
603
|
});
|
|
596
604
|
});
|
|
597
605
|
|
|
@@ -610,9 +618,9 @@ describe("traceCommand", () => {
|
|
|
610
618
|
await traceCommand(["builder-1", "--json"]);
|
|
611
619
|
const out = output();
|
|
612
620
|
|
|
613
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
614
|
-
expect(parsed).toHaveLength(2);
|
|
615
|
-
for (const event of parsed) {
|
|
621
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
622
|
+
expect(parsed.events).toHaveLength(2);
|
|
623
|
+
for (const event of parsed.events) {
|
|
616
624
|
expect(event.agentName).toBe("builder-1");
|
|
617
625
|
}
|
|
618
626
|
});
|
|
@@ -710,11 +718,11 @@ describe("traceCommand", () => {
|
|
|
710
718
|
await traceCommand(["builder-1", "--json"]);
|
|
711
719
|
const out = output();
|
|
712
720
|
|
|
713
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
714
|
-
expect(parsed).toHaveLength(3);
|
|
715
|
-
expect(parsed[0]?.eventType).toBe("session_start");
|
|
716
|
-
expect(parsed[1]?.eventType).toBe("tool_start");
|
|
717
|
-
expect(parsed[2]?.eventType).toBe("session_end");
|
|
721
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
722
|
+
expect(parsed.events).toHaveLength(3);
|
|
723
|
+
expect(parsed.events[0]?.eventType).toBe("session_start");
|
|
724
|
+
expect(parsed.events[1]?.eventType).toBe("tool_start");
|
|
725
|
+
expect(parsed.events[2]?.eventType).toBe("session_end");
|
|
718
726
|
});
|
|
719
727
|
|
|
720
728
|
test("handles event with all null optional fields", async () => {
|
|
@@ -738,7 +746,7 @@ describe("traceCommand", () => {
|
|
|
738
746
|
await traceCommand(["builder-1"]);
|
|
739
747
|
const out = output();
|
|
740
748
|
|
|
741
|
-
expect(out).toContain("Timeline for builder-1");
|
|
749
|
+
expect(stripAnsi(out)).toContain("Timeline for builder-1");
|
|
742
750
|
expect(out).toContain("1 event");
|
|
743
751
|
});
|
|
744
752
|
});
|
package/src/commands/trace.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { Command } from "commander";
|
|
|
10
10
|
import { loadConfig } from "../config.ts";
|
|
11
11
|
import { ValidationError } from "../errors.ts";
|
|
12
12
|
import { createEventStore } from "../events/store.ts";
|
|
13
|
+
import { jsonOutput } from "../json.ts";
|
|
13
14
|
import type { ColorFn } from "../logging/color.ts";
|
|
14
|
-
import { color } from "../logging/color.ts";
|
|
15
|
+
import { accent, color } from "../logging/color.ts";
|
|
15
16
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
16
17
|
import type { EventType, StoredEvent } from "../types.ts";
|
|
17
18
|
|
|
@@ -129,7 +130,7 @@ function buildEventDetail(event: StoredEvent): string {
|
|
|
129
130
|
function printTimeline(events: StoredEvent[], agentName: string, useAbsoluteTime: boolean): void {
|
|
130
131
|
const w = process.stdout.write.bind(process.stdout);
|
|
131
132
|
|
|
132
|
-
w(`${color.bold(`Timeline for ${agentName}`)}\n`);
|
|
133
|
+
w(`${color.bold(`Timeline for ${accent(agentName)}`)}\n`);
|
|
133
134
|
w(`${"=".repeat(70)}\n`);
|
|
134
135
|
|
|
135
136
|
if (events.length === 0) {
|
|
@@ -243,7 +244,7 @@ async function executeTrace(target: string, opts: TraceOpts): Promise<void> {
|
|
|
243
244
|
const eventsFile = Bun.file(eventsDbPath);
|
|
244
245
|
if (!(await eventsFile.exists())) {
|
|
245
246
|
if (json) {
|
|
246
|
-
|
|
247
|
+
jsonOutput("trace", { events: [] });
|
|
247
248
|
} else {
|
|
248
249
|
process.stdout.write("No events data yet.\n");
|
|
249
250
|
}
|
|
@@ -260,7 +261,7 @@ async function executeTrace(target: string, opts: TraceOpts): Promise<void> {
|
|
|
260
261
|
});
|
|
261
262
|
|
|
262
263
|
if (json) {
|
|
263
|
-
|
|
264
|
+
jsonOutput("trace", { events });
|
|
264
265
|
return;
|
|
265
266
|
}
|
|
266
267
|
|
package/src/commands/watch.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { loadConfig } from "../config.ts";
|
|
12
12
|
import { OverstoryError } from "../errors.ts";
|
|
13
|
+
import { printError, printHint, printSuccess } from "../logging/color.ts";
|
|
13
14
|
import type { HealthCheck } from "../types.ts";
|
|
14
15
|
import { startDaemon } from "../watchdog/daemon.ts";
|
|
15
16
|
import { isProcessRunning } from "../watchdog/health.ts";
|
|
@@ -130,9 +131,8 @@ async function runWatch(opts: { interval?: string; background?: boolean }): Prom
|
|
|
130
131
|
// Check if a watchdog is already running
|
|
131
132
|
const existingPid = await readPidFile(pidFilePath);
|
|
132
133
|
if (existingPid !== null && isProcessRunning(existingPid)) {
|
|
133
|
-
|
|
134
|
-
`
|
|
135
|
-
`Kill it first or remove ${pidFilePath}\n`,
|
|
134
|
+
printError(
|
|
135
|
+
`Watchdog already running (PID: ${existingPid}). Kill it first or remove ${pidFilePath}`,
|
|
136
136
|
);
|
|
137
137
|
process.exitCode = 1;
|
|
138
138
|
return;
|
|
@@ -168,16 +168,14 @@ async function runWatch(opts: { interval?: string; background?: boolean }): Prom
|
|
|
168
168
|
// Write PID file for later cleanup
|
|
169
169
|
await writePidFile(pidFilePath, childPid);
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
);
|
|
174
|
-
process.stdout.write(`PID file: ${pidFilePath}\n`);
|
|
171
|
+
printSuccess("Watchdog started in background", `PID: ${childPid}, interval: ${intervalMs}ms`);
|
|
172
|
+
printHint(`PID file: ${pidFilePath}`);
|
|
175
173
|
return;
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
// Foreground mode: show real-time health checks
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
printSuccess("Watchdog running", `interval: ${intervalMs}ms`);
|
|
178
|
+
printHint("Press Ctrl+C to stop.");
|
|
181
179
|
|
|
182
180
|
// Write PID file so `--background` check and external tools can find us
|
|
183
181
|
await writePidFile(pidFilePath, process.pid);
|
|
@@ -200,7 +198,7 @@ async function runWatch(opts: { interval?: string; background?: boolean }): Prom
|
|
|
200
198
|
stop();
|
|
201
199
|
// Clean up PID file on graceful shutdown
|
|
202
200
|
removePidFile(pidFilePath).finally(() => {
|
|
203
|
-
|
|
201
|
+
printSuccess("Watchdog stopped.");
|
|
204
202
|
process.exit(0);
|
|
205
203
|
});
|
|
206
204
|
});
|
|
@@ -131,7 +131,7 @@ describe("worktreeCommand", () => {
|
|
|
131
131
|
await worktreeCommand(["list"]);
|
|
132
132
|
const out = output();
|
|
133
133
|
|
|
134
|
-
expect(out).
|
|
134
|
+
expect(out).toContain("No agent worktrees found");
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
test("with overstory worktrees lists them with agent info", async () => {
|
|
@@ -220,21 +220,27 @@ describe("worktreeCommand", () => {
|
|
|
220
220
|
await worktreeCommand(["list", "--json"]);
|
|
221
221
|
const out = output();
|
|
222
222
|
|
|
223
|
-
const parsed = JSON.parse(out.trim()) as
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
expect(parsed
|
|
237
|
-
expect(parsed
|
|
223
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
224
|
+
success: boolean;
|
|
225
|
+
command: string;
|
|
226
|
+
worktrees: Array<{
|
|
227
|
+
path: string;
|
|
228
|
+
branch: string;
|
|
229
|
+
head: string;
|
|
230
|
+
agentName: string | null;
|
|
231
|
+
state: string | null;
|
|
232
|
+
taskId: string | null;
|
|
233
|
+
}>;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
expect(parsed.success).toBe(true);
|
|
237
|
+
expect(parsed.command).toBe("worktree list");
|
|
238
|
+
expect(parsed.worktrees).toHaveLength(1);
|
|
239
|
+
expect(parsed.worktrees[0]?.path).toBe(worktreePath);
|
|
240
|
+
expect(parsed.worktrees[0]?.branch).toBe("overstory/test-agent/task-1");
|
|
241
|
+
expect(parsed.worktrees[0]?.agentName).toBe("test-agent");
|
|
242
|
+
expect(parsed.worktrees[0]?.state).toBe("working");
|
|
243
|
+
expect(parsed.worktrees[0]?.taskId).toBe("task-1");
|
|
238
244
|
});
|
|
239
245
|
|
|
240
246
|
test("worktrees without sessions show unknown state", async () => {
|
|
@@ -266,7 +272,7 @@ describe("worktreeCommand", () => {
|
|
|
266
272
|
await worktreeCommand(["clean"]);
|
|
267
273
|
const out = output();
|
|
268
274
|
|
|
269
|
-
expect(out).
|
|
275
|
+
expect(out).toContain("No worktrees to clean");
|
|
270
276
|
});
|
|
271
277
|
|
|
272
278
|
test("with completed agent worktree removes it and reports count", async () => {
|
|
@@ -308,7 +314,8 @@ describe("worktreeCommand", () => {
|
|
|
308
314
|
await worktreeCommand(["clean"]);
|
|
309
315
|
const out = output();
|
|
310
316
|
|
|
311
|
-
expect(out).toContain("Removed
|
|
317
|
+
expect(out).toContain("Removed");
|
|
318
|
+
expect(out).toContain("overstory/completed-agent/task-done");
|
|
312
319
|
expect(out).toContain("Cleaned 1 worktree");
|
|
313
320
|
|
|
314
321
|
// Verify the worktree directory is gone
|
|
@@ -455,7 +462,7 @@ describe("worktreeCommand", () => {
|
|
|
455
462
|
const out = output();
|
|
456
463
|
|
|
457
464
|
// Stalled agents should not be cleaned by default (only completed/zombie are cleaned)
|
|
458
|
-
expect(out).
|
|
465
|
+
expect(out).toContain("No worktrees to clean");
|
|
459
466
|
});
|
|
460
467
|
|
|
461
468
|
test("--completed flag only cleans completed agents", async () => {
|
|
@@ -704,7 +711,7 @@ describe("worktreeCommand", () => {
|
|
|
704
711
|
|
|
705
712
|
// Worktree should be removed
|
|
706
713
|
expect(existsSync(wtPath)).toBe(false);
|
|
707
|
-
expect(out).toContain("
|
|
714
|
+
expect(out).toContain("overstory/unmerged-agent/task-force");
|
|
708
715
|
});
|
|
709
716
|
|
|
710
717
|
test("without --force, removes worktrees whose branches ARE merged", async () => {
|