@os-eco/overstory-cli 0.6.4 → 0.6.5

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 (71) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/agents/checkpoint.test.ts +2 -2
  4. package/src/agents/hooks-deployer.test.ts +37 -0
  5. package/src/agents/hooks-deployer.ts +15 -1
  6. package/src/agents/identity.test.ts +27 -27
  7. package/src/agents/identity.ts +10 -10
  8. package/src/agents/lifecycle.test.ts +6 -6
  9. package/src/agents/lifecycle.ts +2 -2
  10. package/src/agents/overlay.test.ts +9 -9
  11. package/src/agents/overlay.ts +4 -4
  12. package/src/commands/agents.test.ts +5 -5
  13. package/src/commands/agents.ts +3 -3
  14. package/src/commands/clean.test.ts +5 -5
  15. package/src/commands/coordinator.test.ts +2 -2
  16. package/src/commands/coordinator.ts +1 -1
  17. package/src/commands/costs.test.ts +45 -45
  18. package/src/commands/dashboard.ts +3 -3
  19. package/src/commands/inspect.test.ts +16 -16
  20. package/src/commands/inspect.ts +1 -1
  21. package/src/commands/log.test.ts +21 -21
  22. package/src/commands/log.ts +7 -7
  23. package/src/commands/mail.test.ts +5 -5
  24. package/src/commands/merge.test.ts +8 -8
  25. package/src/commands/merge.ts +8 -8
  26. package/src/commands/metrics.test.ts +6 -6
  27. package/src/commands/metrics.ts +1 -1
  28. package/src/commands/monitor.ts +1 -1
  29. package/src/commands/nudge.test.ts +1 -1
  30. package/src/commands/prime.test.ts +4 -4
  31. package/src/commands/prime.ts +6 -6
  32. package/src/commands/run.test.ts +1 -1
  33. package/src/commands/sling.test.ts +5 -5
  34. package/src/commands/sling.ts +13 -10
  35. package/src/commands/spec.test.ts +2 -2
  36. package/src/commands/spec.ts +8 -8
  37. package/src/commands/status.test.ts +97 -1
  38. package/src/commands/status.ts +17 -16
  39. package/src/commands/stop.test.ts +1 -1
  40. package/src/commands/supervisor.test.ts +9 -9
  41. package/src/commands/supervisor.ts +11 -11
  42. package/src/commands/trace.test.ts +6 -6
  43. package/src/commands/trace.ts +6 -6
  44. package/src/commands/worktree.test.ts +205 -29
  45. package/src/commands/worktree.ts +47 -9
  46. package/src/doctor/consistency.test.ts +14 -14
  47. package/src/doctor/merge-queue.test.ts +4 -4
  48. package/src/e2e/init-sling-lifecycle.test.ts +2 -2
  49. package/src/errors.ts +1 -1
  50. package/src/index.ts +3 -3
  51. package/src/mail/broadcast.test.ts +1 -1
  52. package/src/mail/client.test.ts +6 -6
  53. package/src/mail/store.test.ts +3 -3
  54. package/src/merge/queue.test.ts +12 -12
  55. package/src/merge/queue.ts +2 -2
  56. package/src/merge/resolver.test.ts +159 -7
  57. package/src/merge/resolver.ts +46 -2
  58. package/src/metrics/store.test.ts +44 -44
  59. package/src/metrics/store.ts +2 -2
  60. package/src/metrics/summary.test.ts +35 -35
  61. package/src/mulch/client.test.ts +1 -1
  62. package/src/sessions/compat.test.ts +3 -3
  63. package/src/sessions/compat.ts +1 -1
  64. package/src/sessions/store.test.ts +4 -4
  65. package/src/sessions/store.ts +2 -2
  66. package/src/types.ts +14 -14
  67. package/src/watchdog/daemon.test.ts +10 -10
  68. package/src/watchdog/daemon.ts +1 -1
  69. package/src/watchdog/health.test.ts +1 -1
  70. package/src/worktree/manager.test.ts +20 -20
  71. package/src/worktree/manager.ts +120 -4
package/README.md CHANGED
@@ -222,7 +222,7 @@ overstory inspect <agent> Deep per-agent inspection
222
222
  --no-tmux Skip tmux capture
