@os-eco/overstory-cli 0.7.0 → 0.7.3

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 (91) hide show
  1. package/README.md +7 -6
  2. package/agents/builder.md +1 -1
  3. package/agents/coordinator.md +12 -11
  4. package/agents/lead.md +6 -6
  5. package/agents/monitor.md +4 -4
  6. package/agents/reviewer.md +1 -1
  7. package/agents/scout.md +5 -5
  8. package/agents/supervisor.md +36 -32
  9. package/package.json +1 -1
  10. package/src/agents/guard-rules.ts +97 -0
  11. package/src/agents/hooks-deployer.test.ts +6 -5
  12. package/src/agents/hooks-deployer.ts +7 -90
  13. package/src/agents/identity.test.ts +3 -2
  14. package/src/agents/manifest.test.ts +4 -3
  15. package/src/agents/overlay.test.ts +10 -9
  16. package/src/agents/overlay.ts +5 -5
  17. package/src/commands/agents.test.ts +10 -4
  18. package/src/commands/clean.test.ts +3 -0
  19. package/src/commands/completions.test.ts +8 -5
  20. package/src/commands/completions.ts +38 -2
  21. package/src/commands/coordinator.test.ts +1 -0
  22. package/src/commands/coordinator.ts +15 -11
  23. package/src/commands/costs.test.ts +9 -3
  24. package/src/commands/dashboard.test.ts +265 -6
  25. package/src/commands/dashboard.ts +367 -64
  26. package/src/commands/doctor.test.ts +3 -2
  27. package/src/commands/errors.test.ts +3 -2
  28. package/src/commands/feed.test.ts +3 -2
  29. package/src/commands/feed.ts +2 -29
  30. package/src/commands/init.test.ts +1 -2
  31. package/src/commands/init.ts +1 -8
  32. package/src/commands/inspect.test.ts +17 -2
  33. package/src/commands/log.test.ts +262 -8
  34. package/src/commands/log.ts +232 -110
  35. package/src/commands/logs.test.ts +3 -2
  36. package/src/commands/mail.test.ts +8 -2
  37. package/src/commands/metrics.test.ts +4 -3
  38. package/src/commands/monitor.ts +15 -11
  39. package/src/commands/nudge.test.ts +4 -2
  40. package/src/commands/prime.test.ts +4 -2
  41. package/src/commands/prime.ts +6 -2
  42. package/src/commands/replay.test.ts +3 -2
  43. package/src/commands/run.test.ts +3 -1
  44. package/src/commands/sling.test.ts +142 -1
  45. package/src/commands/sling.ts +145 -24
  46. package/src/commands/status.test.ts +9 -8
  47. package/src/commands/stop.test.ts +1 -0
  48. package/src/commands/supervisor.ts +19 -12
  49. package/src/commands/trace.test.ts +4 -2
  50. package/src/commands/watch.test.ts +3 -2
  51. package/src/commands/worktree.test.ts +9 -0
  52. package/src/config.test.ts +3 -3
  53. package/src/config.ts +29 -0
  54. package/src/doctor/agents.test.ts +3 -2
  55. package/src/doctor/consistency.test.ts +14 -0
  56. package/src/doctor/logs.test.ts +3 -2
  57. package/src/doctor/structure.test.ts +3 -2
  58. package/src/e2e/init-sling-lifecycle.test.ts +3 -5
  59. package/src/index.ts +3 -1
  60. package/src/logging/color.ts +1 -1
  61. package/src/logging/format.test.ts +110 -0
  62. package/src/logging/format.ts +42 -1
  63. package/src/logging/logger.test.ts +3 -2
  64. package/src/mail/broadcast.test.ts +1 -0
  65. package/src/mail/client.test.ts +3 -2
  66. package/src/mail/store.test.ts +3 -2
  67. package/src/merge/queue.test.ts +3 -2
  68. package/src/merge/resolver.test.ts +39 -0
  69. package/src/merge/resolver.ts +24 -5
  70. package/src/mulch/client.test.ts +63 -2
  71. package/src/mulch/client.ts +62 -1
  72. package/src/runtimes/claude.test.ts +5 -4
  73. package/src/runtimes/pi-guards.test.ts +457 -0
  74. package/src/runtimes/pi-guards.ts +349 -0
  75. package/src/runtimes/pi.test.ts +620 -0
  76. package/src/runtimes/pi.ts +244 -0
  77. package/src/runtimes/registry.test.ts +33 -0
  78. package/src/runtimes/registry.ts +15 -2
  79. package/src/runtimes/types.ts +63 -0
  80. package/src/schema-consistency.test.ts +5 -2
  81. package/src/sessions/compat.test.ts +3 -2
  82. package/src/sessions/compat.ts +1 -0
  83. package/src/sessions/store.test.ts +34 -2
  84. package/src/sessions/store.ts +37 -4
  85. package/src/test-helpers.ts +20 -1
  86. package/src/types.ts +17 -0
  87. package/src/watchdog/daemon.test.ts +11 -7
  88. package/src/watchdog/daemon.ts +1 -1
  89. package/src/watchdog/health.test.ts +1 -0
  90. package/src/watchdog/triage.test.ts +3 -2
  91. package/src/watchdog/triage.ts +14 -4
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, readdir, stat } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { ValidationError } from "../errors.ts";
@@ -9,8 +9,9 @@ import { createMailStore } from "../mail/store.ts";
9
9
  import { createMetricsStore } from "../metrics/store.ts";
