@os-eco/overstory-cli 0.6.4 → 0.6.6

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 (117) hide show
  1. package/README.md +61 -61
  2. package/agents/builder.md +16 -16
  3. package/agents/coordinator.md +57 -57
  4. package/agents/issue-reviews.md +71 -0
  5. package/agents/lead.md +43 -42
  6. package/agents/merger.md +15 -15
  7. package/agents/monitor.md +37 -37
  8. package/agents/pr-reviews.md +60 -0
  9. package/agents/prioritize.md +110 -0
  10. package/agents/release.md +56 -0
  11. package/agents/reviewer.md +15 -15
  12. package/agents/scout.md +18 -18
  13. package/agents/supervisor.md +78 -78
  14. package/package.json +1 -1
  15. package/src/agents/checkpoint.test.ts +2 -2
  16. package/src/agents/hooks-deployer.test.ts +59 -25
  17. package/src/agents/hooks-deployer.ts +24 -6
  18. package/src/agents/identity.test.ts +27 -27
  19. package/src/agents/identity.ts +10 -10
  20. package/src/agents/lifecycle.test.ts +6 -6
  21. package/src/agents/lifecycle.ts +2 -2
  22. package/src/agents/overlay.test.ts +14 -14
  23. package/src/agents/overlay.ts +14 -14
  24. package/src/commands/agents.test.ts +5 -5
  25. package/src/commands/agents.ts +10 -9
  26. package/src/commands/clean.test.ts +5 -5
  27. package/src/commands/clean.ts +5 -5
  28. package/src/commands/completions.test.ts +10 -10
  29. package/src/commands/completions.ts +26 -28
  30. package/src/commands/coordinator.test.ts +4 -4
  31. package/src/commands/coordinator.ts +13 -13
  32. package/src/commands/costs.test.ts +45 -45
  33. package/src/commands/costs.ts +1 -1
  34. package/src/commands/dashboard.ts +11 -11
  35. package/src/commands/doctor.ts +4 -4
  36. package/src/commands/errors.ts +1 -1
  37. package/src/commands/feed.ts +1 -1
  38. package/src/commands/group.ts +3 -3
  39. package/src/commands/hooks.test.ts +7 -7
  40. package/src/commands/hooks.ts +7 -7
  41. package/src/commands/init.test.ts +6 -2
  42. package/src/commands/init.ts +19 -19
  43. package/src/commands/inspect.test.ts +16 -16
  44. package/src/commands/inspect.ts +19 -19
  45. package/src/commands/log.test.ts +21 -21
  46. package/src/commands/log.ts +10 -10
  47. package/src/commands/logs.ts +1 -1
  48. package/src/commands/mail.test.ts +7 -7
  49. package/src/commands/mail.ts +28 -11
  50. package/src/commands/merge.test.ts +8 -8
  51. package/src/commands/merge.ts +15 -15
  52. package/src/commands/metrics.test.ts +7 -7
  53. package/src/commands/metrics.ts +3 -3
  54. package/src/commands/monitor.test.ts +5 -5
  55. package/src/commands/monitor.ts +5 -5
  56. package/src/commands/nudge.test.ts +1 -1
  57. package/src/commands/nudge.ts +1 -1
  58. package/src/commands/prime.test.ts +5 -5
  59. package/src/commands/prime.ts +8 -8
  60. package/src/commands/replay.ts +1 -1
  61. package/src/commands/run.test.ts +1 -1
  62. package/src/commands/run.ts +2 -2
  63. package/src/commands/sling.test.ts +89 -7
  64. package/src/commands/sling.ts +109 -18
  65. package/src/commands/spec.test.ts +2 -2
  66. package/src/commands/spec.ts +13 -14
  67. package/src/commands/status.test.ts +99 -3
  68. package/src/commands/status.ts +19 -20
  69. package/src/commands/stop.test.ts +1 -1
  70. package/src/commands/stop.ts +2 -2
  71. package/src/commands/supervisor.test.ts +10 -10
  72. package/src/commands/supervisor.ts +14 -14
  73. package/src/commands/trace.test.ts +7 -7
  74. package/src/commands/trace.ts +10 -10
  75. package/src/commands/watch.ts +5 -5
  76. package/src/commands/worktree.test.ts +208 -32
  77. package/src/commands/worktree.ts +56 -18
  78. package/src/doctor/consistency.test.ts +14 -14
  79. package/src/doctor/dependencies.test.ts +5 -5
  80. package/src/doctor/dependencies.ts +2 -2
  81. package/src/doctor/logs.ts +1 -1
  82. package/src/doctor/merge-queue.test.ts +4 -4
  83. package/src/doctor/structure.test.ts +1 -1
  84. package/src/doctor/structure.ts +1 -1
  85. package/src/doctor/version.test.ts +3 -3
  86. package/src/doctor/version.ts +1 -1
  87. package/src/e2e/init-sling-lifecycle.test.ts +8 -4
  88. package/src/errors.ts +1 -1
  89. package/src/index.ts +13 -11
  90. package/src/mail/broadcast.test.ts +1 -1
  91. package/src/mail/client.test.ts +7 -7
  92. package/src/mail/client.ts +2 -2
  93. package/src/mail/store.test.ts +3 -3
  94. package/src/merge/queue.test.ts +12 -12
  95. package/src/merge/queue.ts +2 -2
  96. package/src/merge/resolver.test.ts +159 -7
  97. package/src/merge/resolver.ts +46 -2
  98. package/src/metrics/store.test.ts +44 -44
  99. package/src/metrics/store.ts +2 -2
  100. package/src/metrics/summary.test.ts +35 -35
  101. package/src/mulch/client.test.ts +1 -1
  102. package/src/mulch/client.ts +1 -1
  103. package/src/sessions/compat.test.ts +3 -3
  104. package/src/sessions/compat.ts +1 -1
  105. package/src/sessions/store.test.ts +4 -4
  106. package/src/sessions/store.ts +2 -2
  107. package/src/types.ts +14 -14
  108. package/src/watchdog/daemon.test.ts +10 -10
  109. package/src/watchdog/daemon.ts +1 -1
  110. package/src/watchdog/health.test.ts +1 -1
  111. package/src/worktree/manager.test.ts +20 -20
  112. package/src/worktree/manager.ts +120 -4
  113. package/src/worktree/tmux.test.ts +8 -3
  114. package/src/worktree/tmux.ts +19 -18
  115. package/templates/CLAUDE.md.tmpl +27 -27
  116. package/templates/hooks.json.tmpl +15 -11
  117. package/templates/overlay.md.tmpl +7 -7