223
223
  --limit <n> Limit events shown
224
224
 
225
- overstory spec write <bead-id> Write a task specification
225
+ overstory spec write <task-id> Write a task specification
226
226
  --body <content> Spec content (or pipe via stdin)
227
227
 
228
228
  overstory errors Aggregated error view across agents
@@ -270,16 +270,16 @@ Global Flags:
270
270
  ## Tech Stack
271
271
 
272
272
  - **Runtime**: Bun (TypeScript directly, no build step)
273
- - **Dependencies**: Minimal runtime — `chalk` (color output), core I/O via Bun built-in APIs
273
+ - **Dependencies**: Minimal runtime — `chalk` (color output), `commander` (CLI framework), core I/O via Bun built-in APIs
274
274
  - **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
275
275
  - **Linting**: Biome (formatter + linter)
276
- - **Testing**: `bun test` (2128 tests across 76 files, colocated with source)
276
+ - **Testing**: `bun test` (2145 tests across 76 files, colocated with source)
277
277
  - **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
278
278
 
279
279
  ## Development
280
280
 
281
281
  ```bash
282
- # Run tests (2128 tests across 76 files)
282
+ # Run tests (2145 tests across 76 files)
283
283
  bun test
284
284
 
285
285
  # Run a single test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Multi-agent orchestration for Claude Code — spawn worker agents in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution",
5
5
  "author": "Jaymin West",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@ import { clearCheckpoint, loadCheckpoint, saveCheckpoint } from "./checkpoint.ts
9
9
  function makeCheckpoint(overrides?: Partial<SessionCheckpoint>): SessionCheckpoint {
10
10
  return {
11
11
  agentName: "test-agent",
12
- beadId: "overstory-abc1",
12
+ taskId: "overstory-abc1",
13
13
  sessionId: "session-001",
14
14
  timestamp: "2025-01-01T00:00:00.000Z",
15
15
  progressSummary: "Implemented checkpoint module",
@@ -40,7 +40,7 @@ describe("checkpoint", () => {
40
40
 
41
41
  expect(loaded).not.toBeNull();
42
42
  expect(loaded?.agentName).toBe("test-agent");
43
- expect(loaded?.beadId).toBe("overstory-abc1");
43
+ expect(loaded?.taskId).toBe("overstory-abc1");
44
44
  expect(loaded?.sessionId).toBe("session-001");
45
45
  expect(loaded?.progressSummary).toBe("Implemented checkpoint module");
46
46
  expect(loaded?.filesModified).toEqual(["src/agents/checkpoint.ts"]);
@@ -8,6 +8,7 @@ import {
8
8
  buildBashPathBoundaryScript,
9
9
  buildPathBoundaryGuardScript,
10
10
  deployHooks,
11
+ escapeForSingleQuotedShell,
11
12
  getBashPathBoundaryGuards,
12
13
  getCapabilityGuards,
13
14
  getDangerGuards,
@@ -2116,3 +2117,39 @@ describe("bash path boundary integration", () => {
2116
2117
  expect(universalGuard.hooks[0].command).toContain('"decision":"block"');
2117
2118
  });
2118
2119
  });
2120
+
2121
+ describe("escapeForSingleQuotedShell", () => {
2122
+ test("no single quotes: string passes through unchanged", () => {
2123
+ expect(escapeForSingleQuotedShell("hello world")).toBe("hello world");
2124
+ });
2125
+
2126
+ test("single quotes escaped: it's becomes it'\\''s", () => {
2127
+ expect(escapeForSingleQuotedShell("it's")).toBe("it'\\''s");
2128
+ });
2129
+
2130
+ test("multiple single quotes: each one is escaped independently", () => {
2131
+ expect(escapeForSingleQuotedShell("can't won't")).toBe("can'\\''t won'\\''t");
2132
+ });
2133
+
2134
+ test("empty string: returns empty string", () => {
2135
+ expect(escapeForSingleQuotedShell("")).toBe("");
2136
+ });
2137
+
2138
+ test("blockGuard shell command outputs valid JSON when executed", async () => {
2139
+ const guards = getCapabilityGuards("builder");
2140
+ const taskGuard = guards.find((g) => g.matcher === "Task");
2141
+ expect(taskGuard).toBeDefined();
2142
+ const cmd = taskGuard?.hooks[0]?.command ?? "";
2143
+ const echoCmd = cmd.replace('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0; ', "");
2144
+ const proc = Bun.spawn(["sh", "-c", echoCmd], {
2145
+ stdout: "pipe",
2146
+ stderr: "pipe",
2147
+ env: { ...process.env, OVERSTORY_AGENT_NAME: "test-agent" },
2148
+ });
2149
+ const output = await new Response(proc.stdout).text();
2150
+ await proc.exited;
2151
+ const parsed = JSON.parse(output.trim());
2152
+ expect(parsed.decision).toBe("block");
2153
+ expect(parsed.reason).toContain("overstory sling");
2154
+ });
2155
+ });
@@ -205,6 +205,20 @@ export function getPathBoundaryGuards(): HookEntry[] {
205
205
  ];
206
206
  }
207
207
 
208
+ /**
209
+ * Escape a string for use inside a single-quoted POSIX shell string.
210
+ *
211
+ * POSIX single-quoted strings cannot contain single quotes at all.
212
+ * The standard technique is to end the single-quoted segment, emit an escaped
213
+ * single quote using $'\'', then start a new single-quoted segment:
214
+ * 'it'\''s fine' → it's fine
215
+ *
216
+ * Exported so tests can verify escaping directly.
217
+ */
218
+ export function escapeForSingleQuotedShell(str: string): string {
219
+ return str.replace(/'/g, "'\\''");
220
+ }
221
+
208
222
  /**
209
223
  * Build a PreToolUse guard that blocks a specific tool.
210
224
  *
@@ -218,7 +232,7 @@ function blockGuard(toolName: string, reason: string): HookEntry {
218
232
  hooks: [
219
233
  {
220
234
  type: "command",
221
- command: `${ENV_GUARD} echo '${response}'`,
235
+ command: `${ENV_GUARD} echo '${escapeForSingleQuotedShell(response)}'`,
222
236
  },
223
237
  ],
224
238
  };
@@ -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");
@@ -261,7 +261,7 @@ 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);
@@ -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
 
@@ -91,7 +91,7 @@ function formatQualityGates(config: OverlayConfig): string {
91
91
  "Before reporting completion:",
92
92
  "",
93
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"\``,
94
+ `2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of findings"\``,
95
95
  `3. **Send results:** \`overstory 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.",
@@ -113,8 +113,8 @@ function formatQualityGates(config: OverlayConfig): string {
113
113
  ...gateLines,
114
114
  `${gateLines.length + 1}. **Commit:** all changes committed to your branch (${config.branchName})`,
115
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"\``,
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.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
120
  "coordinator via `overstory merge`.",
