@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.
Files changed (72) hide show
  1. package/README.md +5 -2
  2. package/agents/coordinator.md +5 -5
  3. package/agents/lead.md +1 -6
  4. package/agents/merger.md +3 -3
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +3 -3
  7. package/agents/supervisor.md +16 -16
  8. package/package.json +1 -1
  9. package/src/agents/hooks-deployer.test.ts +180 -0
  10. package/src/agents/hooks-deployer.ts +32 -1
  11. package/src/commands/agents.ts +9 -6
  12. package/src/commands/clean.ts +5 -3
  13. package/src/commands/completions.ts +3 -4
  14. package/src/commands/coordinator.test.ts +15 -12
  15. package/src/commands/coordinator.ts +28 -25
  16. package/src/commands/costs.test.ts +48 -38
  17. package/src/commands/costs.ts +48 -38
  18. package/src/commands/dashboard.ts +7 -7
  19. package/src/commands/doctor.test.ts +8 -0
  20. package/src/commands/doctor.ts +2 -6
  21. package/src/commands/errors.test.ts +47 -40
  22. package/src/commands/errors.ts +5 -4
  23. package/src/commands/feed.test.ts +40 -33
  24. package/src/commands/feed.ts +3 -2
  25. package/src/commands/group.ts +28 -18
  26. package/src/commands/hooks.test.ts +1 -1
  27. package/src/commands/hooks.ts +9 -9
  28. package/src/commands/init.test.ts +105 -5
  29. package/src/commands/init.ts +17 -12
  30. package/src/commands/inspect.test.ts +2 -0
  31. package/src/commands/inspect.ts +9 -8
  32. package/src/commands/logs.test.ts +5 -6
  33. package/src/commands/logs.ts +2 -1
  34. package/src/commands/mail.test.ts +17 -16
  35. package/src/commands/mail.ts +17 -17
  36. package/src/commands/merge.ts +12 -12
  37. package/src/commands/metrics.test.ts +15 -2
  38. package/src/commands/metrics.ts +3 -2
  39. package/src/commands/monitor.ts +9 -7
  40. package/src/commands/nudge.ts +4 -4
  41. package/src/commands/prime.test.ts +1 -6
  42. package/src/commands/prime.ts +2 -3
  43. package/src/commands/replay.test.ts +62 -55
  44. package/src/commands/replay.ts +3 -2
  45. package/src/commands/run.ts +24 -26
  46. package/src/commands/sling.ts +4 -2
  47. package/src/commands/spec.test.ts +10 -8
  48. package/src/commands/spec.ts +3 -2
  49. package/src/commands/status.test.ts +2 -1
  50. package/src/commands/status.ts +7 -6
  51. package/src/commands/stop.test.ts +10 -6
  52. package/src/commands/stop.ts +13 -13
  53. package/src/commands/supervisor.ts +12 -10
  54. package/src/commands/trace.test.ts +52 -44
  55. package/src/commands/trace.ts +5 -4
  56. package/src/commands/watch.ts +8 -10
  57. package/src/commands/worktree.test.ts +27 -20
  58. package/src/commands/worktree.ts +29 -30
  59. package/src/doctor/version.ts +2 -2
  60. package/src/e2e/init-sling-lifecycle.test.ts +1 -5
  61. package/src/index.ts +99 -14
  62. package/src/json.test.ts +72 -0
  63. package/src/json.ts +24 -0
  64. package/src/logging/color.test.ts +127 -0
  65. package/src/logging/color.ts +28 -0
  66. package/src/mulch/client.test.ts +22 -22
  67. package/src/worktree/tmux.test.ts +123 -5
  68. package/src/worktree/tmux.ts +38 -8
  69. package/agents/issue-reviews.md +0 -71
  70. package/agents/pr-reviews.md +0 -60
  71. package/agents/prioritize.md +0 -110
  72. package/agents/release.md +0 -56
@@ -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
- process.stdout.write('{"sessions":[]}\n');
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
- process.stdout.write(`${JSON.stringify({ sessions })}\n`);
58
+ jsonOutput("metrics", { sessions } as Record<string, unknown>);
58
59
  return;
59
60
  }
60
61
 
@@ -21,6 +21,8 @@ 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";
25
+ import { printHint, printSuccess } from "../logging/color.ts";
24
26
  import { openSessionStore } from "../sessions/compat.ts";
25
27
  import type { AgentSession } from "../types.ts";
26
28
  import { createSession, isSessionAlive, killSession, sendKeys } from "../worktree/tmux.ts";
@@ -189,9 +191,9 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
189
191
  };
190
192
 