10
10
  import type { MulchClient } from "../mulch/client.ts";
11
11
  import { createRunStore, createSessionStore } from "../sessions/store.ts";
12
+ import { cleanupTempDir } from "../test-helpers.ts";
12
13
  import type { AgentSession, MulchLearnResult, StoredEvent } from "../types.ts";
13
- import { autoRecordExpertise, logCommand } from "./log.ts";
14
+ import { appendOutcomeToAppliedRecords, autoRecordExpertise, logCommand } from "./log.ts";
14
15
 
15
16
  /**
16
17
  * Tests for `overstory log` command.
@@ -50,7 +51,7 @@ describe("logCommand", () => {
50
51
  afterEach(async () => {
51
52
  process.stdout.write = originalWrite;
52
53
  process.chdir(originalCwd);
53
- await rm(tempDir, { recursive: true, force: true });
54
+ await cleanupTempDir(tempDir);
54
55
  });
55
56
 
56
57
  function output(): string {
@@ -58,18 +59,28 @@ describe("logCommand", () => {
58
59
  }
59
60
 
60
61
  /**
61
- * Fake MulchClient for testing autoRecordExpertise.
62
- * Only learn() and record() are implemented — other methods are stubs.
62
+ * Fake MulchClient for testing autoRecordExpertise and appendOutcomeToAppliedRecords.
63
+ * Only learn(), record(), and appendOutcome() are implemented — other methods are stubs.
63
64
  * Justified: we are testing orchestration logic, not the mulch CLI itself.
64
65
  */