@@ -24,6 +24,7 @@ import {
24
24
  createMergeResolver,
25
25
  looksLikeProse,
26
26
  parseConflictPatterns,
27
+ resolveConflictsUnion,
27
28
  } from "./resolver.ts";
28
29
 
29
30
  /**
@@ -53,7 +54,7 @@ function mockSpawnResult(
53
54
  function makeTestEntry(overrides?: Partial<MergeEntry>): MergeEntry {
54
55
  return {
55
56
  branchName: overrides?.branchName ?? "feature-branch",
56
- beadId: overrides?.beadId ?? "bead-123",
57
+ taskId: overrides?.taskId ?? "bead-123",
57
58
  agentName: overrides?.agentName ?? "test-agent",
58
59
  filesModified: overrides?.filesModified ?? ["src/test.ts"],
59
60
  enqueuedAt: overrides?.enqueuedAt ?? new Date().toISOString(),
@@ -551,7 +552,7 @@ describe("createMergeResolver", () => {
551
552
 
552
553
  const entry = makeTestEntry({
553
554
  branchName: "overstory/my-agent/bead-xyz",
554
- beadId: "bead-xyz",
555
+ taskId: "bead-xyz",
555
556
  agentName: "my-agent",
556
557
  filesModified: ["src/test.ts"],
557
558
  });
@@ -564,7 +565,7 @@ describe("createMergeResolver", () => {
564
565
  const result = await resolver.resolve(entry, defaultBranch, repoDir);
565
566
 
566
567
  expect(result.entry.branchName).toBe("overstory/my-agent/bead-xyz");
567
- expect(result.entry.beadId).toBe("bead-xyz");
568
+ expect(result.entry.taskId).toBe("bead-xyz");
568
569
  expect(result.entry.agentName).toBe("my-agent");
569
570
  });
570
571
  });
@@ -744,7 +745,7 @@ describe("createMergeResolver", () => {
744
745
 
745
746
  const entry = makeTestEntry({
746
747
  branchName: "feature-branch",
747
- beadId: "bead-abc-123",
748
+ taskId: "bead-abc-123",
748
749
  agentName: "test-builder",
749
750
  filesModified: ["src/test.ts"],
750
751
  });
@@ -811,7 +812,7 @@ describe("createMergeResolver", () => {
811
812
 
812
813
  const entry = makeTestEntry({
813
814
  branchName: "feature-branch",
814
- beadId: "bead-fail-456",
815
+ taskId: "bead-fail-456",
815
816
  agentName: "test-agent",
816
817
  filesModified: ["src/test.ts"],
817
818
  });
@@ -906,7 +907,7 @@ describe("createMergeResolver", () => {
906
907
 
907
908
  const entry = makeTestEntry({
908
909
  branchName: "feature-branch",
909
- beadId: "bead-ai-789",
910
+ taskId: "bead-ai-789",
910
911
  filesModified: ["src/test.ts"],
911
912
  });
912
913
 
@@ -981,7 +982,7 @@ describe("createMergeResolver", () => {
981
982
 
982
983
  const entry = makeTestEntry({
983
984
  branchName: "feature-branch",
984
- beadId: "bead-reimagine-xyz",
985
+ taskId: "bead-reimagine-xyz",
985
986
  filesModified: ["src/reimagine-target.ts"],
986
987
  });
987
988
 
@@ -1288,6 +1289,157 @@ describe("createMergeResolver", () => {
1288
1289
  });
1289
1290
  });
1290
1291
 
1292
+ describe("resolveConflictsUnion", () => {
1293
+ test("returns null when no conflict markers are present", () => {
1294
+ expect(resolveConflictsUnion("no conflicts here\n")).toBeNull();
1295
+ expect(resolveConflictsUnion("")).toBeNull();
1296
+ });
1297
+
1298
+ test("keeps both canonical and incoming content for a single conflict", () => {
1299
+ const content = [
1300
+ "<<<<<<< HEAD\n",
1301
+ '{"id":"a"}\n',
1302
+ '{"id":"c"}\n',
1303
+ "=======\n",
1304
+ '{"id":"a"}\n',
1305
+ '{"id":"b"}\n',
1306
+ ">>>>>>> feature-branch\n",
1307
+ ].join("");
1308
+ const result = resolveConflictsUnion(content);
1309
+ expect(result).not.toBeNull();
1310
+ expect(result).toContain('{"id":"a"}\n{"id":"c"}\n');
1311
+ expect(result).toContain('{"id":"a"}\n{"id":"b"}\n');
1312
+ // No conflict markers remain
1313
+ expect(result).not.toContain("<<<<<<<");
1314
+ expect(result).not.toContain("=======");
1315
+ expect(result).not.toContain(">>>>>>>");
1316
+ });
1317
+
1318
+ test("resolves multiple conflict blocks with union strategy", () => {
1319
+ const block = (canonical: string, incoming: string): string =>
1320
+ `<<<<<<< HEAD\n${canonical}\n=======\n${incoming}\n>>>>>>> branch\n`;
1321
+ const content = `${block("line-a\n", "line-b\n")}middle\n${block("line-c\n", "line-d\n")}`;
1322
+ const result = resolveConflictsUnion(content);
1323
+ expect(result).not.toBeNull();
1324
+ expect(result).toContain("line-a\n");
1325
+ expect(result).toContain("line-b\n");
1326
+ expect(result).toContain("middle\n");
1327
+ expect(result).toContain("line-c\n");
1328
+ expect(result).toContain("line-d\n");
1329
+ expect(result).not.toContain("<<<<<<<");
1330
+ });
1331
+ });
1332
+
1333
+ describe("merge=union gitattribute support", () => {
1334
+ test("union strategy preserves lines from both sides (git handles cleanly or via auto-resolve)", async () => {
1335
+ const repoDir = await createTempGitRepo();
1336
+ try {
1337
+ const defaultBranch = await getDefaultBranch(repoDir);
1338
+
1339
+ // Set up .gitattributes with merge=union for *.jsonl files so that
1340
+ // both git's built-in union driver AND overstory's Tier 2 union path
1341
+ // are configured to keep all lines from both sides.
1342
+ await commitFile(repoDir, ".gitattributes", "*.jsonl merge=union\n");
1343
+ // Common ancestor: one line
1344
+ await commitFile(repoDir, "data.jsonl", '{"id":"a"}\n');
1345
+
1346
+ // Feature branch: adds line b
1347
+ await runGitInDir(repoDir, ["checkout", "-b", "feature-branch"]);
1348
+ await commitFile(repoDir, "data.jsonl", '{"id":"a"}\n{"id":"b"}\n');
1349
+
1350
+ // Back to main: adds line c (diverges from ancestor)
1351
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
1352
+ await commitFile(repoDir, "data.jsonl", '{"id":"a"}\n{"id":"c"}\n');
1353
+
1354
+ const entry = makeTestEntry({
1355
+ branchName: "feature-branch",
1356
+ filesModified: ["data.jsonl"],
1357
+ });
1358
+
1359
+ const resolver = createMergeResolver({
1360
+ aiResolveEnabled: false,
1361
+ reimagineEnabled: false,
1362
+ });
1363
+
1364
+ const result = await resolver.resolve(entry, defaultBranch, repoDir);
1365
+
1366
+ // With merge=union, git either resolves cleanly (Tier 1) or
1367
+ // overstory's union path handles it (Tier 2). Either way, success
1368
+ // and both sides' content must be preserved.
1369
+ expect(result.success).toBe(true);
1370
+ expect(result.entry.status).toBe("merged");
1371
+
1372
+ // Both sides' lines must be present — no lines dropped
1373
+ const file = Bun.file(join(repoDir, "data.jsonl"));
1374
+ const content = await file.text();
1375
+ expect(content).toContain('{"id":"a"}');
1376
+ expect(content).toContain('{"id":"b"}');
1377
+ expect(content).toContain('{"id":"c"}');
1378
+ } finally {
1379
+ await cleanupTempDir(repoDir);
1380
+ }
1381
+ });
1382
+
1383
+ test("Tier 2 union auto-resolve keeps both sides when git produces conflict markers", async () => {
1384
+ // This test verifies the Tier 2 code path: when git produces conflict
1385
+ // markers for a file that has merge=union set in .gitattributes,
1386
+ // overstory resolves it by keeping both canonical and incoming content.
1387
+ // We produce conflict markers by doing a content conflict on a file whose
1388
+ // attribute is set to merge=union AFTER the conflict state exists, then
1389
+ // run only the auto-resolve path via a standalone resolver call.
1390
+ //
1391
+ // To force conflict markers despite merge=union: we DON'T commit
1392
+ // .gitattributes before the merge, so git uses the default driver and
1393
+ // produces conflict markers. Then we write .gitattributes to the working
1394
+ // tree (not committed) so git check-attr sees it and our code detects union.
1395
+ const repoDir = await createTempGitRepo();
1396
+ try {
1397
+ const defaultBranch = await getDefaultBranch(repoDir);
1398
+
1399
+ // Common ancestor
1400
+ await commitFile(repoDir, "data.jsonl", '{"id":"a"}\n');
1401
+
1402
+ // Feature branch: adds line b (also appends to same position as main)
1403
+ await runGitInDir(repoDir, ["checkout", "-b", "feature-branch"]);
1404
+ await commitFile(repoDir, "data.jsonl", '{"id":"a"}\n{"id":"b"}\n');
1405
+
1406
+ // Back to main: adds line c — diverges from ancestor at same position
1407
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
1408
+ await commitFile(repoDir, "data.jsonl", '{"id":"a"}\n{"id":"c"}\n');
1409
+
1410
+ // Now WRITE .gitattributes to working tree (not committed).
1411
+ // git check-attr reads from the working tree during Tier 2.
1412
+ await Bun.write(`${repoDir}/.gitattributes`, "*.jsonl merge=union\n");
1413
+
1414
+ // Attempt merge — git uses default driver (no committed .gitattributes)
1415
+ // so it WILL produce conflict markers if branches diverge at same position.
1416
+ // (If git resolves cleanly, the test still passes because content is preserved.)
1417
+ const entry = makeTestEntry({
1418
+ branchName: "feature-branch",
1419
+ filesModified: ["data.jsonl"],
1420
+ });
1421
+
1422
+ const resolver = createMergeResolver({
1423
+ aiResolveEnabled: false,
1424
+ reimagineEnabled: false,
1425
+ });
1426
+
1427
+ const result = await resolver.resolve(entry, defaultBranch, repoDir);
1428
+
1429
+ expect(result.success).toBe(true);
1430
+ expect(result.entry.status).toBe("merged");
1431
+
1432
+ const file = Bun.file(join(repoDir, "data.jsonl"));
1433
+ const content = await file.text();
1434
+ expect(content).toContain('{"id":"a"}');
1435
+ expect(content).toContain('{"id":"b"}');
1436
+ expect(content).toContain('{"id":"c"}');
1437
+ } finally {
1438
+ await cleanupTempDir(repoDir);
1439
+ }
1440
+ });
1441
+ });
1442
+
1291
1443
  describe("AI-resolve with history context", () => {
1292
1444
  test("includes historical context in AI prompt when available", async () => {
1293
1445
  const repoDir = await createTempGitRepo();
@@ -89,6 +89,47 @@ function resolveConflictsKeepIncoming(content: string): string | null {
89
89
  });
90
90
  }
91
91
 
92
+ /**
93
+ * Parse conflict markers in file content and keep ALL lines from both sides.
94
+ * Used when the file has `merge=union` gitattribute — dedup-on-read handles duplicates.
95
+ *
96
+ * A conflict block looks like:
97
+ * ```
98
+ * <<<<<<< HEAD
99
+ * canonical content
100
+ * =======
101
+ * incoming content
102
+ * >>>>>>> branch
103
+ * ```
104
+ *
105
+ * This function replaces each conflict block with canonical + incoming content concatenated.
106
+ * Returns the resolved content, or null if no conflict markers were found.
107
+ */
108
+ export function resolveConflictsUnion(content: string): string | null {
109
+ const conflictPattern = /^<{7} .+\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} .+\n?/gm;
110
+
111
+ if (!conflictPattern.test(content)) {
112
+ return null;
113
+ }
114
+
115
+ // Reset regex lastIndex after test()
116
+ conflictPattern.lastIndex = 0;
117
+
118
+ return content.replace(conflictPattern, (_match, canonical: string, incoming: string) => {
119
+ return canonical + incoming;
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Check if a file has the `merge=union` gitattribute set.
125
+ * Returns true if `git check-attr merge -- <file>` ends with ": merge: union".
126
+ */
127
+ async function checkMergeUnion(repoRoot: string, filePath: string): Promise<boolean> {
128
+ const { stdout, exitCode } = await runGit(repoRoot, ["check-attr", "merge", "--", filePath]);
129
+ if (exitCode !== 0) return false;
130
+ return stdout.trim().endsWith(": merge: union");
131
+ }
132
+
92
133
  /**
93
134
  * Read a file's content using Bun.file().
94
135
  */
@@ -138,7 +179,10 @@ async function tryAutoResolve(
138
179
 
139
180
  try {
140
181
  const content = await readFile(filePath);
141
- const resolved = resolveConflictsKeepIncoming(content);
182
+ const isUnion = await checkMergeUnion(repoRoot, file);
183
+ const resolved = isUnion
184
+ ? resolveConflictsUnion(content)
185
+ : resolveConflictsKeepIncoming(content);
142
186
 
143
187
  if (resolved === null) {
144
188
  // No conflict markers found (shouldn't happen but be defensive)
@@ -496,7 +540,7 @@ function recordConflictPattern(
496
540
  type: "pattern",
497
541
  description,
498
542
  tags: ["merge-conflict"],
499
- evidenceBead: entry.beadId,
543
+ evidenceBead: entry.taskId,
500
544
  })
501
545
  .catch(() => {});
502
546
  }
@@ -32,7 +32,7 @@ afterEach(async () => {
32
32
  function makeSession(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
33
33
  return {
34
34
  agentName: "test-agent",
35
- beadId: "test-task-123",
35
+ taskId: "test-task-123",
36
36
  capability: "builder",
37
37
  startedAt: new Date("2026-01-01T00:00:00Z").toISOString(),
38
38
  completedAt: new Date("2026-01-01T00:05:00Z").toISOString(),
@@ -78,7 +78,7 @@ describe("recordSession", () => {
78
78
  test("all fields roundtrip correctly (camelCase TS → snake_case SQLite → camelCase TS)", () => {
79
79
  const session = makeSession({
80
80
  agentName: "special-agent",
81
- beadId: "task-xyz",
81
+ taskId: "task-xyz",
82
82
  capability: "reviewer",
83
83
  startedAt: "2026-02-01T12:00:00Z",
84
84
  completedAt: "2026-02-01T12:30:00Z",
@@ -119,15 +119,15 @@ describe("recordSession", () => {
119
119
  describe("getRecentSessions", () => {
120
120
  test("returns sessions ordered by started_at DESC (most recent first)", () => {
121
121
  const session1 = makeSession({
122
- beadId: "task-1",
122
+ taskId: "task-1",
123
123
  startedAt: "2026-01-01T10:00:00Z",
124
124
  });
125
125
  const session2 = makeSession({
126
- beadId: "task-2",
126
+ taskId: "task-2",
127
127
  startedAt: "2026-01-01T12:00:00Z",
128
128
  });
129
129
  const session3 = makeSession({
130
- beadId: "task-3",
130
+ taskId: "task-3",
131
131
  startedAt: "2026-01-01T11:00:00Z",
132
132
  });
133
133
 
@@ -137,9 +137,9 @@ describe("getRecentSessions", () => {
137
137
 
138
138
  const retrieved = store.getRecentSessions(10);
139
139
  expect(retrieved).toHaveLength(3);
140
- expect(retrieved[0]?.beadId).toBe("task-2"); // most recent
141
- expect(retrieved[1]?.beadId).toBe("task-3");
142
- expect(retrieved[2]?.beadId).toBe("task-1"); // oldest
140
+ expect(retrieved[0]?.taskId).toBe("task-2"); // most recent
141
+ expect(retrieved[1]?.taskId).toBe("task-3");
142
+ expect(retrieved[2]?.taskId).toBe("task-1"); // oldest
143
143
  });
144
144
 
145
145
  test("default limit is 20", () => {
@@ -147,7 +147,7 @@ describe("getRecentSessions", () => {
147
147
  for (let i = 0; i < 25; i++) {
148
148
  store.recordSession(
149
149
  makeSession({
150
- beadId: `task-${i}`,
150
+ taskId: `task-${i}`,
151
151
  startedAt: new Date(Date.now() + i * 1000).toISOString(),
152
152
  }),
153
153
  );
@@ -158,9 +158,9 @@ describe("getRecentSessions", () => {
158
158
  });
159
159
 
160
160
  test("custom limit works (e.g., limit=2 returns only 2)", () => {
161
- store.recordSession(makeSession({ beadId: "task-1" }));
162
- store.recordSession(makeSession({ beadId: "task-2" }));
163
- store.recordSession(makeSession({ beadId: "task-3" }));
161
+ store.recordSession(makeSession({ taskId: "task-1" }));
162
+ store.recordSession(makeSession({ taskId: "task-2" }));
163
+ store.recordSession(makeSession({ taskId: "task-3" }));
164
164
 
165
165
  const retrieved = store.getRecentSessions(2);
166
166
  expect(retrieved).toHaveLength(2);
@@ -176,9 +176,9 @@ describe("getRecentSessions", () => {
176
176
 
177
177
  describe("getSessionsByAgent", () => {
178
178
  test("filters by agent name correctly", () => {
179
- store.recordSession(makeSession({ agentName: "agent-a", beadId: "task-1" }));
180
- store.recordSession(makeSession({ agentName: "agent-b", beadId: "task-2" }));
181
- store.recordSession(makeSession({ agentName: "agent-a", beadId: "task-3" }));
179
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-1" }));
180
+ store.recordSession(makeSession({ agentName: "agent-b", taskId: "task-2" }));
181
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-3" }));
182
182
 
183
183
  const retrieved = store.getSessionsByAgent("agent-a");
184
184
  expect(retrieved).toHaveLength(2);
@@ -197,30 +197,30 @@ describe("getSessionsByAgent", () => {
197
197
  store.recordSession(
198
198
  makeSession({
199
199
  agentName: "agent-x",
200
- beadId: "task-1",
200
+ taskId: "task-1",
201
201
  startedAt: "2026-01-01T10:00:00Z",
202
202
  }),
203
203
  );
204
204
  store.recordSession(
205
205
  makeSession({
206
206
  agentName: "agent-x",
207
- beadId: "task-2",
207
+ taskId: "task-2",
208
208
  startedAt: "2026-01-01T12:00:00Z",
209
209
  }),
210
210
  );
211
211
  store.recordSession(
212
212
  makeSession({
213
213
  agentName: "agent-x",
214
- beadId: "task-3",
214
+ taskId: "task-3",
215
215
  startedAt: "2026-01-01T11:00:00Z",
216
216
  }),
217
217
  );
218
218
 
219
219
  const retrieved = store.getSessionsByAgent("agent-x");
220
220
  expect(retrieved).toHaveLength(3);
221
- expect(retrieved[0]?.beadId).toBe("task-2"); // most recent
222
- expect(retrieved[1]?.beadId).toBe("task-3");
223
- expect(retrieved[2]?.beadId).toBe("task-1"); // oldest
221
+ expect(retrieved[0]?.taskId).toBe("task-2"); // most recent
222
+ expect(retrieved[1]?.taskId).toBe("task-3");
223
+ expect(retrieved[2]?.taskId).toBe("task-1"); // oldest
224
224
  });
225
225
  });
226
226
 
@@ -228,9 +228,9 @@ describe("getSessionsByAgent", () => {
228
228
 
229
229
  describe("getAverageDuration", () => {
230
230
  test("average across all completed sessions (completedAt IS NOT NULL)", () => {
231
- store.recordSession(makeSession({ beadId: "task-1", durationMs: 100_000 }));
232
- store.recordSession(makeSession({ beadId: "task-2", durationMs: 200_000 }));
233
- store.recordSession(makeSession({ beadId: "task-3", durationMs: 300_000 }));
231
+ store.recordSession(makeSession({ taskId: "task-1", durationMs: 100_000 }));
232
+ store.recordSession(makeSession({ taskId: "task-2", durationMs: 200_000 }));
233
+ store.recordSession(makeSession({ taskId: "task-3", durationMs: 300_000 }));
234
234
 
235
235
  const avg = store.getAverageDuration();
236
236
  expect(avg).toBe(200_000);
@@ -238,11 +238,11 @@ describe("getAverageDuration", () => {
238
238
 
239
239
  test("average filtered by capability", () => {
240
240
  store.recordSession(
241
- makeSession({ beadId: "task-1", capability: "builder", durationMs: 100_000 }),
241
+ makeSession({ taskId: "task-1", capability: "builder", durationMs: 100_000 }),
242
242
  );
243
- store.recordSession(makeSession({ beadId: "task-2", capability: "scout", durationMs: 50_000 }));
243
+ store.recordSession(makeSession({ taskId: "task-2", capability: "scout", durationMs: 50_000 }));
244
244
  store.recordSession(
245
- makeSession({ beadId: "task-3", capability: "builder", durationMs: 200_000 }),
245
+ makeSession({ taskId: "task-3", capability: "builder", durationMs: 200_000 }),
246
246
  );
247
247
 
248
248
  const avgBuilder = store.getAverageDuration("builder");
@@ -258,9 +258,9 @@ describe("getAverageDuration", () => {
258
258
  });
259
259
 
260
260
  test("sessions with completedAt=null are excluded from average", () => {
261
- store.recordSession(makeSession({ beadId: "task-1", durationMs: 100_000, completedAt: null }));
262
- store.recordSession(makeSession({ beadId: "task-2", durationMs: 200_000 }));
263
- store.recordSession(makeSession({ beadId: "task-3", durationMs: 300_000 }));
261
+ store.recordSession(makeSession({ taskId: "task-1", durationMs: 100_000, completedAt: null }));
262
+ store.recordSession(makeSession({ taskId: "task-2", durationMs: 200_000 }));
263
+ store.recordSession(makeSession({ taskId: "task-3", durationMs: 300_000 }));
264
264
 
265
265
  const avg = store.getAverageDuration();
266
266
  expect(avg).toBe(250_000); // (200_000 + 300_000) / 2
@@ -359,7 +359,7 @@ describe("token fields", () => {
359
359
  store.recordSession(
360
360
  makeSession({
361
361
  agentName: "new-agent",
362
- beadId: "new-task",
362
+ taskId: "new-task",
363
363
  inputTokens: 5000,
364
364
  outputTokens: 1000,
365
365
  estimatedCostUsd: 0.42,
@@ -378,9 +378,9 @@ describe("token fields", () => {
378
378
 
379
379
  describe("getSessionsByRun", () => {
380
380
  test("returns sessions matching run_id", () => {
381
- store.recordSession(makeSession({ agentName: "a1", beadId: "t1", runId: "run-001" }));
382
- store.recordSession(makeSession({ agentName: "a2", beadId: "t2", runId: "run-001" }));
383
- store.recordSession(makeSession({ agentName: "a3", beadId: "t3", runId: "run-002" }));
381
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1", runId: "run-001" }));
382
+ store.recordSession(makeSession({ agentName: "a2", taskId: "t2", runId: "run-001" }));
383
+ store.recordSession(makeSession({ agentName: "a3", taskId: "t3", runId: "run-002" }));
384
384
 
385
385
  const sessions = store.getSessionsByRun("run-001");
386
386
  expect(sessions).toHaveLength(2);
@@ -388,13 +388,13 @@ describe("getSessionsByRun", () => {
388
388
  });
389
389
 
390
390
  test("returns empty array for unknown run_id", () => {
391
- store.recordSession(makeSession({ agentName: "a1", beadId: "t1", runId: "run-001" }));
391
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1", runId: "run-001" }));
392
392
  expect(store.getSessionsByRun("run-nonexistent")).toEqual([]);
393
393
  });
394
394
 
395
395
  test("sessions with null run_id are not returned", () => {
396
- store.recordSession(makeSession({ agentName: "a1", beadId: "t1", runId: null }));
397
- store.recordSession(makeSession({ agentName: "a2", beadId: "t2", runId: "run-001" }));
396
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1", runId: null }));
397
+ store.recordSession(makeSession({ agentName: "a2", taskId: "t2", runId: "run-001" }));
398
398
  expect(store.getSessionsByRun("run-001")).toHaveLength(1);
399
399
  });
400
400
  });
@@ -403,9 +403,9 @@ describe("getSessionsByRun", () => {
403
403
 
404
404
  describe("purge", () => {
405
405
  test("purge all deletes everything and returns count", () => {
406
- store.recordSession(makeSession({ agentName: "agent-a", beadId: "task-1" }));
407
- store.recordSession(makeSession({ agentName: "agent-b", beadId: "task-2" }));
408
- store.recordSession(makeSession({ agentName: "agent-c", beadId: "task-3" }));
406
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-1" }));
407
+ store.recordSession(makeSession({ agentName: "agent-b", taskId: "task-2" }));
408
+ store.recordSession(makeSession({ agentName: "agent-c", taskId: "task-3" }));
409
409
 
410
410
  const count = store.purge({ all: true });
411
411
  expect(count).toBe(3);
@@ -413,9 +413,9 @@ describe("purge", () => {
413
413
  });
414
414
 
415
415
  test("purge by agent deletes only that agent's records", () => {
416
- store.recordSession(makeSession({ agentName: "agent-a", beadId: "task-1" }));
417
- store.recordSession(makeSession({ agentName: "agent-b", beadId: "task-2" }));
418
- store.recordSession(makeSession({ agentName: "agent-a", beadId: "task-3" }));
416
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-1" }));
417
+ store.recordSession(makeSession({ agentName: "agent-b", taskId: "task-2" }));
418
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-3" }));
419
419
 
420
420
  const count = store.purge({ agent: "agent-a" });
421
421
  expect(count).toBe(2);
@@ -431,7 +431,7 @@ describe("purge", () => {
431
431
  });
432
432
 
433
433
  test("purge with no options returns 0 without deleting", () => {
434
- store.recordSession(makeSession({ beadId: "task-1" }));
434
+ store.recordSession(makeSession({ taskId: "task-1" }));
435
435
 
436
436
  const count = store.purge({});
437
437
  expect(count).toBe(0);
@@ -152,7 +152,7 @@ function migrateTokenColumns(db: Database): void {
152
152
  function rowToMetrics(row: SessionRow): SessionMetrics {
153
153
  return {
154
154
  agentName: row.agent_name,
155
- beadId: row.task_id,
155
+ taskId: row.task_id,
156
156
  capability: row.capability,
157
157
  startedAt: row.started_at,
158
158
  completedAt: row.completed_at,
@@ -303,7 +303,7 @@ export function createMetricsStore(dbPath: string): MetricsStore {
303
303
  recordSession(metrics: SessionMetrics): void {
304
304
  insertStmt.run({
305
305
  $agent_name: metrics.agentName,
306
- $task_id: metrics.beadId,
306
+ $task_id: metrics.taskId,
307
307
  $capability: metrics.capability,
308
308
  $started_at: metrics.startedAt,
309
309
  $completed_at: metrics.completedAt,