191
193
  if (json) {
192
- process.stdout.write(`${JSON.stringify(output)}\n`);
194
+ jsonOutput("monitor start", output);
193
195
  } else {
194
- process.stdout.write("Monitor started\n");
196
+ printSuccess("Monitor started");
195
197
  process.stdout.write(` Tmux: ${tmuxSession}\n`);
196
198
  process.stdout.write(` Root: ${projectRoot}\n`);
197
199
  process.stdout.write(` PID: ${pid}\n`);
@@ -247,9 +249,9 @@ async function stopMonitor(opts: { json: boolean }): Promise<void> {
247
249
  store.updateLastActivity(MONITOR_NAME);
248
250
 
249
251
  if (json) {
250
- process.stdout.write(`${JSON.stringify({ stopped: true, sessionId: session.id })}\n`);
252
+ jsonOutput("monitor stop", { stopped: true, sessionId: session.id });
251
253
  } else {
252
- process.stdout.write(`Monitor stopped (session: ${session.id})\n`);
254
+ printSuccess("Monitor stopped", session.id);
253
255
  }
254
256
  } finally {
255
257
  store.close();
@@ -279,9 +281,9 @@ async function statusMonitor(opts: { json: boolean }): Promise<void> {
279
281
  session.state === "zombie"
280
282
  ) {
281
283
  if (json) {
282
- process.stdout.write(`${JSON.stringify({ running: false })}\n`);
284
+ jsonOutput("monitor status", { running: false });
283
285
  } else {
284
- process.stdout.write("Monitor is not running\n");
286
+ printHint("Monitor is not running");
285
287
  }
286
288
  return;
287
289
  }
@@ -306,7 +308,7 @@ async function statusMonitor(opts: { json: boolean }): Promise<void> {
306
308
  };
307
309
 
308
310
  if (json) {
309
- process.stdout.write(`${JSON.stringify(status)}\n`);
311
+ jsonOutput("monitor status", status);
310
312
  } else {
311
313
  const stateLabel = alive ? "running" : session.state;
312
314
  process.stdout.write(`Monitor: ${stateLabel}\n`);
@@ -13,6 +13,8 @@ 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";
17
+ import { printSuccess } from "../logging/color.ts";
16
18
  import { openSessionStore } from "../sessions/compat.ts";
17
19
  import type { EventStore } from "../types.ts";
18
20
  import { isSessionAlive, sendKeys } from "../worktree/tmux.ts";
@@ -309,11 +311,9 @@ export async function nudgeCommand(args: string[]): Promise<void> {
309
311
  const result = await nudgeAgent(projectRoot, agentName, message, opts.force ?? false);
310
312
 
311
313
  if (opts.json) {
312
- process.stdout.write(
313
- `${JSON.stringify({ agentName, delivered: result.delivered, reason: result.reason })}\n`,
314
- );
314
+ jsonOutput("nudge", { agentName, delivered: result.delivered, reason: result.reason });
315
315
  } else if (result.delivered) {
316
- process.stdout.write(`📢 Nudged "${agentName}"\n`);
316
+ printSuccess("Nudge delivered", agentName);
317
317
  } else {
318
318
  throw new AgentError(`Nudge failed: ${result.reason}`, { agentName });
319
319
  }
@@ -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(err).toContain('Warning: agent "unknown-agent" not found');
116
+ expect(out).toContain('agent "unknown-agent" not found');
122
117
  });
123
118
 
124
119
  test("agent with identity.yaml shows identity details", async () => {
@@ -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
- process.stderr.write(
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
- expect(out).toBe("[]\n");
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
 
@@ -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
- process.stdout.write("[]\n");
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
- process.stdout.write(`${JSON.stringify(events)}\n`);
309
+ jsonOutput("replay", { events });
309
310
  return;
310
311
  }
311
312
 
@@ -15,6 +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 { jsonError, jsonOutput } from "../json.ts";
19
+ import { accent, printError, printHint, printSuccess } from "../logging/color.ts";
18
20
  import { createRunStore, createSessionStore } from "../sessions/store.ts";
19
21
  import type { AgentSession, Run } from "../types.ts";
20
22
 
@@ -82,9 +84,9 @@ async function showCurrentRun(overstoryDir: string, json: boolean): Promise<void
82
84
  const runId = await readCurrentRunId(overstoryDir);
83
85
  if (!runId) {
84
86
  if (json) {
85
- process.stdout.write('{"run":null,"message":"No active run"}\n');
87
+ jsonOutput("run", { run: null, message: "No active run" });
86
88
  } else {
87
- process.stdout.write("No active run\n");
89
+ printHint("No active run");
88
90
  }
89
91
  return;
90
92
  }
@@ -95,23 +97,21 @@ async function showCurrentRun(overstoryDir: string, json: boolean): Promise<void
95
97
  const run = runStore.getRun(runId);
96
98
  if (!run) {
97
99
  if (json) {
98
- process.stdout.write(
99
- `${JSON.stringify({ run: null, message: `Run ${runId} not found in store` })}\n`,
100
- );
100
+ jsonOutput("run", { run: null, message: `Run ${runId} not found in store` });
101
101
  } else {
102
- process.stdout.write(`Run ${runId} not found in store\n`);
102
+ process.stdout.write(`Run ${accent(runId)} not found in store\n`);
103
103
  }
104
104
  return;
105
105
  }
106
106
 
107
107
  if (json) {
108
- process.stdout.write(`${JSON.stringify({ run, duration: runDuration(run) })}\n`);
108
+ jsonOutput("run", { run, duration: runDuration(run) });
109
109
  return;
110
110
  }
111
111
 
112
112
  process.stdout.write("Current Run\n");
113
113
  process.stdout.write(`${"=".repeat(50)}\n`);
114
- process.stdout.write(` ID: ${run.id}\n`);
114
+ process.stdout.write(` ID: ${accent(run.id)}\n`);
115
115
  process.stdout.write(` Status: ${run.status}\n`);
116
116
  process.stdout.write(` Started: ${run.startedAt}\n`);
117
117
  process.stdout.write(` Agents: ${run.agentCount}\n`);
@@ -129,9 +129,9 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
129
129
  const dbFile = Bun.file(dbPath);
130
130
  if (!(await dbFile.exists())) {
131
131
  if (json) {
132
- process.stdout.write('{"runs":[]}\n');
132
+ jsonOutput("run list", { runs: [] });
133
133
  } else {
134
- process.stdout.write("No runs recorded yet.\n");
134
+ printHint("No runs recorded yet");
135
135
  }
136
136
  return;
137
137
  }
@@ -142,12 +142,12 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
142
142
 
143
143
  if (json) {
144
144
  const runsWithDuration = runs.map((r) => ({ ...r, duration: runDuration(r) }));
145
- process.stdout.write(`${JSON.stringify({ runs: runsWithDuration })}\n`);
145
+ jsonOutput("run list", { runs: runsWithDuration });
146
146
  return;
147
147
  }
148
148
 
149
149
  if (runs.length === 0) {
150
- process.stdout.write("No runs recorded yet.\n");
150
+ printHint("No runs recorded yet");
151
151
  return;
152
152
  }
153
153
 
@@ -159,7 +159,7 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
159
159
  process.stdout.write(`${"-".repeat(70)}\n`);
160
160
 
161
161
  for (const run of runs) {
162
- 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));
163
163
  const status = run.status.padEnd(10);
164
164
  const agents = String(run.agentCount).padEnd(7);
165
165
  const duration = runDuration(run);
@@ -177,9 +177,9 @@ async function completeCurrentRun(overstoryDir: string, json: boolean): Promise<
177
177
  const runId = await readCurrentRunId(overstoryDir);
178
178
  if (!runId) {
179
179
  if (json) {
180
- process.stdout.write('{"success":false,"message":"No active run to complete"}\n');
180
+ jsonError("run complete", "No active run to complete");
181
181
  } else {
182
- process.stderr.write("No active run to complete\n");
182
+ printError("No active run to complete");
183
183
  }
184
184
  process.exitCode = 1;
185
185
  return;
@@ -202,9 +202,9 @@ async function completeCurrentRun(overstoryDir: string, json: boolean): Promise<
202
202
  }
203
203
 
204
204
  if (json) {
205
- process.stdout.write(`${JSON.stringify({ success: true, runId, status: "completed" })}\n`);
205
+ jsonOutput("run complete", { runId, status: "completed" });
206
206
  } else {
207
- process.stdout.write(`Run ${runId} marked as completed\n`);
207
+ printSuccess("Run completed", runId);
208
208
  }
209
209
  }
210
210
 
@@ -216,9 +216,9 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
216
216
  const dbFile = Bun.file(dbPath);
217
217
  if (!(await dbFile.exists())) {
218
218
  if (json) {
219
- process.stdout.write(`${JSON.stringify({ run: null, message: `Run ${runId} not found` })}\n`);
219
+ jsonError("run show", `Run ${runId} not found`);
220
220
  } else {
221
- process.stderr.write(`Run ${runId} not found\n`);
221
+ printError("Run not found", runId);
222
222
  }
223
223
  process.exitCode = 1;
224
224
  return;
@@ -230,11 +230,9 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
230
230
  const run = runStore.getRun(runId);
231
231
  if (!run) {
232
232
  if (json) {
233
- process.stdout.write(
234
- `${JSON.stringify({ run: null, message: `Run ${runId} not found` })}\n`,
235
- );
233
+ jsonError("run show", `Run ${runId} not found`);
236
234
  } else {
237
- process.stderr.write(`Run ${runId} not found\n`);
235
+ printError("Run not found", runId);
238
236
  }
239
237
  process.exitCode = 1;
240
238
  return;
@@ -243,13 +241,13 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
243
241
  const agents = sessionStore.getByRun(runId);
244
242
 
245
243
  if (json) {
246
- process.stdout.write(`${JSON.stringify({ run, duration: runDuration(run), agents })}\n`);
244
+ jsonOutput("run show", { run, duration: runDuration(run), agents });
247
245
  return;
248
246
  }
249
247
 
250
248
  process.stdout.write("Run Details\n");
251
249
  process.stdout.write(`${"=".repeat(60)}\n`);
252
- process.stdout.write(` ID: ${run.id}\n`);
250
+ process.stdout.write(` ID: ${accent(run.id)}\n`);
253
251
  process.stdout.write(` Status: ${run.status}\n`);
254
252
  process.stdout.write(` Started: ${run.startedAt}\n`);
255
253
  if (run.completedAt) {
@@ -264,7 +262,7 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
264
262
  for (const agent of agents) {
265
263
  const agentDuration = formatAgentDuration(agent);
266
264
  process.stdout.write(
267
- ` ${agent.agentName} [${agent.capability}] ${agent.state} | ${agentDuration}\n`,
265
+ ` ${accent(agent.agentName)} [${agent.capability}] ${agent.state} | ${agentDuration}\n`,
268
266
  );
269
267
  }
270
268
  } else {
@@ -27,6 +27,8 @@ 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";
31
+ import { printSuccess } from "../logging/color.ts";
30
32
  import { createMailClient } from "../mail/client.ts";
31
33
  import { createMailStore } from "../mail/store.ts";
32
34
  import { createMulchClient } from "../mulch/client.ts";
@@ -707,9 +709,9 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
707
709
  };
708
710
 
709
711
  if (opts.json ?? false) {
710
- process.stdout.write(`${JSON.stringify(output)}\n`);
712
+ jsonOutput("sling", output);
711
713
  } else {
712
- process.stdout.write(`Agent "${name}" launched!\n`);
714
+ printSuccess("Agent launched", name);
713
715
  process.stdout.write(` Task: ${taskId}\n`);
714
716
  process.stdout.write(` Branch: ${branchName}\n`);
715
717
  process.stdout.write(` Worktree: ${worktreePath}\n`);
@@ -142,13 +142,13 @@ describe("writeSpec", () => {
142
142
  // === specWriteCommand (CLI integration) ===
143
143
 
144
144
  describe("specWriteCommand (integration)", () => {
145
- test("writes spec and prints path", async () => {
145
+ test("writes spec and prints success", async () => {
146
146
  await specWriteCommand("task-cmd", { body: "# CLI Spec" });
147
147
 
148
- // Path may differ due to macOS /var -> /private/var symlink resolution
149
- expect(stdoutOutput.trim()).toContain(".overstory/specs/task-cmd.md");
148
+ expect(stdoutOutput).toContain("Spec written");
149
+ expect(stdoutOutput).toContain("task-cmd");
150
150
 
151
- const specPath = stdoutOutput.trim();
151
+ const specPath = join(tempDir, ".overstory", "specs", "task-cmd.md");
152
152
  const content = await Bun.file(specPath).text();
153
153
  expect(content).toBe("# CLI Spec\n");
154
154
  });
@@ -156,9 +156,10 @@ describe("specWriteCommand (integration)", () => {
156
156
  test("writes spec with agent attribution", async () => {
157
157
  await specWriteCommand("task-attr", { body: "# Attributed", agent: "scout-2" });
158
158
 
159
- expect(stdoutOutput.trim()).toContain(".overstory/specs/task-attr.md");
159
+ expect(stdoutOutput).toContain("Spec written");
160
+ expect(stdoutOutput).toContain("task-attr");
160
161
 
161
- const specPath = stdoutOutput.trim();
162
+ const specPath = join(tempDir, ".overstory", "specs", "task-attr.md");
162
163
  const content = await Bun.file(specPath).text();
163
164
  expect(content).toContain("<!-- written-by: scout-2 -->");
164
165
  expect(content).toContain("# Attributed");
@@ -167,9 +168,10 @@ describe("specWriteCommand (integration)", () => {
167
168
  test("writes spec without agent when agent is omitted", async () => {
168
169
  await specWriteCommand("task-noagent", { body: "# No Agent" });
169
170
 
170
- expect(stdoutOutput.trim()).toContain(".overstory/specs/task-noagent.md");
171
+ expect(stdoutOutput).toContain("Spec written");
172
+ expect(stdoutOutput).toContain("task-noagent");
171
173
 
172
- const specPath = stdoutOutput.trim();
174
+ const specPath = join(tempDir, ".overstory", "specs", "task-noagent.md");
173
175
  const content = await Bun.file(specPath).text();
174
176
  expect(content).not.toContain("written-by");
175
177
  expect(content).toBe("# No Agent\n");