@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
@@ -106,6 +106,7 @@ const DANGEROUS_BASH_PATTERNS = [
106
106
  * This whitelist is checked BEFORE the blocklist.
107
107
  */
108
108
  const SAFE_BASH_PREFIXES = [
109
+ "ov ",
109
110
  "overstory ",
110
111
  "bd ",
111
112
  "sd ",
@@ -205,6 +206,20 @@ export function getPathBoundaryGuards(): HookEntry[] {
205
206
  ];
206
207
  }
207
208
 
209
+ /**
210
+ * Escape a string for use inside a single-quoted POSIX shell string.
211
+ *
212
+ * POSIX single-quoted strings cannot contain single quotes at all.
213
+ * The standard technique is to end the single-quoted segment, emit an escaped
214
+ * single quote using $'\'', then start a new single-quoted segment:
215
+ * 'it'\''s fine' → it's fine
216
+ *
217
+ * Exported so tests can verify escaping directly.
218
+ */
219
+ export function escapeForSingleQuotedShell(str: string): string {
220
+ return str.replace(/'/g, "'\\''");
221
+ }
222
+
208
223
  /**
209
224
  * Build a PreToolUse guard that blocks a specific tool.
210
225
  *
@@ -218,7 +233,7 @@ function blockGuard(toolName: string, reason: string): HookEntry {
218
233
  hooks: [
219
234
  {
220
235
  type: "command",
221
- command: `${ENV_GUARD} echo '${response}'`,
236
+ command: `${ENV_GUARD} echo '${escapeForSingleQuotedShell(response)}'`,
222
237
  },
223
238
  ],
224
239
  };
@@ -242,7 +257,7 @@ function buildBashGuardScript(agentName: string): string {
242
257
  'CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\');',
243
258
  // Check 1: Block all git push — agents must never push to remote
244
259
  "if echo \"$CMD\" | grep -qE '\\bgit\\s+push\\b'; then",
245
- ' echo \'{"decision":"block","reason":"git push is blocked — use overstory merge to integrate changes, push manually when ready"}\';',
260
+ ' echo \'{"decision":"block","reason":"git push is blocked — use ov merge to integrate changes, push manually when ready"}\';',
246
261
  " exit 0;",
247
262
  "fi;",
248
263
  // Check 2: Block git reset --hard
@@ -447,7 +462,7 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
447
462
  const teamToolGuards = NATIVE_TEAM_TOOLS.map((tool) =>
448
463
  blockGuard(
449
464
  tool,
450
- `Overstory agents must use 'overstory sling' for delegation — ${tool} is not allowed`,
465
+ `Overstory agents must use 'ov sling' for delegation — ${tool} is not allowed`,
451
466
  ),
452
467
  );
453
468
  guards.push(...teamToolGuards);
@@ -458,7 +473,7 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
458
473
  const interactiveGuards = INTERACTIVE_TOOLS.map((tool) =>
459
474
  blockGuard(
460
475
  tool,
461
- `${tool} requires human interaction -- agents run non-interactively. Use overstory mail (--type question) to escalate`,
476
+ `${tool} requires human interaction -- agents run non-interactively. Use ov mail (--type question) to escalate`,
462
477
  ),
463
478
  );
464
479
  guards.push(...interactiveGuards);
@@ -495,13 +510,16 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
495
510
  /**
496
511
  * Check whether a hook entry is overstory-managed.
497
512
  *
498
- * Overstory hook commands always reference either `overstory` (CLI commands)
513
+ * Overstory hook commands always reference either `ov ` / `overstory` (CLI commands)
499
514
  * or `OVERSTORY_` (env var guards like OVERSTORY_AGENT_NAME, OVERSTORY_WORKTREE_PATH).
500
515
  * User hooks will not contain these patterns.
501
516
  */
502
517
  export function isOverstoryHookEntry(entry: HookEntry): boolean {
503
518
  return entry.hooks.some(
504
- (h) => h.command.includes("overstory") || h.command.includes("OVERSTORY_"),
519
+ (h) =>
520
+ h.command.includes("ov ") ||
521
+ h.command.includes("overstory") ||
522
+ h.command.includes("OVERSTORY_"),
505
523
  );
506
524
  }
507
525
 
@@ -72,12 +72,12 @@ describe("identity", () => {
72
72
  expertiseDomains: [],
73
73
  recentTasks: [
74
74
  {
75
- beadId: "beads-001",
75
+ taskId: "beads-001",
76
76
  summary: "Fixed authentication bug",
77
77
  completedAt: "2024-01-15T12:00:00Z",
78
78
  },
79
79
  {
80
- beadId: "beads-002",
80
+ taskId: "beads-002",
81
81
  summary: "Added user profile page",
82
82
  completedAt: "2024-01-16T14:30:00Z",
83
83
  },
@@ -89,10 +89,10 @@ describe("identity", () => {
89
89
  const filePath = join(tempDir, "test-agent", "identity.yaml");
90
90
  const content = await Bun.file(filePath).text();
91
91
  expect(content).toContain("recentTasks:");
92
- expect(content).toContain("\t- beadId: beads-001");
92
+ expect(content).toContain("\t- taskId: beads-001");
93
93
  expect(content).toContain("\t\tsummary: Fixed authentication bug");
94
94
  expect(content).toContain('\t\tcompletedAt: "2024-01-15T12:00:00Z"');
95
- expect(content).toContain("\t- beadId: beads-002");
95
+ expect(content).toContain("\t- taskId: beads-002");
96
96
  expect(content).toContain("\t\tsummary: Added user profile page");
97
97
  expect(content).toContain('\t\tcompletedAt: "2024-01-16T14:30:00Z"');
98
98
  });
@@ -106,7 +106,7 @@ describe("identity", () => {
106
106
  expertiseDomains: ["domain: with colon", "domain#with hash", " leading space"],
107
107
  recentTasks: [
108
108
  {
109
- beadId: "beads-001",
109
+ taskId: "beads-001",
110
110
  summary: 'Fixed bug: "memory leak"',
111
111
  completedAt: "2024-01-15T12:00:00Z",
112
112
  },
@@ -198,7 +198,7 @@ describe("identity", () => {
198
198
  expertiseDomains: ["typescript", "testing"],
199
199
  recentTasks: [
200
200
  {
201
- beadId: "beads-001",
201
+ taskId: "beads-001",
202
202
  summary: "Fixed bug",
203
203
  completedAt: "2024-01-15T12:00:00Z",
204
204
  },
@@ -216,7 +216,7 @@ describe("identity", () => {
216
216
  expect(loaded?.sessionsCompleted).toBe(7);
217
217
  expect(loaded?.expertiseDomains).toEqual(["typescript", "testing"]);
218
218
  expect(loaded?.recentTasks).toHaveLength(1);
219
- expect(loaded?.recentTasks[0]?.beadId).toBe("beads-001");
219
+ expect(loaded?.recentTasks[0]?.taskId).toBe("beads-001");
220
220
  expect(loaded?.recentTasks[0]?.summary).toBe("Fixed bug");
221
221
  expect(loaded?.recentTasks[0]?.completedAt).toBe("2024-01-15T12:00:00Z");
222
222
  });
@@ -252,17 +252,17 @@ describe("identity", () => {
252
252
  expertiseDomains: [],
253
253
  recentTasks: [
254
254
  {
255
- beadId: "beads-001",
255
+ taskId: "beads-001",
256
256
  summary: "Task 1",
257
257
  completedAt: "2024-01-15T12:00:00Z",
258
258
  },
259
259
  {
260
- beadId: "beads-002",
260
+ taskId: "beads-002",
261
261
  summary: "Task 2",
262
262
  completedAt: "2024-01-16T12:00:00Z",
263
263
  },
264
264
  {
265
- beadId: "beads-003",
265
+ taskId: "beads-003",
266
266
  summary: "Task 3",
267
267
  completedAt: "2024-01-17T12:00:00Z",
268
268
  },
@@ -273,9 +273,9 @@ describe("identity", () => {
273
273
  const loaded = await loadIdentity(tempDir, "test-agent");
274
274
 
275
275
  expect(loaded?.recentTasks).toHaveLength(3);
276
- expect(loaded?.recentTasks[0]?.beadId).toBe("beads-001");
277
- expect(loaded?.recentTasks[1]?.beadId).toBe("beads-002");
278
- expect(loaded?.recentTasks[2]?.beadId).toBe("beads-003");
276
+ expect(loaded?.recentTasks[0]?.taskId).toBe("beads-001");
277
+ expect(loaded?.recentTasks[1]?.taskId).toBe("beads-002");
278
+ expect(loaded?.recentTasks[2]?.taskId).toBe("beads-003");
279
279
  });
280
280
 
281
281
  test("handles quoted strings with special characters", async () => {
@@ -287,7 +287,7 @@ describe("identity", () => {
287
287
  expertiseDomains: ["domain: with colon", "domain#with hash"],
288
288
  recentTasks: [
289
289
  {
290
- beadId: "beads-001",
290
+ taskId: "beads-001",
291
291
  summary: 'Fixed bug: "memory leak"',
292
292
  completedAt: "2024-01-15T12:00:00Z",
293
293
  },
@@ -311,7 +311,7 @@ describe("identity", () => {
311
311
  expertiseDomains: [],
312
312
  recentTasks: [
313
313
  {
314
- beadId: "beads-001",
314
+ taskId: "beads-001",
315
315
  summary: "Path: C:\\Users\\test\\file.txt",
316
316
  completedAt: "2024-01-15T12:00:00Z",
317
317
  },
@@ -435,14 +435,14 @@ recentTasks: []
435
435
  const beforeUpdate = Date.now();
436
436
  const updated = await updateIdentity(tempDir, "test-agent", {
437
437
  completedTask: {
438
- beadId: "beads-001",
438
+ taskId: "beads-001",
439
439
  summary: "Fixed authentication bug",
440
440
  },
441
441
  });
442
442
  const afterUpdate = Date.now();
443
443
 
444
444
  expect(updated.recentTasks).toHaveLength(1);
445
- expect(updated.recentTasks[0]?.beadId).toBe("beads-001");
445
+ expect(updated.recentTasks[0]?.taskId).toBe("beads-001");
446
446
  expect(updated.recentTasks[0]?.summary).toBe("Fixed authentication bug");
447
447
 
448
448
  // Verify timestamp is within the update window
@@ -454,7 +454,7 @@ recentTasks: []
454
454
  test("caps recentTasks at 20 entries, dropping oldest", async () => {
455
455
  // Create identity with 19 tasks
456
456
  const existingTasks = Array.from({ length: 19 }, (_, i) => ({
457
- beadId: `beads-${i.toString().padStart(3, "0")}`,
457
+ taskId: `beads-${i.toString().padStart(3, "0")}`,
458
458
  summary: `Task ${i}`,
459
459
  completedAt: `2024-01-${(i + 1).toString().padStart(2, "0")}T12:00:00Z`,
460
460
  }));
@@ -472,20 +472,20 @@ recentTasks: []
472
472
 
473
473
  // Add two more tasks (total would be 21)
474
474
  let updated = await updateIdentity(tempDir, "test-agent", {
475
- completedTask: { beadId: "beads-019", summary: "Task 19" },
475
+ completedTask: { taskId: "beads-019", summary: "Task 19" },
476
476
  });
477
477
 
478
478
  expect(updated.recentTasks).toHaveLength(20);
479
- expect(updated.recentTasks[0]?.beadId).toBe("beads-000");
479
+ expect(updated.recentTasks[0]?.taskId).toBe("beads-000");
480
480
 
481
481
  updated = await updateIdentity(tempDir, "test-agent", {
482
- completedTask: { beadId: "beads-020", summary: "Task 20" },
482
+ completedTask: { taskId: "beads-020", summary: "Task 20" },
483
483
  });
484
484
 
485
485
  expect(updated.recentTasks).toHaveLength(20);
486
486
  // Oldest task (beads-000) should be dropped
487
- expect(updated.recentTasks[0]?.beadId).toBe("beads-001");
488
- expect(updated.recentTasks[19]?.beadId).toBe("beads-020");
487
+ expect(updated.recentTasks[0]?.taskId).toBe("beads-001");
488
+ expect(updated.recentTasks[19]?.taskId).toBe("beads-020");
489
489
  });
490
490
 
491
491
  test("applies multiple updates simultaneously", async () => {
@@ -503,7 +503,7 @@ recentTasks: []
503
503
  sessionsCompleted: 2,
504
504
  expertiseDomains: ["testing", "architecture"],
505
505
  completedTask: {
506
- beadId: "beads-001",
506
+ taskId: "beads-001",
507
507
  summary: "Completed task",
508
508
  },
509
509
  });
@@ -554,12 +554,12 @@ recentTasks: []
554
554
  expertiseDomains: ["typescript", "testing", "architecture"],
555
555
  recentTasks: [
556
556
  {
557
- beadId: "beads-001",
557
+ taskId: "beads-001",
558
558
  summary: "Implemented feature X",
559
559
  completedAt: "2024-01-15T12:00:00Z",
560
560
  },
561
561
  {
562
- beadId: "beads-002",
562
+ taskId: "beads-002",
563
563
  summary: "Fixed bug in module Y",
564
564
  completedAt: "2024-01-16T14:30:00Z",
565
565
  },
@@ -587,7 +587,7 @@ recentTasks: []
587
587
  ],
588
588
  recentTasks: [
589
589
  {
590
- beadId: "beads-001",
590
+ taskId: "beads-001",
591
591
  summary: 'Summary with "quotes" and: colons',
592
592
  completedAt: "2024-01-15T12:00:00Z",
593
593
  },
@@ -39,7 +39,7 @@ function serializeIdentityYaml(identity: AgentIdentity): string {
39
39
  } else {
40
40
  lines.push("recentTasks:");
41
41
  for (const task of identity.recentTasks) {
42
- lines.push(`\t- beadId: ${quoteIfNeeded(task.beadId)}`);
42
+ lines.push(`\t- taskId: ${quoteIfNeeded(task.taskId)}`);
43
43
  lines.push(`\t\tsummary: ${quoteIfNeeded(task.summary)}`);
44
44
  lines.push(`\t\tcompletedAt: ${quoteIfNeeded(task.completedAt)}`);
45
45
  }
@@ -82,7 +82,7 @@ function quoteIfNeeded(value: string): string {
82
82
  * This is a purpose-built parser for the identity YAML format. It handles:
83
83
  * - Simple key: value pairs (strings, numbers)
84
84
  * - Arrays of scalars (expertiseDomains)
85
- * - Arrays of objects (recentTasks with beadId, summary, completedAt)
85
+ * - Arrays of objects (recentTasks with taskId, summary, completedAt)
86
86
  * - Empty arrays (`[]`)
87
87
  * - Quoted strings
88
88
  * - Tab indentation
@@ -95,10 +95,10 @@ function parseIdentityYaml(text: string): AgentIdentity {
95
95
  let created = "";
96
96
  let sessionsCompleted = 0;
97
97
  const expertiseDomains: string[] = [];
98
- const recentTasks: Array<{ beadId: string; summary: string; completedAt: string }> = [];
98
+ const recentTasks: Array<{ taskId: string; summary: string; completedAt: string }> = [];
99
99
 
100
100
  let currentSection: "none" | "expertiseDomains" | "recentTasks" = "none";
101
- let currentTask: { beadId: string; summary: string; completedAt: string } | null = null;
101
+ let currentTask: { taskId: string; summary: string; completedAt: string } | null = null;
102
102
 
103
103
  for (const rawLine of lines) {
104
104
  const trimmed = rawLine.trim();
@@ -169,7 +169,7 @@ function parseIdentityYaml(text: string): AgentIdentity {
169
169
  if (currentTask !== null) {
170
170
  recentTasks.push(currentTask);
171
171
  }
172
- currentTask = { beadId: "", summary: "", completedAt: "" };
172
+ currentTask = { taskId: "", summary: "", completedAt: "" };
173
173
 
174
174
  // Parse the key-value on the same line as the dash
175
175
  const itemContent = trimmed.slice(2).trim();
@@ -210,13 +210,13 @@ function parseIdentityYaml(text: string): AgentIdentity {
210
210
  * Assign a parsed field value to a task object by key name.
211
211
  */
212
212
  function assignTaskField(
213
- task: { beadId: string; summary: string; completedAt: string },
213
+ task: { taskId: string; summary: string; completedAt: string },
214
214
  key: string,
215
215
  value: string,
216
216
  ): void {
217
217
  switch (key) {
218
- case "beadId":
219
- task.beadId = value;
218
+ case "taskId":
219
+ task.taskId = value;
220
220
  break;
221
221
  case "summary":
222
222
  task.summary = value;
@@ -336,7 +336,7 @@ export async function updateIdentity(
336
336
  baseDir: string,
337
337
  name: string,
338
338
  update: Partial<Pick<AgentIdentity, "sessionsCompleted" | "expertiseDomains">> & {
339
- completedTask?: { beadId: string; summary: string };
339
+ completedTask?: { taskId: string; summary: string };
340
340
  },
341
341
  ): Promise<AgentIdentity> {
342
342
  const identity = await loadIdentity(baseDir, name);
@@ -364,7 +364,7 @@ export async function updateIdentity(
364
364
  // Append completed task
365
365
  if (update.completedTask !== undefined) {
366
366
  identity.recentTasks.push({
367
- beadId: update.completedTask.beadId,
367
+ taskId: update.completedTask.taskId,
368
368
  summary: update.completedTask.summary,
369
369
  completedAt: new Date().toISOString(),
370
370
  });
@@ -23,7 +23,7 @@ describe("lifecycle", () => {
23
23
  agentsDir,
24
24
  agentName: "builder-1",
25
25
  sessionId: "session-100",
26
- beadId: "overstory-xyz1",
26
+ taskId: "overstory-xyz1",
27
27
  reason: "compaction",
28
28
  progressSummary: "Built the widget",
29
29
  pendingWork: "Tests remain",
@@ -56,7 +56,7 @@ describe("lifecycle", () => {
56
56
  agentsDir,
57
57
  agentName: "builder-2",
58
58
  sessionId: "session-200",
59
- beadId: "overstory-abc2",
59
+ taskId: "overstory-abc2",
60
60
  reason: "crash",
61
61
  progressSummary: "Halfway done",
62
62
  pendingWork: "Finish implementation",
@@ -82,7 +82,7 @@ describe("lifecycle", () => {
82
82
  agentsDir,
83
83
  agentName: "builder-3",
84
84
  sessionId: "session-300",
85
- beadId: "overstory-def3",
85
+ taskId: "overstory-def3",
86
86
  reason: "manual",
87
87
  progressSummary: "Done with phase 1",
88
88
  pendingWork: "Phase 2",
@@ -116,7 +116,7 @@ describe("lifecycle", () => {
116
116
  agentsDir,
117
117
  agentName: "builder-4",
118
118
  sessionId: "session-400",
119
- beadId: "overstory-ghi4",
119
+ taskId: "overstory-ghi4",
120
120
  reason: "compaction",
121
121
  progressSummary: "First session work",
122
122
  pendingWork: "Continue",
@@ -137,7 +137,7 @@ describe("lifecycle", () => {
137
137
  agentsDir,
138
138
  agentName: "builder-4",
139
139
  sessionId: "session-401",
140
- beadId: "overstory-ghi4",
140
+ taskId: "overstory-ghi4",
141
141
  reason: "timeout",
142
142
  progressSummary: "Second session work",
143
143
  pendingWork: "Finish up",
@@ -172,7 +172,7 @@ describe("lifecycle", () => {
172
172
  agentsDir,
173
173
  agentName: "builder-5",
174
174
  sessionId: "session-500",
175
- beadId: "overstory-jkl5",
175
+ taskId: "overstory-jkl5",
176
176
  reason: "compaction",
177
177
  progressSummary: "Done",
178
178
  pendingWork: "Nothing",
@@ -73,7 +73,7 @@ export async function initiateHandoff(options: {
73
73
  agentsDir: string;
74
74
  agentName: string;
75
75
  sessionId: string;
76
- beadId: string;
76
+ taskId: string;
77
77
  reason: SessionHandoff["reason"];
78
78
  progressSummary: string;
79
79
  pendingWork: string;
@@ -83,7 +83,7 @@ export async function initiateHandoff(options: {
83
83
  }): Promise<SessionHandoff> {
84
84
  const checkpoint: SessionCheckpoint = {
85
85
  agentName: options.agentName,
86
- beadId: options.beadId,
86
+ taskId: options.taskId,
87
87
  sessionId: options.sessionId,
88
88
  timestamp: new Date().toISOString(),
89
89
  progressSummary: options.progressSummary,
@@ -25,7 +25,7 @@ Read your assignment. Execute immediately.
25
25
  function makeConfig(overrides?: Partial<OverlayConfig>): OverlayConfig {
26
26
  return {
27
27
  agentName: "test-builder",
28
- beadId: "overstory-abc",
28
+ taskId: "overstory-abc",
29
29
  specPath: ".overstory/specs/overstory-abc.md",
30
30
  branchName: "agent/test-builder/overstory-abc",
31
31
  worktreePath: "/tmp/test-project/.overstory/worktrees/test-builder",
@@ -48,8 +48,8 @@ describe("generateOverlay", () => {
48
48
  expect(output).toContain("my-scout");
49
49
  });
50
50
 
51
- test("output contains bead ID", async () => {
52
- const config = makeConfig({ beadId: "overstory-xyz" });
51
+ test("output contains task ID", async () => {
52
+ const config = makeConfig({ taskId: "overstory-xyz" });
53
53
  const output = await generateOverlay(config);
54
54
 
55
55
  expect(output).toContain("overstory-xyz");
@@ -134,7 +134,7 @@ describe("generateOverlay", () => {
134
134
  const config = makeConfig({ mulchDomains: ["typescript", "testing"] });
135
135
  const output = await generateOverlay(config);
136
136
 
137
- expect(output).toContain("mulch prime typescript testing");
137
+ expect(output).toContain("ml prime typescript testing");
138
138
  });
139
139
 
140
140
  test("empty mulch domains shows fallback text", async () => {
@@ -159,7 +159,7 @@ describe("generateOverlay", () => {
159
159
  });
160
160
  const output = await generateOverlay(config);
161
161
 
162
- expect(output).toContain("overstory sling");
162
+ expect(output).toContain("ov sling");
163
163
  expect(output).toContain("--parent lead-alpha");
164
164
  expect(output).toContain("--depth 2");
165
165
  });
@@ -261,13 +261,13 @@ describe("generateOverlay", () => {
261
261
  const config = makeConfig({
262
262
  capability: "scout",
263
263
  agentName: "recon-1",
264
- beadId: "overstory-task1",
264
+ taskId: "overstory-task1",
265
265
  parentAgent: "lead-alpha",
266
266
  });
267
267
  const output = await generateOverlay(config);
268
268
 
269
269
  expect(output).toContain("bd close overstory-task1");
270
- expect(output).toContain("overstory mail send --to lead-alpha");
270
+ expect(output).toContain("ov mail send --to lead-alpha");
271
271
  });
272
272
 
273
273
  test("reviewer completion section uses coordinator when no parent", async () => {
@@ -284,8 +284,8 @@ describe("generateOverlay", () => {
284
284
  const config = makeConfig({ agentName: "worker-42" });
285
285
  const output = await generateOverlay(config);
286
286
 
287
- expect(output).toContain("overstory mail check --agent worker-42");
288
- expect(output).toContain("overstory mail send --to");
287
+ expect(output).toContain("ov mail check --agent worker-42");
288
+ expect(output).toContain("ov mail send --to");
289
289
  });
290
290
 
291
291
  test("output includes base agent definition content (Layer 1)", async () => {
@@ -419,7 +419,7 @@ describe("generateOverlay", () => {
419
419
  });
420
420
 
421
421
  test("default trackerCli renders as bd in quality gates", async () => {
422
- const config = makeConfig({ capability: "builder", beadId: "overstory-task1" });
422
+ const config = makeConfig({ capability: "builder", taskId: "overstory-task1" });
423
423
  const output = await generateOverlay(config);
424
424
 
425
425
  expect(output).toContain("bd close overstory-task1");
@@ -429,7 +429,7 @@ describe("generateOverlay", () => {
429
429
  const config = makeConfig({
430
430
  capability: "builder",
431
431
  trackerCli: "sd",
432
- beadId: "overstory-test1",
432
+ taskId: "overstory-test1",
433
433
  });
434
434
  const output = await generateOverlay(config);
435
435
 
@@ -451,7 +451,7 @@ describe("generateOverlay", () => {
451
451
  const config = makeConfig({
452
452
  capability: "scout",
453
453
  trackerCli: "sd",
454
- beadId: "overstory-test2",
454
+ taskId: "overstory-test2",
455
455
  });
456
456
  const output = await generateOverlay(config);
457
457
 
@@ -482,7 +482,7 @@ describe("generateOverlay", () => {
482
482
  });
483
483
 
484
484
  test("defaults backward-compatible: no trackerCli/trackerName produces bd/beads", async () => {
485
- const config = makeConfig({ capability: "builder", beadId: "overstory-back" });
485
+ const config = makeConfig({ capability: "builder", taskId: "overstory-back" });
486
486
  const output = await generateOverlay(config);
487
487
 
488
488
  expect(output).toContain("bd close overstory-back");
@@ -521,7 +521,7 @@ describe("writeOverlay", () => {
521
521
  const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
522
522
  const content = await Bun.file(outputPath).text();
523
523
  expect(content).toContain("file-writer-test");
524
- expect(content).toContain(config.beadId);
524
+ expect(content).toContain(config.taskId);
525
525
  expect(content).toContain(config.branchName);
526
526
  });
527
527
 
@@ -24,14 +24,14 @@ function formatFileScope(fileScope: readonly string[]): string {
24
24
  }
25
25
 
26
26
  /**
27
- * Format mulch domains as a `mulch prime` command.
27
+ * Format mulch domains as a `ml prime` command.
28
28
  * Returns a human-readable fallback if no domains are configured.
29
29
  */
30
30
  function formatMulchDomains(domains: readonly string[]): string {
31
31
  if (domains.length === 0) {
32
32
  return "No specific expertise domains configured";
33
33
  }
34
- return `\`\`\`bash\nmulch prime ${domains.join(" ")}\n\`\`\``;
34
+ return `\`\`\`bash\nml prime ${domains.join(" ")}\n\`\`\``;
35
35
  }
36
36
 
37
37
  /**
@@ -90,9 +90,9 @@ function formatQualityGates(config: OverlayConfig): string {
90
90
  "",
91
91
  "Before reporting completion:",
92
92
  "",
93
- `1. **Record mulch learnings:** \`mulch record <domain> --type <convention|pattern|reference> --description "..."\` — capture reusable knowledge from your work`,
94
- `2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.beadId} --reason "summary of findings"\``,
95
- `3. **Send results:** \`overstory mail send --to ${config.parentAgent ?? "coordinator"} --subject "done" --body "Summary" --type result --agent ${config.agentName}\``,
93
+ `1. **Record mulch learnings:** \`ml record <domain> --type <convention|pattern|reference> --description "..."\` — capture reusable knowledge from your work`,
94
+ `2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of findings"\``,
95
+ `3. **Send results:** \`ov mail send --to ${config.parentAgent ?? "coordinator"} --subject "done" --body "Summary" --type result --agent ${config.agentName}\``,
96
96
  "",
97
97
  "You are a read-only agent. Do NOT commit, modify files, or run quality gates.",
98
98
  ].join("\n");
@@ -112,12 +112,12 @@ function formatQualityGates(config: OverlayConfig): string {
112
112
  "",
113
113
  ...gateLines,
114
114
  `${gateLines.length + 1}. **Commit:** all changes committed to your branch (${config.branchName})`,
115
- `${gateLines.length + 2}. **Record mulch learnings:** \`mulch record <domain> --type <convention|pattern|failure|decision> --description "..." --outcome-status success --outcome-agent ${config.agentName}\` — capture insights from your work`,
116
- `${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`overstory mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.beadId}" --body "Quality gates passed." --type worker_done --agent ${config.agentName}\``,
117
- `${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.beadId} --reason "summary of changes"\``,
115
+ `${gateLines.length + 2}. **Record mulch learnings:** \`ml record <domain> --type <convention|pattern|failure|decision> --description "..." --outcome-status success --outcome-agent ${config.agentName}\` — capture insights from your work`,
116
+ `${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`ov mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.taskId}" --body "Quality gates passed." --type worker_done --agent ${config.agentName}\``,
117
+ `${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of changes"\``,
118
118
  "",
119
119
  "Do NOT push to the canonical branch. Your work will be merged by the",
120
- "coordinator via `overstory merge`.",
120
+ "coordinator via `ov merge`.",
121
121
  ].join("\n");
122
122
  }
123
123
 
@@ -132,7 +132,7 @@ function formatConstraints(config: OverlayConfig): string {
132
132
  "",
133
133
  "- You are **read-only**: do NOT modify, create, or delete any files",
134
134
  "- Do NOT commit, push, or make any git state changes",
135
- `- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`overstory mail send --type result\``,
135
+ `- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`ov mail send --type result\``,
136
136
  "- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
137
137
  ].join("\n");
138
138
  }
@@ -145,7 +145,7 @@ function formatConstraints(config: OverlayConfig): string {
145
145
  "- Only modify files in your File Scope",
146
146
  `- Commit only to your branch: ${config.branchName}`,
147
147
  "- Never push to the canonical branch",
148
- `- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`overstory mail send --type result\``,
148
+ `- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`ov mail send --type result\``,
149
149
  "- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
150
150
  ].join("\n");
151
151
  }
@@ -159,10 +159,10 @@ function formatCanSpawn(config: OverlayConfig): string {
159
159
  return "You may NOT spawn sub-workers.";
160
160
  }
161
161
  return [
162
- "You may spawn sub-workers using `overstory sling`. Example:",
162
+ "You may spawn sub-workers using `ov sling`. Example:",
163
163
  "",
164
164
  "```bash",
165
- "overstory sling <task-id> --capability builder --name <worker-name> \\",
165
+ "ov sling <task-id> --capability builder --name <worker-name> \\",
166
166
  ` --parent ${config.agentName} --depth ${config.depth + 1}`,
167
167
  "```",
168
168
  ].join("\n");
@@ -205,7 +205,7 @@ export async function generateOverlay(config: OverlayConfig): Promise<string> {
205
205
 
206
206
  const replacements: Record<string, string> = {
207
207
  "{{AGENT_NAME}}": config.agentName,
208
- "{{BEAD_ID}}": config.beadId,
208
+ "{{BEAD_ID}}": config.taskId,
209
209
  "{{SPEC_PATH}}": config.specPath ?? "No spec file provided",
210
210
  "{{BRANCH_NAME}}": config.branchName,
211
211
  "{{WORKTREE_PATH}}": config.worktreePath,
@@ -97,7 +97,7 @@ describe("discoverAgents", () => {
97
97
  capability: "builder",
98
98
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
99
99
  branchName: "overstory/builder-test/task-123",
100
- beadId: "task-123",
100
+ taskId: "task-123",
101
101
  tmuxSession: "overstory-test-builder",
102
102
  state: "working",
103
103
  pid: 12345,
@@ -129,7 +129,7 @@ describe("discoverAgents", () => {
129
129
  capability: "builder",
130
130
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
131
131
  branchName: "overstory/builder-test/task-123",
132
- beadId: "task-123",
132
+ taskId: "task-123",
133
133
  tmuxSession: "overstory-test-builder",
134
134
  state: "working",
135
135
  pid: 12345,
@@ -148,7 +148,7 @@ describe("discoverAgents", () => {
148
148
  capability: "scout",
149
149
  worktreePath: join(tempDir, ".overstory", "worktrees", "scout-test"),
150
150
  branchName: "overstory/scout-test/task-456",
151
- beadId: "task-456",
151
+ taskId: "task-456",
152
152
  tmuxSession: "overstory-test-scout",
153
153
  state: "working",
154
154
  pid: 12346,
@@ -180,7 +180,7 @@ describe("discoverAgents", () => {
180
180
  capability: "builder",
181
181
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-working"),
182
182
  branchName: "overstory/builder-working/task-123",
183
- beadId: "task-123",
183
+ taskId: "task-123",
184
184
  tmuxSession: "overstory-test-working",
185
185
  state: "working",
186
186
  pid: 12345,
@@ -199,7 +199,7 @@ describe("discoverAgents", () => {
199
199
  capability: "builder",
200
200
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-completed"),
201
201
  branchName: "overstory/builder-completed/task-456",
202
- beadId: "task-456",
202
+ taskId: "task-456",
203
203
  tmuxSession: "overstory-test-completed",
204
204
  state: "completed",
205
205
  pid: null,