65
66
  function createFakeMulchClient(
66
67
  learnResult: MulchLearnResult,
67
- opts?: { recordShouldFail?: boolean },
68
+ opts?: { recordShouldFail?: boolean; appendOutcomeShouldFail?: boolean },
68
69
  ): {
69
70
  client: MulchClient;
70
71
  recordCalls: Array<{ domain: string; options: Record<string, unknown> }>;
72
+ appendOutcomeCalls: Array<{
73
+ domain: string;
74
+ id: string;
75
+ outcome: Record<string, unknown>;
76
+ }>;
71
77
  } {
72
78
  const recordCalls: Array<{ domain: string; options: Record<string, unknown> }> = [];
79
+ const appendOutcomeCalls: Array<{
80
+ domain: string;
81
+ id: string;
82
+ outcome: Record<string, unknown>;
83
+ }> = [];
73
84
  const client = {
74
85
  async learn() {
75
86
  return learnResult;
@@ -80,8 +91,14 @@ describe("logCommand", () => {
80
91
  }
81
92
  recordCalls.push({ domain, options });
82
93
  },
94
+ async appendOutcome(domain: string, id: string, outcome: Record<string, unknown>) {
95
+ if (opts?.appendOutcomeShouldFail) {
96
+ throw new Error("mulch appendOutcome failed");
97
+ }
98
+ appendOutcomeCalls.push({ domain, id, outcome });
99
+ },
83
100
  } as unknown as MulchClient;
84
- return { client, recordCalls };
101
+ return { client, recordCalls, appendOutcomeCalls };
85
102
  }
86
103
 
87
104
  test("--help flag shows help text", async () => {
@@ -229,6 +246,7 @@ describe("logCommand", () => {
229
246
  lastActivity: new Date().toISOString(),
230
247
  escalationLevel: 0,
231
248
  stalledSince: null,
249
+ transcriptPath: null,
232
250
  };
233
251
  const store = createSessionStore(dbPath);
234
252
  store.upsert(session);
@@ -284,6 +302,7 @@ describe("logCommand", () => {
284
302
  lastActivity: new Date().toISOString(),
285
303
  escalationLevel: 0,
286
304
  stalledSince: null,
305
+ transcriptPath: null,
287
306
  };
288
307
  const sessStore = createSessionStore(sessionsDbPath);
289
308
  sessStore.upsert(session);
@@ -324,6 +343,7 @@ describe("logCommand", () => {
324
343
  lastActivity: new Date(Date.now() - 60_000).toISOString(),
325
344
  escalationLevel: 0,
326
345
  stalledSince: null,
346
+ transcriptPath: null,
327
347
  };
328
348
  const store = createSessionStore(dbPath);
329
349
  store.upsert(session);
@@ -363,6 +383,7 @@ describe("logCommand", () => {
363
383
  lastActivity: new Date(Date.now() - 60_000).toISOString(),
364
384
  escalationLevel: 0,
365
385
  stalledSince: null,
386
+ transcriptPath: null,
366
387
  };
367
388
  const store = createSessionStore(dbPath);
368
389
  store.upsert(session);
@@ -400,6 +421,7 @@ describe("logCommand", () => {
400
421
  lastActivity: new Date().toISOString(),
401
422
  escalationLevel: 0,
402
423
  stalledSince: null,
424
+ transcriptPath: null,
403
425
  });
404
426
  sessionStoreLocal.close();
405
427
 
@@ -457,6 +479,7 @@ describe("logCommand", () => {
457
479
  lastActivity: new Date().toISOString(),
458
480
  escalationLevel: 0,
459
481
  stalledSince: null,
482
+ transcriptPath: null,
460
483
  });
461
484
  sessionStoreLocal.close();
462
485
 
@@ -487,6 +510,7 @@ describe("logCommand", () => {
487
510
  lastActivity: new Date().toISOString(),
488
511
  escalationLevel: 0,
489
512
  stalledSince: null,
513
+ transcriptPath: null,
490
514
  });
491
515
  sessionStoreLocal.close();
492
516
 
@@ -541,6 +565,7 @@ describe("logCommand", () => {
541
565
  lastActivity: new Date().toISOString(),
542
566
  escalationLevel: 0,
543
567
  stalledSince: null,
568
+ transcriptPath: null,
544
569
  });
545
570
  sessionStoreLocal.close();
546
571
 
@@ -594,6 +619,7 @@ describe("logCommand", () => {
594
619
  lastActivity: new Date().toISOString(),
595
620
  escalationLevel: 0,
596
621
  stalledSince: null,
622
+ transcriptPath: null,
597
623
  };
598
624
  const store = createSessionStore(dbPath);
599
625
  store.upsert(session);
@@ -634,6 +660,7 @@ describe("logCommand", () => {
634
660
  lastActivity: new Date().toISOString(),
635
661
  escalationLevel: 0,
636
662
  stalledSince: null,
663
+ transcriptPath: null,
637
664
  };
638
665
  const store = createSessionStore(dbPath);
639
666
  store.upsert(session);
@@ -676,6 +703,7 @@ describe("logCommand", () => {
676
703
  lastActivity: oldTimestamp,
677
704
  escalationLevel: 0,
678
705
  stalledSince: null,
706
+ transcriptPath: null,
679
707
  };
680
708
  const store = createSessionStore(dbPath);
681
709
  store.upsert(session);
@@ -715,6 +743,7 @@ describe("logCommand", () => {
715
743
  lastActivity: new Date().toISOString(),
716
744
  escalationLevel: 0,
717
745
  stalledSince: null,
746
+ transcriptPath: null,
718
747
  };
719
748
  const store = createSessionStore(dbPath);
720
749
  store.upsert(session);
@@ -773,6 +802,54 @@ describe("logCommand", () => {
773
802
  expect(eventsContent).toContain("unknown");
774
803
  });
775
804
 
805
+ test("tool-start writes to EventStore without --stdin flag (Pi runtime path)", async () => {
806
+ await logCommand(["tool-start", "--agent", "pi-agent", "--tool-name", "Read"]);
807
+
808
+ const eventsDbPath = join(tempDir, ".overstory", "events.db");
809
+ const eventStore = createEventStore(eventsDbPath);
810
+ const events = eventStore.getByAgent("pi-agent");
811
+ eventStore.close();
812
+
813
+ expect(events).toHaveLength(1);
814
+ expect(events[0]?.eventType).toBe("tool_start");
815
+ expect(events[0]?.toolName).toBe("Read");
816
+ expect(events[0]?.sessionId).toBeNull();
817
+ expect(events[0]?.agentName).toBe("pi-agent");
818
+ });
819
+
820
+ test("tool-end writes to EventStore without --stdin flag (Pi runtime path)", async () => {
821
+ await logCommand(["tool-start", "--agent", "pi-end-agent", "--tool-name", "Write"]);
822
+ await logCommand(["tool-end", "--agent", "pi-end-agent", "--tool-name", "Write"]);
823
+
824
+ const eventsDbPath = join(tempDir, ".overstory", "events.db");
825
+ const eventStore = createEventStore(eventsDbPath);
826
+ const events = eventStore.getByAgent("pi-end-agent");
827
+ eventStore.close();
828
+
829
+ expect(events).toHaveLength(2);
830
+ const startEv = events.find((e) => e.eventType === "tool_start");
831
+ const endEv = events.find((e) => e.eventType === "tool_end");
832
+ expect(startEv).toBeDefined();
833
+ expect(endEv).toBeDefined();
834
+ expect(startEv?.toolName).toBe("Write");
835
+ expect(endEv?.toolName).toBe("Write");
836
+ expect(startEv?.sessionId).toBeNull();
837
+ });
838
+
839
+ test("session-end writes to EventStore without --stdin flag (Pi runtime path)", async () => {
840
+ await logCommand(["session-end", "--agent", "pi-session-agent"]);
841
+
842
+ const eventsDbPath = join(tempDir, ".overstory", "events.db");
843
+ const eventStore = createEventStore(eventsDbPath);
844
+ const events = eventStore.getByAgent("pi-session-agent");
845
+ eventStore.close();
846
+
847
+ expect(events).toHaveLength(1);
848
+ expect(events[0]?.eventType).toBe("session_end");
849
+ expect(events[0]?.sessionId).toBeNull();
850
+ expect(events[0]?.agentName).toBe("pi-session-agent");
851
+ });
852
+
776
853
  test("--help includes --stdin option in output", async () => {
777
854
  await logCommand(["--help"]);
778
855
  const out = output();
@@ -800,6 +877,7 @@ describe("logCommand", () => {
800
877
  lastActivity: new Date().toISOString(),
801
878
  escalationLevel: 0,
802
879
  stalledSince: null,
880
+ transcriptPath: null,
803
881
  };
804
882
  const store = createSessionStore(dbPath);
805
883
  store.upsert(session);
@@ -839,6 +917,7 @@ describe("logCommand", () => {
839
917
  lastActivity: new Date().toISOString(),
840
918
  escalationLevel: 0,
841
919
  stalledSince: null,
920
+ transcriptPath: null,
842
921
  };
843
922
  const store = createSessionStore(dbPath);
844
923
  store.upsert(session);
@@ -1168,7 +1247,7 @@ describe("logCommand --stdin integration", () => {
1168
1247
  });
1169
1248
 
1170
1249
  afterEach(async () => {
1171
- await rm(tempDir, { recursive: true, force: true });
1250
+ await cleanupTempDir(tempDir);
1172
1251
  });
1173
1252
 
1174
1253
  /**
@@ -1373,6 +1452,7 @@ try {
1373
1452
  const scriptPath = join(tempDir, "_run-log-empty.ts");
1374
1453
  const scriptContent = `
1375
1454
  import { logCommand } from "${join(import.meta.dir, "log.ts").replace(/\\/g, "/")}";
1455
+
1376
1456
  try {
1377
1457
  await logCommand(["tool-start", "--agent", "empty-stdin-agent", "--stdin"]);
1378
1458
  } catch (e) {
@@ -1447,3 +1527,177 @@ try {
1447
1527
  // tool_result is not stored in EventStore (filtered out), but tool_name was parsed correctly
1448
1528
  });
1449
1529
  });
1530
+
1531
+ describe("appendOutcomeToAppliedRecords", () => {
1532
+ let tempDir: string;
1533
+
1534
+ /** Minimal fake MulchClient for appendOutcomeToAppliedRecords tests. */
1535
+ function makeOutcomeClient(opts?: { appendOutcomeShouldFail?: boolean }): {
1536
+ client: MulchClient;
1537
+ appendOutcomeCalls: Array<{ domain: string; id: string; outcome: Record<string, unknown> }>;
1538
+ } {
1539
+ const appendOutcomeCalls: Array<{
1540
+ domain: string;
1541
+ id: string;
1542
+ outcome: Record<string, unknown>;
1543
+ }> = [];
1544
+ const client = {
1545
+ async appendOutcome(domain: string, id: string, outcome: Record<string, unknown>) {
1546
+ if (opts?.appendOutcomeShouldFail) throw new Error("mulch appendOutcome failed");
1547
+ appendOutcomeCalls.push({ domain, id, outcome });
1548
+ },
1549
+ } as unknown as MulchClient;
1550
+ return { client, appendOutcomeCalls };
1551
+ }
1552
+
1553
+ beforeEach(async () => {
1554
+ tempDir = await mkdtemp(join(tmpdir(), "outcome-test-"));
1555
+ });
1556
+
1557
+ afterEach(async () => {
1558
+ await cleanupTempDir(tempDir);
1559
+ });
1560
+
1561
+ test("returns 0 when applied-records.json does not exist (backward compat)", async () => {
1562
+ const { client } = makeOutcomeClient();
1563
+ const count = await appendOutcomeToAppliedRecords({
1564
+ mulchClient: client,
1565
+ agentName: "test-agent",
1566
+ capability: "builder",
1567
+ taskId: "bead-001",
1568
+ projectRoot: tempDir,
1569
+ });
1570
+ expect(count).toBe(0);
1571
+ });
1572
+
1573
+ test("returns 0 when records array is empty", async () => {
1574
+ const agentDir = join(tempDir, ".overstory", "agents", "test-agent");
1575
+ await mkdir(agentDir, { recursive: true });
1576
+ await Bun.write(
1577
+ join(agentDir, "applied-records.json"),
1578
+ JSON.stringify({
1579
+ taskId: "bead-001",
1580
+ agentName: "test-agent",
1581
+ capability: "builder",
1582
+ records: [],
1583
+ }),
1584
+ );
1585
+
1586
+ const { client } = makeOutcomeClient();
1587
+ const count = await appendOutcomeToAppliedRecords({
1588
+ mulchClient: client,
1589
+ agentName: "test-agent",
1590
+ capability: "builder",
1591
+ taskId: "bead-001",
1592
+ projectRoot: tempDir,
1593
+ });
1594
+ expect(count).toBe(0);
1595
+ });
1596
+
1597
+ test("calls appendOutcome for each record and returns count", async () => {
1598
+ const agentDir = join(tempDir, ".overstory", "agents", "test-agent");
1599
+ await mkdir(agentDir, { recursive: true });
1600
+ const records = [
1601
+ { id: "mx-aaa111", domain: "agents" },
1602
+ { id: "mx-bbb222", domain: "typescript" },
1603
+ ];
1604
+ await Bun.write(
1605
+ join(agentDir, "applied-records.json"),
1606
+ JSON.stringify({
1607
+ taskId: "bead-001",
1608
+ agentName: "test-agent",
1609
+ capability: "builder",
1610
+ records,
1611
+ }),
1612
+ );
1613
+
1614
+ const { client, appendOutcomeCalls } = makeOutcomeClient();
1615
+ const count = await appendOutcomeToAppliedRecords({
1616
+ mulchClient: client,
1617
+ agentName: "test-agent",
1618
+ capability: "builder",
1619
+ taskId: "bead-001",
1620
+ projectRoot: tempDir,
1621
+ });
1622
+
1623
+ expect(count).toBe(2);
1624
+ expect(appendOutcomeCalls).toHaveLength(2);
1625
+ expect(appendOutcomeCalls[0]).toMatchObject({ id: "mx-aaa111", domain: "agents" });
1626
+ expect(appendOutcomeCalls[1]).toMatchObject({ id: "mx-bbb222", domain: "typescript" });
1627
+ expect(appendOutcomeCalls[0]?.outcome).toMatchObject({
1628
+ status: "success",
1629
+ agent: "test-agent",
1630
+ });
1631
+ });
1632
+
1633
+ test("cleans up applied-records.json after processing", async () => {
1634
+ const agentDir = join(tempDir, ".overstory", "agents", "test-agent");
1635
+ await mkdir(agentDir, { recursive: true });
1636
+ const appliedPath = join(agentDir, "applied-records.json");
1637
+ await Bun.write(
1638
+ appliedPath,
1639
+ JSON.stringify({
1640
+ taskId: "bead-001",
1641
+ agentName: "test-agent",
1642
+ capability: "builder",
1643
+ records: [{ id: "mx-abc123", domain: "agents" }],
1644
+ }),
1645
+ );
1646
+
1647
+ const { client } = makeOutcomeClient();
1648
+ await appendOutcomeToAppliedRecords({
1649
+ mulchClient: client,
1650
+ agentName: "test-agent",
1651
+ capability: "builder",
1652
+ taskId: "bead-001",
1653
+ projectRoot: tempDir,
1654
+ });
1655
+
1656
+ expect(await Bun.file(appliedPath).exists()).toBe(false);
1657
+ });
1658
+
1659
+ test("continues when individual appendOutcome calls fail (non-fatal per record)", async () => {
1660
+ const agentDir = join(tempDir, ".overstory", "agents", "test-agent");
1661
+ await mkdir(agentDir, { recursive: true });
1662
+ const records = [
1663
+ { id: "mx-fail111", domain: "agents" },
1664
+ { id: "mx-fail222", domain: "typescript" },
1665
+ ];
1666
+ await Bun.write(
1667
+ join(agentDir, "applied-records.json"),
1668
+ JSON.stringify({
1669
+ taskId: "bead-002",
1670
+ agentName: "test-agent",
1671
+ capability: "builder",
1672
+ records,
1673
+ }),
1674
+ );
1675
+
1676
+ // appendOutcomeShouldFail=true makes all calls throw — should return 0 but not throw
1677
+ const { client } = makeOutcomeClient({ appendOutcomeShouldFail: true });
1678
+ const count = await appendOutcomeToAppliedRecords({
1679
+ mulchClient: client,
1680
+ agentName: "test-agent",
1681
+ capability: "builder",
1682
+ taskId: "bead-002",
1683
+ projectRoot: tempDir,
1684
+ });
1685
+ expect(count).toBe(0);
1686
+ });
1687
+
1688
+ test("returns 0 for malformed JSON", async () => {
1689
+ const agentDir = join(tempDir, ".overstory", "agents", "test-agent");
1690
+ await mkdir(agentDir, { recursive: true });
1691
+ await Bun.write(join(agentDir, "applied-records.json"), "not-valid-json{{{");
1692
+
1693
+ const { client } = makeOutcomeClient();
1694
+ const count = await appendOutcomeToAppliedRecords({
1695
+ mulchClient: client,
1696
+ agentName: "test-agent",
1697
+ capability: "builder",
1698
+ taskId: null,
1699
+ projectRoot: tempDir,
1700
+ });
1701
+ expect(count).toBe(0);
1702
+ });
1703
+ });