@@ -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,
@@ -18,7 +18,7 @@ export interface DiscoveredAgent {
18
18
  agentName: string;
19
19
  capability: string;
20
20
  state: string;
21
- beadId: string;
21
+ taskId: string;
22
22
  branchName: string;
23
23
  parentAgent: string | null;
24
24
  depth: number;
@@ -117,7 +117,7 @@ export async function discoverAgents(
117
117
  agentName: session.agentName,
118
118
  capability: session.capability,
119
119
  state: session.state,
120
- beadId: session.beadId,
120
+ taskId: session.taskId,
121
121
  branchName: session.branchName,
122
122
  parentAgent: session.parentAgent,
123
123
  depth: session.depth,
@@ -166,7 +166,7 @@ function printAgents(agents: DiscoveredAgent[]): void {
166
166
  for (const agent of agents) {
167
167
  const icon = getStateIcon(agent.state);
168
168
  w(` ${icon} ${agent.agentName} [${agent.capability}]\n`);
169
- w(` State: ${agent.state} | Task: ${agent.beadId}\n`);
169
+ w(` State: ${agent.state} | Task: ${agent.taskId}\n`);
170
170
  w(` Branch: ${agent.branchName}\n`);
171
171
  w(` Parent: ${agent.parentAgent ?? "none"} | Depth: ${agent.depth}\n`);
172
172