@os-eco/overstory-cli 0.6.8 → 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 +4 -2
- 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 +2 -1
- package/src/commands/completions.ts +3 -4
- package/src/commands/coordinator.test.ts +8 -0
- package/src/commands/coordinator.ts +11 -8
- 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 +23 -14
- package/src/commands/hooks.ts +2 -1
- package/src/commands/init.test.ts +104 -0
- package/src/commands/init.ts +11 -7
- 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 +11 -10
- package/src/commands/mail.ts +11 -12
- package/src/commands/merge.ts +11 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +5 -4
- package/src/commands/nudge.ts +2 -3
- 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 +17 -20
- package/src/commands/sling.ts +2 -1
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +2 -0
- package/src/commands/stop.ts +10 -11
- package/src/commands/supervisor.ts +7 -6
- 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 +21 -15
- package/src/commands/worktree.ts +10 -4
- package/src/index.ts +3 -1
package/src/commands/metrics.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { loadConfig } from "../config.ts";
|
|
11
|
+
import { jsonOutput } from "../json.ts";
|
|
11
12
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
12
13
|
|
|
13
14
|
interface MetricsOpts {
|
|
@@ -41,7 +42,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
|
41
42
|
const dbFile = Bun.file(dbPath);
|
|
42
43
|
if (!(await dbFile.exists())) {
|
|
43
44
|
if (json) {
|
|
44
|
-
|
|
45
|
+
jsonOutput("metrics", { sessions: [] });
|
|
45
46
|
} else {
|
|
46
47
|
process.stdout.write("No metrics data yet.\n");
|
|
47
48
|
}
|
|
@@ -54,7 +55,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
|
54
55
|
const sessions = store.getRecentSessions(limit);
|
|
55
56
|
|
|
56
57
|
if (json) {
|
|
57
|
-
|
|
58
|
+
jsonOutput("metrics", { sessions } as Record<string, unknown>);
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
|
package/src/commands/monitor.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
|
21
21
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
22
22
|
import { loadConfig } from "../config.ts";
|
|
23
23
|
import { AgentError, ValidationError } from "../errors.ts";
|
|
24
|
+
import { jsonOutput } from "../json.ts";
|
|
24
25
|
import { printHint, printSuccess } from "../logging/color.ts";
|
|
25
26
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
26
27
|
import type { AgentSession } from "../types.ts";
|
|
@@ -190,7 +191,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
190
191
|
};
|
|
191
192
|
|
|
192
193
|
if (json) {
|
|
193
|
-
|
|
194
|
+
jsonOutput("monitor start", output);
|
|
194
195
|
} else {
|
|
195
196
|
printSuccess("Monitor started");
|
|
196
197
|
process.stdout.write(` Tmux: ${tmuxSession}\n`);
|
|
@@ -248,7 +249,7 @@ async function stopMonitor(opts: { json: boolean }): Promise<void> {
|
|
|
248
249
|
store.updateLastActivity(MONITOR_NAME);
|
|
249
250
|
|
|
250
251
|
if (json) {
|
|
251
|
-
|
|
252
|
+
jsonOutput("monitor stop", { stopped: true, sessionId: session.id });
|
|
252
253
|
} else {
|
|
253
254
|
printSuccess("Monitor stopped", session.id);
|
|
254
255
|
}
|
|
@@ -280,7 +281,7 @@ async function statusMonitor(opts: { json: boolean }): Promise<void> {
|
|
|
280
281
|
session.state === "zombie"
|
|
281
282
|
) {
|
|
282
283
|
if (json) {
|
|
283
|
-
|
|
284
|
+
jsonOutput("monitor status", { running: false });
|
|
284
285
|
} else {
|
|
285
286
|
printHint("Monitor is not running");
|
|
286
287
|
}
|
|
@@ -307,7 +308,7 @@ async function statusMonitor(opts: { json: boolean }): Promise<void> {
|
|
|
307
308
|
};
|
|
308
309
|
|
|
309
310
|
if (json) {
|
|
310
|
-
|
|
311
|
+
jsonOutput("monitor status", status);
|
|
311
312
|
} else {
|
|
312
313
|
const stateLabel = alive ? "running" : session.state;
|
|
313
314
|
process.stdout.write(`Monitor: ${stateLabel}\n`);
|
package/src/commands/nudge.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { join } from "node:path";
|
|
|
13
13
|
import { Command } from "commander";
|
|
14
14
|
import { AgentError } from "../errors.ts";
|
|
15
15
|
import { createEventStore } from "../events/store.ts";
|
|
16
|
+
import { jsonOutput } from "../json.ts";
|
|
16
17
|
import { printSuccess } from "../logging/color.ts";
|
|
17
18
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
18
19
|
import type { EventStore } from "../types.ts";
|
|
@@ -310,9 +311,7 @@ export async function nudgeCommand(args: string[]): Promise<void> {
|
|
|
310
311
|
const result = await nudgeAgent(projectRoot, agentName, message, opts.force ?? false);
|
|
311
312
|
|
|
312
313
|
if (opts.json) {
|
|
313
|
-
|
|
314
|
-
`${JSON.stringify({ agentName, delivered: result.delivered, reason: result.reason })}\n`,
|
|
315
|
-
);
|
|
314
|
+
jsonOutput("nudge", { agentName, delivered: result.delivered, reason: result.reason });
|
|
316
315
|
} else if (result.delivered) {
|
|
317
316
|
printSuccess("Nudge delivered", agentName);
|
|
318
317
|
} else {
|
|
@@ -62,10 +62,6 @@ describe("primeCommand", () => {
|
|
|
62
62
|
return chunks.join("");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function stderr(): string {
|
|
66
|
-
return stderrChunks.join("");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
65
|
describe("Orchestrator priming (no --agent flag)", () => {
|
|
70
66
|
test("default prime outputs project context", async () => {
|
|
71
67
|
await primeCommand({});
|
|
@@ -113,12 +109,11 @@ describe("primeCommand", () => {
|
|
|
113
109
|
test("unknown agent outputs basic context and warns", async () => {
|
|
114
110
|
await primeCommand({ agent: "unknown-agent" });
|
|
115
111
|
const out = output();
|
|
116
|
-
const err = stderr();
|
|
117
112
|
|
|
118
113
|
expect(out).toContain("# Agent Context: unknown-agent");
|
|
119
114
|
expect(out).toContain("## Identity");
|
|
120
115
|
expect(out).toContain("New agent - no prior sessions");
|
|
121
|
-
expect(
|
|
116
|
+
expect(out).toContain('agent "unknown-agent" not found');
|
|
122
117
|
});
|
|
123
118
|
|
|
124
119
|
test("agent with identity.yaml shows identity details", async () => {
|
package/src/commands/prime.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { loadCheckpoint } from "../agents/checkpoint.ts";
|
|
|
12
12
|
import { loadIdentity } from "../agents/identity.ts";
|
|
13
13
|
import { createManifestLoader } from "../agents/manifest.ts";
|
|
14
14
|
import { loadConfig } from "../config.ts";
|
|
15
|
+
import { printWarning } from "../logging/color.ts";
|
|
15
16
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
16
17
|
import { createMulchClient } from "../mulch/client.ts";
|
|
17
18
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
@@ -211,9 +212,7 @@ async function outputAgentContext(
|
|
|
211
212
|
|
|
212
213
|
// Warn if agent is completely unknown (no session and no identity)
|
|
213
214
|
if (!sessionExists && identity === null) {
|
|
214
|
-
|
|
215
|
-
`Warning: agent "${agentName}" not found in sessions or identity store.\n`,
|
|
216
|
-
);
|
|
215
|
+
printWarning(`agent "${agentName}" not found in sessions or identity store.`);
|
|
217
216
|
}
|
|
218
217
|
|
|
219
218
|
sections.push("\n## Identity");
|
|
@@ -133,7 +133,14 @@ describe("replayCommand", () => {
|
|
|
133
133
|
await replayCommand(["--json"]);
|
|
134
134
|
const out = output();
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
137
|
+
success: boolean;
|
|
138
|
+
command: string;
|
|
139
|
+
events: unknown[];
|
|
140
|
+
};
|
|
141
|
+
expect(parsed.success).toBe(true);
|
|
142
|
+
expect(parsed.command).toBe("replay");
|
|
143
|
+
expect(parsed.events).toEqual([]);
|
|
137
144
|
});
|
|
138
145
|
});
|
|
139
146
|
|
|
@@ -151,9 +158,9 @@ describe("replayCommand", () => {
|
|
|
151
158
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
152
159
|
const out = output();
|
|
153
160
|
|
|
154
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
155
|
-
expect(parsed).toHaveLength(3);
|
|
156
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
161
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
162
|
+
expect(parsed.events).toHaveLength(3);
|
|
163
|
+
expect(Array.isArray(parsed.events)).toBe(true);
|
|
157
164
|
});
|
|
158
165
|
|
|
159
166
|
test("JSON output includes expected fields", async () => {
|
|
@@ -172,9 +179,9 @@ describe("replayCommand", () => {
|
|
|
172
179
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
173
180
|
const out = output();
|
|
174
181
|
|
|
175
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
176
|
-
expect(parsed).toHaveLength(1);
|
|
177
|
-
const event = parsed[0];
|
|
182
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
183
|
+
expect(parsed.events).toHaveLength(1);
|
|
184
|
+
const event = parsed.events[0];
|
|
178
185
|
expect(event).toBeDefined();
|
|
179
186
|
expect(event?.agentName).toBe("builder-1");
|
|
180
187
|
expect(event?.eventType).toBe("tool_start");
|
|
@@ -192,8 +199,8 @@ describe("replayCommand", () => {
|
|
|
192
199
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
193
200
|
const out = output();
|
|
194
201
|
|
|
195
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
196
|
-
expect(parsed).toEqual([]);
|
|
202
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
203
|
+
expect(parsed.events).toEqual([]);
|
|
197
204
|
});
|
|
198
205
|
});
|
|
199
206
|
|
|
@@ -332,9 +339,9 @@ describe("replayCommand", () => {
|
|
|
332
339
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
333
340
|
const out = output();
|
|
334
341
|
|
|
335
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
336
|
-
expect(parsed).toHaveLength(2);
|
|
337
|
-
for (const event of parsed) {
|
|
342
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
343
|
+
expect(parsed.events).toHaveLength(2);
|
|
344
|
+
for (const event of parsed.events) {
|
|
338
345
|
expect(event.runId).toBe("run-001");
|
|
339
346
|
}
|
|
340
347
|
});
|
|
@@ -349,8 +356,8 @@ describe("replayCommand", () => {
|
|
|
349
356
|
await replayCommand(["--run", "run-001", "--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
350
357
|
const out = output();
|
|
351
358
|
|
|
352
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
353
|
-
expect(parsed).toEqual([]);
|
|
359
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
360
|
+
expect(parsed.events).toEqual([]);
|
|
354
361
|
});
|
|
355
362
|
});
|
|
356
363
|
|
|
@@ -368,9 +375,9 @@ describe("replayCommand", () => {
|
|
|
368
375
|
await replayCommand(["--agent", "builder-1", "--json"]);
|
|
369
376
|
const out = output();
|
|
370
377
|
|
|
371
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
372
|
-
expect(parsed).toHaveLength(2);
|
|
373
|
-
for (const event of parsed) {
|
|
378
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
379
|
+
expect(parsed.events).toHaveLength(2);
|
|
380
|
+
for (const event of parsed.events) {
|
|
374
381
|
expect(event.agentName).toBe("builder-1");
|
|
375
382
|
}
|
|
376
383
|
});
|
|
@@ -387,9 +394,9 @@ describe("replayCommand", () => {
|
|
|
387
394
|
await replayCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
|
|
388
395
|
const out = output();
|
|
389
396
|
|
|
390
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
391
|
-
expect(parsed).toHaveLength(3);
|
|
392
|
-
const agents = new Set(parsed.map((e) => e.agentName));
|
|
397
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
398
|
+
expect(parsed.events).toHaveLength(3);
|
|
399
|
+
const agents = new Set(parsed.events.map((e) => e.agentName));
|
|
393
400
|
expect(agents.has("builder-1")).toBe(true);
|
|
394
401
|
expect(agents.has("scout-1")).toBe(true);
|
|
395
402
|
expect(agents.has("reviewer-1")).toBe(false);
|
|
@@ -407,12 +414,12 @@ describe("replayCommand", () => {
|
|
|
407
414
|
await replayCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
|
|
408
415
|
const out = output();
|
|
409
416
|
|
|
410
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
411
|
-
expect(parsed).toHaveLength(3);
|
|
417
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
418
|
+
expect(parsed.events).toHaveLength(3);
|
|
412
419
|
// They should be in chronological order
|
|
413
|
-
for (let i = 1; i < parsed.length; i++) {
|
|
414
|
-
const prev = parsed[i - 1];
|
|
415
|
-
const curr = parsed[i];
|
|
420
|
+
for (let i = 1; i < parsed.events.length; i++) {
|
|
421
|
+
const prev = parsed.events[i - 1];
|
|
422
|
+
const curr = parsed.events[i];
|
|
416
423
|
if (prev && curr) {
|
|
417
424
|
expect(
|
|
418
425
|
(prev.createdAt as string).localeCompare(curr.createdAt as string),
|
|
@@ -430,8 +437,8 @@ describe("replayCommand", () => {
|
|
|
430
437
|
await replayCommand(["--agent", "nonexistent", "--json"]);
|
|
431
438
|
const out = output();
|
|
432
439
|
|
|
433
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
434
|
-
expect(parsed).toEqual([]);
|
|
440
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
441
|
+
expect(parsed.events).toEqual([]);
|
|
435
442
|
});
|
|
436
443
|
});
|
|
437
444
|
|
|
@@ -451,9 +458,9 @@ describe("replayCommand", () => {
|
|
|
451
458
|
await replayCommand(["--json"]);
|
|
452
459
|
const out = output();
|
|
453
460
|
|
|
454
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
455
|
-
expect(parsed).toHaveLength(1);
|
|
456
|
-
expect(parsed[0]?.runId).toBe("run-from-file");
|
|
461
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
462
|
+
expect(parsed.events).toHaveLength(1);
|
|
463
|
+
expect(parsed.events[0]?.runId).toBe("run-from-file");
|
|
457
464
|
});
|
|
458
465
|
|
|
459
466
|
test("falls back to 24h timeline when no current-run.txt", async () => {
|
|
@@ -467,8 +474,8 @@ describe("replayCommand", () => {
|
|
|
467
474
|
await replayCommand(["--json"]);
|
|
468
475
|
const out = output();
|
|
469
476
|
|
|
470
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
471
|
-
expect(parsed).toHaveLength(2);
|
|
477
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
478
|
+
expect(parsed.events).toHaveLength(2);
|
|
472
479
|
});
|
|
473
480
|
|
|
474
481
|
test("falls back to timeline when current-run.txt is empty", async () => {
|
|
@@ -482,8 +489,8 @@ describe("replayCommand", () => {
|
|
|
482
489
|
await replayCommand(["--json"]);
|
|
483
490
|
const out = output();
|
|
484
491
|
|
|
485
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
486
|
-
expect(parsed).toHaveLength(1);
|
|
492
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
493
|
+
expect(parsed.events).toHaveLength(1);
|
|
487
494
|
});
|
|
488
495
|
});
|
|
489
496
|
|
|
@@ -501,8 +508,8 @@ describe("replayCommand", () => {
|
|
|
501
508
|
await replayCommand(["--run", "run-001", "--json", "--limit", "3"]);
|
|
502
509
|
const out = output();
|
|
503
510
|
|
|
504
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
505
|
-
expect(parsed).toHaveLength(3);
|
|
511
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
512
|
+
expect(parsed.events).toHaveLength(3);
|
|
506
513
|
});
|
|
507
514
|
|
|
508
515
|
test("default limit is 200", async () => {
|
|
@@ -516,8 +523,8 @@ describe("replayCommand", () => {
|
|
|
516
523
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
517
524
|
const out = output();
|
|
518
525
|
|
|
519
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
520
|
-
expect(parsed).toHaveLength(200);
|
|
526
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
527
|
+
expect(parsed.events).toHaveLength(200);
|
|
521
528
|
});
|
|
522
529
|
|
|
523
530
|
test("--limit applies to merged --agent queries", async () => {
|
|
@@ -532,8 +539,8 @@ describe("replayCommand", () => {
|
|
|
532
539
|
await replayCommand(["--agent", "builder-1", "--agent", "scout-1", "--json", "--limit", "4"]);
|
|
533
540
|
const out = output();
|
|
534
541
|
|
|
535
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
536
|
-
expect(parsed).toHaveLength(4);
|
|
542
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
543
|
+
expect(parsed.events).toHaveLength(4);
|
|
537
544
|
});
|
|
538
545
|
});
|
|
539
546
|
|
|
@@ -550,8 +557,8 @@ describe("replayCommand", () => {
|
|
|
550
557
|
await replayCommand(["--run", "run-001", "--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
551
558
|
const out = output();
|
|
552
559
|
|
|
553
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
554
|
-
expect(parsed).toEqual([]);
|
|
560
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
561
|
+
expect(parsed.events).toEqual([]);
|
|
555
562
|
});
|
|
556
563
|
|
|
557
564
|
test("--since with past timestamp returns all events", async () => {
|
|
@@ -564,8 +571,8 @@ describe("replayCommand", () => {
|
|
|
564
571
|
await replayCommand(["--run", "run-001", "--json", "--since", "2020-01-01T00:00:00Z"]);
|
|
565
572
|
const out = output();
|
|
566
573
|
|
|
567
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
568
|
-
expect(parsed).toHaveLength(2);
|
|
574
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
575
|
+
expect(parsed.events).toHaveLength(2);
|
|
569
576
|
});
|
|
570
577
|
|
|
571
578
|
test("--until with past timestamp returns no events", async () => {
|
|
@@ -577,8 +584,8 @@ describe("replayCommand", () => {
|
|
|
577
584
|
await replayCommand(["--run", "run-001", "--json", "--until", "2000-01-01T00:00:00Z"]);
|
|
578
585
|
const out = output();
|
|
579
586
|
|
|
580
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
581
|
-
expect(parsed).toEqual([]);
|
|
587
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
588
|
+
expect(parsed.events).toEqual([]);
|
|
582
589
|
});
|
|
583
590
|
|
|
584
591
|
test("--since causes absolute timestamps in text mode", async () => {
|
|
@@ -611,12 +618,12 @@ describe("replayCommand", () => {
|
|
|
611
618
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
612
619
|
const out = output();
|
|
613
620
|
|
|
614
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
615
|
-
expect(parsed).toHaveLength(4);
|
|
621
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
622
|
+
expect(parsed.events).toHaveLength(4);
|
|
616
623
|
// Verify chronological order
|
|
617
|
-
for (let i = 1; i < parsed.length; i++) {
|
|
618
|
-
const prev = parsed[i - 1];
|
|
619
|
-
const curr = parsed[i];
|
|
624
|
+
for (let i = 1; i < parsed.events.length; i++) {
|
|
625
|
+
const prev = parsed.events[i - 1];
|
|
626
|
+
const curr = parsed.events[i];
|
|
620
627
|
if (prev && curr) {
|
|
621
628
|
expect(
|
|
622
629
|
(prev.createdAt as string).localeCompare(curr.createdAt as string),
|
|
@@ -624,10 +631,10 @@ describe("replayCommand", () => {
|
|
|
624
631
|
}
|
|
625
632
|
}
|
|
626
633
|
// Verify interleaving: agents should alternate
|
|
627
|
-
expect(parsed[0]?.agentName).toBe("builder-1");
|
|
628
|
-
expect(parsed[1]?.agentName).toBe("scout-1");
|
|
629
|
-
expect(parsed[2]?.agentName).toBe("builder-1");
|
|
630
|
-
expect(parsed[3]?.agentName).toBe("scout-1");
|
|
634
|
+
expect(parsed.events[0]?.agentName).toBe("builder-1");
|
|
635
|
+
expect(parsed.events[1]?.agentName).toBe("scout-1");
|
|
636
|
+
expect(parsed.events[2]?.agentName).toBe("builder-1");
|
|
637
|
+
expect(parsed.events[3]?.agentName).toBe("scout-1");
|
|
631
638
|
});
|
|
632
639
|
});
|
|
633
640
|
|
package/src/commands/replay.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Command } from "commander";
|
|
|
11
11
|
import { loadConfig } from "../config.ts";
|
|
12
12
|
import { ValidationError } from "../errors.ts";
|
|
13
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
|
+
import { jsonOutput } from "../json.ts";
|
|
14
15
|
import type { ColorFn } from "../logging/color.ts";
|
|
15
16
|
import { color } from "../logging/color.ts";
|
|
16
17
|
import type { EventType, StoredEvent } from "../types.ts";
|
|
@@ -246,7 +247,7 @@ async function executeReplay(opts: ReplayOpts): Promise<void> {
|
|
|
246
247
|
const eventsFile = Bun.file(eventsDbPath);
|
|
247
248
|
if (!(await eventsFile.exists())) {
|
|
248
249
|
if (json) {
|
|
249
|
-
|
|
250
|
+
jsonOutput("replay", { events: [] });
|
|
250
251
|
} else {
|
|
251
252
|
process.stdout.write("No events data yet.\n");
|
|
252
253
|
}
|
|
@@ -305,7 +306,7 @@ async function executeReplay(opts: ReplayOpts): Promise<void> {
|
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
if (json) {
|
|
308
|
-
|
|
309
|
+
jsonOutput("replay", { events });
|
|
309
310
|
return;
|
|
310
311
|
}
|
|
311
312
|
|
package/src/commands/run.ts
CHANGED
|
@@ -15,7 +15,8 @@ import { join } from "node:path";
|
|
|
15
15
|
import { Command, CommanderError } from "commander";
|
|
16
16
|
import { loadConfig } from "../config.ts";
|
|
17
17
|
import { ValidationError } from "../errors.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { jsonError, jsonOutput } from "../json.ts";
|
|
19
|
+
import { accent, printError, printHint, printSuccess } from "../logging/color.ts";
|
|
19
20
|
import { createRunStore, createSessionStore } from "../sessions/store.ts";
|
|
20
21
|
import type { AgentSession, Run } from "../types.ts";
|
|
21
22
|
|
|
@@ -83,7 +84,7 @@ async function showCurrentRun(overstoryDir: string, json: boolean): Promise<void
|
|
|
83
84
|
const runId = await readCurrentRunId(overstoryDir);
|
|
84
85
|
if (!runId) {
|
|
85
86
|
if (json) {
|
|
86
|
-
|
|
87
|
+
jsonOutput("run", { run: null, message: "No active run" });
|
|
87
88
|
} else {
|
|
88
89
|
printHint("No active run");
|
|
89
90
|
}
|
|
@@ -96,23 +97,21 @@ async function showCurrentRun(overstoryDir: string, json: boolean): Promise<void
|
|
|
96
97
|
const run = runStore.getRun(runId);
|
|
97
98
|
if (!run) {
|
|
98
99
|
if (json) {
|
|
99
|
-
|
|
100
|
-
`${JSON.stringify({ run: null, message: `Run ${runId} not found in store` })}\n`,
|
|
101
|
-
);
|
|
100
|
+
jsonOutput("run", { run: null, message: `Run ${runId} not found in store` });
|
|
102
101
|
} else {
|
|
103
|
-
process.stdout.write(`Run ${runId} not found in store\n`);
|
|
102
|
+
process.stdout.write(`Run ${accent(runId)} not found in store\n`);
|
|
104
103
|
}
|
|
105
104
|
return;
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
if (json) {
|
|
109
|
-
|
|
108
|
+
jsonOutput("run", { run, duration: runDuration(run) });
|
|
110
109
|
return;
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
process.stdout.write("Current Run\n");
|
|
114
113
|
process.stdout.write(`${"=".repeat(50)}\n`);
|
|
115
|
-
process.stdout.write(` ID: ${run.id}\n`);
|
|
114
|
+
process.stdout.write(` ID: ${accent(run.id)}\n`);
|
|
116
115
|
process.stdout.write(` Status: ${run.status}\n`);
|
|
117
116
|
process.stdout.write(` Started: ${run.startedAt}\n`);
|
|
118
117
|
process.stdout.write(` Agents: ${run.agentCount}\n`);
|
|
@@ -130,7 +129,7 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
|
|
|
130
129
|
const dbFile = Bun.file(dbPath);
|
|
131
130
|
if (!(await dbFile.exists())) {
|
|
132
131
|
if (json) {
|
|
133
|
-
|
|
132
|
+
jsonOutput("run list", { runs: [] });
|
|
134
133
|
} else {
|
|
135
134
|
printHint("No runs recorded yet");
|
|
136
135
|
}
|
|
@@ -143,7 +142,7 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
|
|
|
143
142
|
|
|
144
143
|
if (json) {
|
|
145
144
|
const runsWithDuration = runs.map((r) => ({ ...r, duration: runDuration(r) }));
|
|
146
|
-
|
|
145
|
+
jsonOutput("run list", { runs: runsWithDuration });
|
|
147
146
|
return;
|
|
148
147
|
}
|
|
149
148
|
|
|
@@ -160,7 +159,7 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
|
|
|
160
159
|
process.stdout.write(`${"-".repeat(70)}\n`);
|
|
161
160
|
|
|
162
161
|
for (const run of runs) {
|
|
163
|
-
const id = run.id.length > 35 ? `${run.id.slice(0, 32)}...` : run.id.padEnd(36);
|
|
162
|
+
const id = accent(run.id.length > 35 ? `${run.id.slice(0, 32)}...` : run.id.padEnd(36));
|
|
164
163
|
const status = run.status.padEnd(10);
|
|
165
164
|
const agents = String(run.agentCount).padEnd(7);
|
|
166
165
|
const duration = runDuration(run);
|
|
@@ -178,7 +177,7 @@ async function completeCurrentRun(overstoryDir: string, json: boolean): Promise<
|
|
|
178
177
|
const runId = await readCurrentRunId(overstoryDir);
|
|
179
178
|
if (!runId) {
|
|
180
179
|
if (json) {
|
|
181
|
-
|
|
180
|
+
jsonError("run complete", "No active run to complete");
|
|
182
181
|
} else {
|
|
183
182
|
printError("No active run to complete");
|
|
184
183
|
}
|
|
@@ -203,7 +202,7 @@ async function completeCurrentRun(overstoryDir: string, json: boolean): Promise<
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
if (json) {
|
|
206
|
-
|
|
205
|
+
jsonOutput("run complete", { runId, status: "completed" });
|
|
207
206
|
} else {
|
|
208
207
|
printSuccess("Run completed", runId);
|
|
209
208
|
}
|
|
@@ -217,7 +216,7 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
217
216
|
const dbFile = Bun.file(dbPath);
|
|
218
217
|
if (!(await dbFile.exists())) {
|
|
219
218
|
if (json) {
|
|
220
|
-
|
|
219
|
+
jsonError("run show", `Run ${runId} not found`);
|
|
221
220
|
} else {
|
|
222
221
|
printError("Run not found", runId);
|
|
223
222
|
}
|
|
@@ -231,9 +230,7 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
231
230
|
const run = runStore.getRun(runId);
|
|
232
231
|
if (!run) {
|
|
233
232
|
if (json) {
|
|
234
|
-
|
|
235
|
-
`${JSON.stringify({ run: null, message: `Run ${runId} not found` })}\n`,
|
|
236
|
-
);
|
|
233
|
+
jsonError("run show", `Run ${runId} not found`);
|
|
237
234
|
} else {
|
|
238
235
|
printError("Run not found", runId);
|
|
239
236
|
}
|
|
@@ -244,13 +241,13 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
244
241
|
const agents = sessionStore.getByRun(runId);
|
|
245
242
|
|
|
246
243
|
if (json) {
|
|
247
|
-
|
|
244
|
+
jsonOutput("run show", { run, duration: runDuration(run), agents });
|
|
248
245
|
return;
|
|
249
246
|
}
|
|
250
247
|
|
|
251
248
|
process.stdout.write("Run Details\n");
|
|
252
249
|
process.stdout.write(`${"=".repeat(60)}\n`);
|
|
253
|
-
process.stdout.write(` ID: ${run.id}\n`);
|
|
250
|
+
process.stdout.write(` ID: ${accent(run.id)}\n`);
|
|
254
251
|
process.stdout.write(` Status: ${run.status}\n`);
|
|
255
252
|
process.stdout.write(` Started: ${run.startedAt}\n`);
|
|
256
253
|
if (run.completedAt) {
|
|
@@ -265,7 +262,7 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
265
262
|
for (const agent of agents) {
|
|
266
263
|
const agentDuration = formatAgentDuration(agent);
|
|
267
264
|
process.stdout.write(
|
|
268
|
-
` ${agent.agentName} [${agent.capability}] ${agent.state} | ${agentDuration}\n`,
|
|
265
|
+
` ${accent(agent.agentName)} [${agent.capability}] ${agent.state} | ${agentDuration}\n`,
|
|
269
266
|
);
|
|
270
267
|
}
|
|
271
268
|
} else {
|
package/src/commands/sling.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { writeOverlay } from "../agents/overlay.ts";
|
|
|
27
27
|
import { loadConfig } from "../config.ts";
|
|
28
28
|
import { AgentError, HierarchyError, ValidationError } from "../errors.ts";
|
|
29
29
|
import { inferDomain } from "../insights/analyzer.ts";
|
|
30
|
+
import { jsonOutput } from "../json.ts";
|
|
30
31
|
import { printSuccess } from "../logging/color.ts";
|
|
31
32
|
import { createMailClient } from "../mail/client.ts";
|
|
32
33
|
import { createMailStore } from "../mail/store.ts";
|
|
@@ -708,7 +709,7 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
|
|
|
708
709
|
};
|
|
709
710
|
|
|
710
711
|
if (opts.json ?? false) {
|
|
711
|
-
|
|
712
|
+
jsonOutput("sling", output);
|
|
712
713
|
} else {
|
|
713
714
|
printSuccess("Agent launched", name);
|
|
714
715
|
process.stdout.write(` Task: ${taskId}\n`);
|
|
@@ -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
|
}
|
|
@@ -314,6 +314,8 @@ describe("stopCommand --json output", () => {
|
|
|
314
314
|
const output = await captureStdout(() => stopCommand("my-builder", { json: true }, deps));
|
|
315
315
|
|
|
316
316
|
const parsed = JSON.parse(output.trim()) as Record<string, unknown>;
|
|
317
|
+
expect(parsed.success).toBe(true);
|
|
318
|
+
expect(parsed.command).toBe("stop");
|
|
317
319
|
expect(parsed.stopped).toBe(true);
|
|
318
320
|
expect(parsed.agentName).toBe("my-builder");
|
|
319
321
|
expect(parsed.sessionId).toBe(session.id);
|