@os-eco/overstory-cli 0.6.1 → 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 (110) hide show
  1. package/README.md +8 -7
  2. package/package.json +12 -4
  3. package/src/agents/checkpoint.test.ts +2 -2
  4. package/src/agents/hooks-deployer.test.ts +131 -16
  5. package/src/agents/hooks-deployer.ts +33 -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/manifest.test.ts +86 -0
  11. package/src/agents/overlay.test.ts +9 -9
  12. package/src/agents/overlay.ts +4 -4
  13. package/src/commands/agents.test.ts +8 -8
  14. package/src/commands/agents.ts +62 -91
  15. package/src/commands/clean.test.ts +36 -51
  16. package/src/commands/clean.ts +28 -49
  17. package/src/commands/completions.ts +14 -0
  18. package/src/commands/coordinator.test.ts +133 -26
  19. package/src/commands/coordinator.ts +101 -64
  20. package/src/commands/costs.test.ts +47 -47
  21. package/src/commands/costs.ts +96 -75
  22. package/src/commands/dashboard.test.ts +2 -2
  23. package/src/commands/dashboard.ts +75 -95
  24. package/src/commands/doctor.test.ts +2 -2
  25. package/src/commands/doctor.ts +92 -79
  26. package/src/commands/errors.test.ts +2 -2
  27. package/src/commands/errors.ts +56 -50
  28. package/src/commands/feed.test.ts +2 -2
  29. package/src/commands/feed.ts +86 -83
  30. package/src/commands/group.ts +167 -177
  31. package/src/commands/hooks.test.ts +2 -2
  32. package/src/commands/hooks.ts +52 -42
  33. package/src/commands/init.test.ts +19 -19
  34. package/src/commands/init.ts +7 -16
  35. package/src/commands/inspect.test.ts +18 -18
  36. package/src/commands/inspect.ts +55 -58
  37. package/src/commands/log.test.ts +26 -31
  38. package/src/commands/log.ts +97 -91
  39. package/src/commands/logs.test.ts +1 -1
  40. package/src/commands/logs.ts +101 -104
  41. package/src/commands/mail.test.ts +5 -5
  42. package/src/commands/mail.ts +157 -169
  43. package/src/commands/merge.test.ts +28 -66
  44. package/src/commands/merge.ts +21 -51
  45. package/src/commands/metrics.test.ts +8 -8
  46. package/src/commands/metrics.ts +34 -35
  47. package/src/commands/monitor.test.ts +3 -3
  48. package/src/commands/monitor.ts +57 -62
  49. package/src/commands/nudge.test.ts +1 -1
  50. package/src/commands/nudge.ts +41 -89
  51. package/src/commands/prime.test.ts +19 -51
  52. package/src/commands/prime.ts +13 -50
  53. package/src/commands/replay.test.ts +2 -2
  54. package/src/commands/replay.ts +79 -86
  55. package/src/commands/run.test.ts +1 -1
  56. package/src/commands/run.ts +97 -77
  57. package/src/commands/sling.test.ts +201 -5
  58. package/src/commands/sling.ts +37 -64
  59. package/src/commands/spec.test.ts +14 -40
  60. package/src/commands/spec.ts +32 -101
  61. package/src/commands/status.test.ts +97 -1
  62. package/src/commands/status.ts +63 -58
  63. package/src/commands/stop.test.ts +22 -40
  64. package/src/commands/stop.ts +18 -33
  65. package/src/commands/supervisor.test.ts +12 -14
  66. package/src/commands/supervisor.ts +144 -165
  67. package/src/commands/trace.test.ts +15 -15
  68. package/src/commands/trace.ts +59 -82
  69. package/src/commands/watch.test.ts +2 -2
  70. package/src/commands/watch.ts +38 -45
  71. package/src/commands/worktree.test.ts +213 -37
  72. package/src/commands/worktree.ts +110 -55
  73. package/src/config.test.ts +96 -0
  74. package/src/doctor/consistency.test.ts +14 -14
  75. package/src/doctor/databases.test.ts +22 -2
  76. package/src/doctor/databases.ts +16 -0
  77. package/src/doctor/dependencies.test.ts +55 -1
  78. package/src/doctor/dependencies.ts +113 -18
  79. package/src/doctor/merge-queue.test.ts +4 -4
  80. package/src/e2e/init-sling-lifecycle.test.ts +8 -8
  81. package/src/errors.ts +1 -1
  82. package/src/index.ts +223 -213
  83. package/src/logging/color.test.ts +74 -91
  84. package/src/logging/color.ts +52 -46
  85. package/src/logging/reporter.test.ts +10 -10
  86. package/src/logging/reporter.ts +6 -5
  87. package/src/mail/broadcast.test.ts +1 -1
  88. package/src/mail/client.test.ts +6 -6
  89. package/src/mail/store.test.ts +3 -3
  90. package/src/merge/queue.test.ts +73 -7
  91. package/src/merge/queue.ts +17 -2
  92. package/src/merge/resolver.test.ts +159 -7
  93. package/src/merge/resolver.ts +46 -2
  94. package/src/metrics/store.test.ts +44 -44
  95. package/src/metrics/store.ts +2 -2
  96. package/src/metrics/summary.test.ts +35 -35
  97. package/src/mulch/client.test.ts +1 -1
  98. package/src/schema-consistency.test.ts +239 -0
  99. package/src/sessions/compat.test.ts +3 -3
  100. package/src/sessions/compat.ts +2 -2
  101. package/src/sessions/store.test.ts +41 -4
  102. package/src/sessions/store.ts +13 -2
  103. package/src/types.ts +14 -14
  104. package/src/watchdog/daemon.test.ts +10 -10
  105. package/src/watchdog/daemon.ts +1 -1
  106. package/src/watchdog/health.test.ts +1 -1
  107. package/src/worktree/manager.test.ts +20 -20
  108. package/src/worktree/manager.ts +120 -4
  109. package/src/worktree/tmux.test.ts +98 -9
  110. package/src/worktree/tmux.ts +18 -0
@@ -5,138 +5,121 @@ import { dirname, join } from "node:path";
5
5
  const projectRoot = join(dirname(import.meta.dir), "..");
6
6
 
7
7
  describe("color module", () => {
8
- // Test via subprocess to control env vars at import time
8
+ test("color.red is a function that wraps text", async () => {
9
+ const { color } = await import("./color.ts");
10
+ const result = color.red("hello");
11
+ expect(result).toContain("hello");
12
+ expect(typeof result).toBe("string");
13
+ });
9
14
 
10
- test("colors enabled by default (no env vars)", async () => {
11
- const proc = Bun.spawn(
12
- [
13
- "bun",
14
- "-e",
15
- 'import { color, colorsEnabled } from "./src/logging/color.ts"; console.log(JSON.stringify({ colorsEnabled, reset: color.reset }))',
16
- ],
17
- {
18
- cwd: projectRoot,
19
- stdout: "pipe",
20
- stderr: "pipe",
21
- env: { ...process.env, NO_COLOR: undefined, FORCE_COLOR: undefined, TERM: undefined },
22
- },
23
- );
24
- await proc.exited;
25
- const output = await new Response(proc.stdout).text();
26
- const result = JSON.parse(output.trim());
27
- expect(result.colorsEnabled).toBe(true);
28
- expect(result.reset).toBe("\x1b[0m");
15
+ test("color functions all exist", async () => {
16
+ const { color } = await import("./color.ts");
17
+ const expectedKeys = [
18
+ "bold",
19
+ "dim",
20
+ "red",
21
+ "green",
22
+ "yellow",
23
+ "blue",
24
+ "magenta",
25
+ "cyan",
26
+ "white",
27
+ "gray",
28
+ ];
29
+ for (const key of expectedKeys) {
30
+ expect(key in color).toBe(true);
31
+ expect(typeof (color as Record<string, unknown>)[key]).toBe("function");
32
+ }
29
33
  });
30
34
 
31
- test("NO_COLOR disables colors", async () => {
32
- const proc = Bun.spawn(
33
- [
34
- "bun",
35
- "-e",
36
- 'import { color, colorsEnabled } from "./src/logging/color.ts"; console.log(JSON.stringify({ colorsEnabled, reset: color.reset }))',
37
- ],
38
- {
39
- cwd: projectRoot,
40
- stdout: "pipe",
41
- stderr: "pipe",
42
- env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: undefined },
43
- },
44
- );
45
- await proc.exited;
46
- const output = await new Response(proc.stdout).text();
47
- const result = JSON.parse(output.trim());
48
- expect(result.colorsEnabled).toBe(false);
49
- expect(result.reset).toBe("");
35
+ test("brand palette functions wrap text", async () => {
36
+ const { brand, accent, muted } = await import("./color.ts");
37
+ const result = brand("Overstory");
38
+ expect(result).toContain("Overstory");
39
+ expect(typeof accent("test")).toBe("string");
40
+ expect(typeof muted("test")).toBe("string");
50
41
  });
51
42
 
52
- test("TERM=dumb disables colors", async () => {
53
- const proc = Bun.spawn(
54
- [
55
- "bun",
56
- "-e",
57
- 'import { color, colorsEnabled } from "./src/logging/color.ts"; console.log(JSON.stringify({ colorsEnabled, reset: color.reset }))',
58
- ],
59
- {
60
- cwd: projectRoot,
61
- stdout: "pipe",
62
- stderr: "pipe",
63
- env: { ...process.env, TERM: "dumb", NO_COLOR: undefined, FORCE_COLOR: undefined },
64
- },
65
- );
66
- await proc.exited;
67
- const output = await new Response(proc.stdout).text();
68
- const result = JSON.parse(output.trim());
69
- expect(result.colorsEnabled).toBe(false);
70
- expect(result.reset).toBe("");
43
+ test("noColor is an identity function", async () => {
44
+ const { noColor } = await import("./color.ts");
45
+ expect(noColor("hello")).toBe("hello");
46
+ expect(noColor("")).toBe("");
71
47
  });
72
48
 
73
- test("FORCE_COLOR overrides NO_COLOR", async () => {
49
+ test("stripAnsi removes escape codes", async () => {
50
+ const { stripAnsi } = await import("./color.ts");
51
+ expect(stripAnsi("\x1b[31mhello\x1b[39m")).toBe("hello");
52
+ expect(stripAnsi("plain")).toBe("plain");
53
+ expect(stripAnsi("\x1b[1m\x1b[31mbold red\x1b[39m\x1b[22m")).toBe("bold red");
54
+ });
55
+
56
+ test("visibleLength excludes ANSI codes", async () => {
57
+ const { visibleLength } = await import("./color.ts");
58
+ expect(visibleLength("\x1b[31mhello\x1b[39m")).toBe(5);
59
+ expect(visibleLength("hello")).toBe(5);
60
+ expect(visibleLength("")).toBe(0);
61
+ });
62
+
63
+ test("setQuiet/isQuiet controls quiet mode", async () => {
64
+ const { isQuiet, setQuiet } = await import("./color.ts");
65
+ expect(isQuiet()).toBe(false);
66
+ setQuiet(true);
67
+ expect(isQuiet()).toBe(true);
68
+ setQuiet(false);
69
+ expect(isQuiet()).toBe(false);
70
+ });
71
+
72
+ test("NO_COLOR env causes chalk.level to be 0", async () => {
74
73
  const proc = Bun.spawn(
75
74
  [
76
75
  "bun",
77
76
  "-e",
78
- 'import { color, colorsEnabled } from "./src/logging/color.ts"; console.log(JSON.stringify({ colorsEnabled, reset: color.reset }))',
77
+ 'import chalk from "chalk"; console.log(JSON.stringify({ level: chalk.level }))',
79
78
  ],
80
79
  {
81
80
  cwd: projectRoot,
82
81
  stdout: "pipe",
83
82
  stderr: "pipe",
84
- env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "1" },
83
+ env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: undefined },
85
84
  },
86
85
  );
87
86
  await proc.exited;
88
87
  const output = await new Response(proc.stdout).text();
89
88
  const result = JSON.parse(output.trim());
90
- expect(result.colorsEnabled).toBe(true);
91
- expect(result.reset).toBe("\x1b[0m");
89
+ expect(result.level).toBe(0);
92
90
  });
93
91
 
94
- test("FORCE_COLOR=0 disables colors", async () => {
92
+ test("FORCE_COLOR overrides NO_COLOR", async () => {
95
93
  const proc = Bun.spawn(
96
94
  [
97
95
  "bun",
98
96
  "-e",
99
- 'import { color, colorsEnabled } from "./src/logging/color.ts"; console.log(JSON.stringify({ colorsEnabled, reset: color.reset }))',
97
+ 'import chalk from "chalk"; console.log(JSON.stringify({ level: chalk.level }))',
100
98
  ],
101
99
  {
102
100
  cwd: projectRoot,
103
101
  stdout: "pipe",
104
102
  stderr: "pipe",
105
- env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: undefined },
103
+ env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "1" },
106
104
  },
107
105
  );
108
106
  await proc.exited;
109
107
  const output = await new Response(proc.stdout).text();
110
108
  const result = JSON.parse(output.trim());
111
- expect(result.colorsEnabled).toBe(false);
109
+ expect(result.level).toBeGreaterThan(0);
112
110
  });
113
111
 
114
- test("setQuiet/isQuiet controls quiet mode", async () => {
115
- const { isQuiet, setQuiet } = await import("./color.ts");
116
- expect(isQuiet()).toBe(false);
117
- setQuiet(true);
118
- expect(isQuiet()).toBe(true);
119
- setQuiet(false);
120
- expect(isQuiet()).toBe(false);
112
+ test("chalk re-export is available", async () => {
113
+ const { chalk } = await import("./color.ts");
114
+ expect(typeof chalk.red).toBe("function");
115
+ expect(chalk.red("test")).toContain("test");
121
116
  });
122
117
 
123
- test("all color keys present", async () => {
118
+ test("ColorFn type: color functions accept strings and return strings", async () => {
124
119
  const { color } = await import("./color.ts");
125
- const expectedKeys = [
126
- "reset",
127
- "bold",
128
- "dim",
129
- "red",
130
- "green",
131
- "yellow",
132
- "blue",
133
- "magenta",
134
- "cyan",
135
- "white",
136
- "gray",
137
- ];
138
- for (const key of expectedKeys) {
139
- expect(key in color).toBe(true);
140
- }
120
+ // Each color function should accept a string and return a string
121
+ const result = color.bold(color.red("nested"));
122
+ expect(result).toContain("nested");
123
+ expect(typeof result).toBe("string");
141
124
  });
142
125
  });
@@ -1,63 +1,69 @@
1
1
  /**
2
- * Central ANSI color and output control.
2
+ * Central color and output control using Chalk.
3
3
  *
4
- * Respects the NO_COLOR convention (https://no-color.org/):
5
- * - When NO_COLOR env var is set (any value), all color codes become empty strings
6
- * - When TERM=dumb, colors are disabled
7
- * - When FORCE_COLOR is set to a truthy value, colors are forced on
8
- *
9
- * Also provides --quiet support: when quiet mode is enabled, non-error
10
- * output is suppressed. Commands check isQuiet() before writing to stdout.
4
+ * Chalk natively handles NO_COLOR, FORCE_COLOR, and TERM=dumb.
5
+ * See https://github.com/chalk/chalk#supportscolor for detection logic.
11
6
  */
12
7
 
13
- /**
14
- * Priority order for color detection:
15
- * 1. FORCE_COLOR (highest) — set to non-"0" to force colors on
16
- * 2. NO_COLOR — any value disables colors
17
- * 3. TERM=dumb — disables colors
18
- * 4. Default: colors enabled
19
- */
20
- function shouldUseColor(): boolean {
21
- if (process.env.FORCE_COLOR !== undefined) {
22
- return process.env.FORCE_COLOR !== "0";
23
- }
24
- if (process.env.NO_COLOR !== undefined) {
25
- return false;
26
- }
27
- if (process.env.TERM === "dumb") {
28
- return false;
29
- }
30
- return true;
31
- }
8
+ import chalk from "chalk";
32
9
 
33
- const useColor = shouldUseColor();
10
+ // --- Brand palette (os-eco brand colors) ---
34
11
 
35
- function code(ansiCode: string): string {
36
- return useColor ? ansiCode : "";
37
- }
12
+ /** Forest green Overstory primary brand color. */
13
+ export const brand = chalk.rgb(27, 94, 32);
14
+
15
+ /** Amber — highlights, warnings. */
16
+ export const accent = chalk.rgb(255, 183, 77);
17
+
18
+ /** Stone gray — secondary text, muted content. */
19
+ export const muted = chalk.rgb(120, 120, 110);
20
+
21
+ // --- Standard color functions ---
38
22
 
39
23
  /**
40
- * ANSI color codes that respect NO_COLOR.
41
- * When colors are disabled, all values are empty strings.
24
+ * Color functions that wrap text with ANSI codes.
25
+ * Each value is a function: color.red("text") returns "\x1b[31mtext\x1b[39m".
26
+ * Chalk auto-resets when wrapping, so color.reset is not needed.
42
27
  */
43
28
  export const color = {
44
- reset: code("\x1b[0m"),
45
- bold: code("\x1b[1m"),
46
- dim: code("\x1b[2m"),
47
- red: code("\x1b[31m"),
48
- green: code("\x1b[32m"),
49
- yellow: code("\x1b[33m"),
50
- blue: code("\x1b[34m"),
51
- magenta: code("\x1b[35m"),
52
- cyan: code("\x1b[36m"),
53
- white: code("\x1b[37m"),
54
- gray: code("\x1b[90m"),
29
+ bold: chalk.bold,
30
+ dim: chalk.dim,
31
+ red: chalk.red,
32
+ green: chalk.green,
33
+ yellow: chalk.yellow,
34
+ blue: chalk.blue,
35
+ magenta: chalk.magenta,
36
+ cyan: chalk.cyan,
37
+ white: chalk.white,
38
+ gray: chalk.gray,
55
39
  } as const;
56
40
 
57
- /** Whether ANSI colors are currently enabled. */
58
- export const colorsEnabled = useColor;
41
+ // Re-export chalk for direct use (chaining, custom RGB, etc.)
42
+ export { chalk };
43
+
44
+ /** Type for color function values (for consumers that store colors in variables). */
45
+ export type ColorFn = (text: string) => string;
46
+
47
+ /** Identity function for "no color" cases (replaces old color.white as default). */
48
+ export const noColor: ColorFn = (text: string) => text;
49
+
50
+ // --- ANSI strip utilities (for visible-width calculations in dashboard) ---
51
+
52
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ESC (0x1B) is required to match ANSI escape sequences
53
+ const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
54
+
55
+ /** Strip ANSI escape codes from a string. */
56
+ export function stripAnsi(str: string): string {
57
+ return str.replace(ANSI_REGEX, "");
58
+ }
59
+
60
+ /** Visible string length (excluding ANSI escape codes). */
61
+ export function visibleLength(str: string): number {
62
+ return stripAnsi(str).length;
63
+ }
59
64
 
60
65
  // --- Quiet mode ---
66
+
61
67
  let quietMode = false;
62
68
 
63
69
  /** Enable quiet mode (suppress non-error output). */
@@ -106,12 +106,12 @@ describe("formatLogLine", () => {
106
106
  expect(result).toContain("[invalid-timestamp]");
107
107
  });
108
108
 
109
- test("contains ANSI escape codes in output", () => {
109
+ test("output is a formatted string with level label and timestamp", () => {
110
110
  const result = formatLogLine(makeEvent());
111
- // \x1b[ is the ANSI escape sequence prefix
112
- expect(result).toContain("\x1b[");
113
- // Reset sequence should appear at least once
114
- expect(result).toContain("\x1b[0m");
111
+ // Chalk adds ANSI codes in TTY environments; in non-TTY (CI/tests) output is plain.
112
+ // Verify the result contains the expected label and timestamp regardless of color mode.
113
+ expect(result).toContain("INF");
114
+ expect(result).toContain("[14:30:00]");
115
115
  });
116
116
 
117
117
  test("uses different ANSI color codes for different levels", () => {
@@ -120,11 +120,11 @@ describe("formatLogLine", () => {
120
120
  const warnResult = formatLogLine(makeEvent({ level: "warn" }));
121
121
  const errorResult = formatLogLine(makeEvent({ level: "error" }));
122
122
 
123
- // Each level uses a distinct color: gray(90), blue(34), yellow(33), red(31)
124
- expect(debugResult).toContain("\x1b[90m");
125
- expect(infoResult).toContain("\x1b[34m");
126
- expect(warnResult).toContain("\x1b[33m");
127
- expect(errorResult).toContain("\x1b[31m");
123
+ // Each level produces distinct output (Chalk's exact escape codes may vary by terminal)
124
+ expect(debugResult).not.toBe(infoResult);
125
+ expect(infoResult).not.toBe(warnResult);
126
+ expect(warnResult).not.toBe(errorResult);
127
+ expect(debugResult).not.toBe(errorResult);
128
128
  });
129
129
 
130
130
  test("formats boolean data values via String()", () => {
@@ -2,13 +2,14 @@
2
2
  * Console reporter with ANSI colors for human-readable log output.
3
3
  *
4
4
  * Formats LogEvent objects into colored terminal output.
5
- * Uses ANSI escape codes directly (no external dependencies).
5
+ * Uses Chalk for color formatting.
6
6
  */
7
7
 
8
8
  import type { LogEvent } from "../types.ts";
9
+ import type { ColorFn } from "./color.ts";
9
10
  import { color, isQuiet } from "./color.ts";
10
11
 
11
- const LEVEL_COLORS: Record<LogEvent["level"], string> = {
12
+ const LEVEL_COLORS: Record<LogEvent["level"], ColorFn> = {
12
13
  debug: color.gray,
13
14
  info: color.blue,
14
15
  warn: color.yellow,
@@ -35,13 +36,13 @@ export function formatLogLine(event: LogEvent): string {
35
36
  const time = extractTime(event.timestamp);
36
37
 
37
38
  // Build the agent prefix
38
- const agentPart = event.agentName ? `${color.dim}${event.agentName}${color.reset} | ` : "";
39
+ const agentPart = event.agentName ? `${color.dim(event.agentName)} | ` : "";
39
40
 
40
41
  // Build key=value pairs from data
41
42
  const dataPart = formatData(event.data);
42
- const dataSuffix = dataPart.length > 0 ? ` ${color.dim}${dataPart}${color.reset}` : "";
43
+ const dataSuffix = dataPart.length > 0 ? ` ${color.dim(dataPart)}` : "";
43
44
 
44
- return `${color.dim}[${time}]${color.reset} ${levelColor}${color.bold}${label}${color.reset} ${agentPart}${event.event}${dataSuffix}`;
45
+ return `${color.dim(`[${time}]`)} ${levelColor(color.bold(label))} ${agentPart}${event.event}${dataSuffix}`;
45
46
  }
46
47
 
47
48
  /**
@@ -30,7 +30,7 @@ describe("resolveGroupAddress", () => {
30
30
  capability,
31
31
  worktreePath: `/worktrees/${agentName}`,
32
32
  branchName: `branch-${agentName}`,
33
- beadId: "bead-001",
33
+ taskId: "bead-001",
34
34
  tmuxSession: `overstory-test-${agentName}`,
35
35
  state: "working",
36
36
  pid: 12345,
@@ -598,7 +598,7 @@ describe("createMailClient", () => {
598
598
  describe("sendProtocol", () => {
599
599
  test("sends a worker_done message with serialized payload", () => {
600
600
  const payload: WorkerDonePayload = {
601
- beadId: "beads-abc",
601
+ taskId: "beads-abc",
602
602
  branch: "agent/builder-1",
603
603
  exitCode: 0,
604
604
  filesModified: ["src/foo.ts", "src/bar.ts"],
@@ -625,7 +625,7 @@ describe("createMailClient", () => {
625
625
  subject: "Merged",
626
626
  body: "Branch merged",
627
627
  type: "merged",
628
- payload: { branch: "agent/b1", beadId: "beads-xyz", tier: "clean-merge" as const },
628
+ payload: { branch: "agent/b1", taskId: "beads-xyz", tier: "clean-merge" as const },
629
629
  });
630
630
 
631
631
  const msg = store.getById(id);
@@ -640,7 +640,7 @@ describe("createMailClient", () => {
640
640
  body: "Build failing",
641
641
  type: "escalation",
642
642
  priority: "urgent",
643
- payload: { severity: "critical" as const, beadId: null, context: "OOM" },
643
+ payload: { severity: "critical" as const, taskId: null, context: "OOM" },
644
644
  });
645
645
 
646
646
  const msg = store.getById(id);
@@ -656,7 +656,7 @@ describe("createMailClient", () => {
656
656
  type: "assign",
657
657
  threadId: "thread-dispatch-1",
658
658
  payload: {
659
- beadId: "beads-123",
659
+ taskId: "beads-123",
660
660
  specPath: ".overstory/specs/beads-123.md",
661
661
  workerName: "builder-1",
662
662
  branch: "agent/builder-1",
@@ -671,7 +671,7 @@ describe("createMailClient", () => {
671
671
  describe("parsePayload", () => {
672
672
  test("parses a valid JSON payload", () => {
673
673
  const payload: WorkerDonePayload = {
674
- beadId: "beads-abc",
674
+ taskId: "beads-abc",
675
675
  branch: "agent/builder-1",
676
676
  exitCode: 0,
677
677
  filesModified: ["src/foo.ts"],
@@ -727,7 +727,7 @@ describe("createMailClient", () => {
727
727
  describe("checkInject with protocol messages", () => {
728
728
  test("includes payload in injection output for protocol messages", () => {
729
729
  const payload: WorkerDonePayload = {
730
- beadId: "beads-abc",
730
+ taskId: "beads-abc",
731
731
  branch: "agent/builder-1",
732
732
  exitCode: 0,
733
733
  filesModified: ["src/foo.ts"],
@@ -638,7 +638,7 @@ describe("createMailStore", () => {
638
638
 
639
639
  test("stores JSON payload string", () => {
640
640
  const payload = JSON.stringify({
641
- beadId: "beads-abc",
641
+ taskId: "beads-abc",
642
642
  branch: "agent/builder-1",
643
643
  exitCode: 0,
644
644
  filesModified: ["src/foo.ts"],
@@ -661,7 +661,7 @@ describe("createMailStore", () => {
661
661
  });
662
662
 
663
663
  test("returns payload in getUnread results", () => {
664
- const payload = JSON.stringify({ severity: "critical", beadId: null, context: "OOM" });
664
+ const payload = JSON.stringify({ severity: "critical", taskId: null, context: "OOM" });
665
665
  store.insert({
666
666
  id: "msg-escalation",
667
667
  from: "builder-1",
@@ -682,7 +682,7 @@ describe("createMailStore", () => {
682
682
  test("returns payload in getAll results", () => {
683
683
  const payload = JSON.stringify({
684
684
  branch: "agent/b1",
685
- beadId: "beads-xyz",
685
+ taskId: "beads-xyz",
686
686
  tier: "clean-merge",
687
687
  });
688
688
  store.insert({
@@ -1,3 +1,4 @@
1
+ import { Database } from "bun:sqlite";
1
2
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
3
  import { mkdtemp, rm } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
@@ -22,14 +23,14 @@ describe("createMergeQueue", () => {
22
23
  function makeInput(
23
24
  overrides?: Partial<{
24
25
  branchName: string;
25
- beadId: string;
26
+ taskId: string;
26
27
  agentName: string;
27
28
  filesModified: string[];
28
29
  }>,
29
30
  ) {
30
31
  return {
31
32
  branchName: overrides?.branchName ?? "overstory/test-agent/bead-123",
32
- beadId: overrides?.beadId ?? "bead-123",
33
+ taskId: overrides?.taskId ?? "bead-123",
33
34
  agentName: overrides?.agentName ?? "test-agent",
34
35
  filesModified: overrides?.filesModified ?? ["src/test.ts"],
35
36
  };
@@ -51,7 +52,7 @@ describe("createMergeQueue", () => {
51
52
  const after = new Date().toISOString();
52
53
 
53
54
  expect(entry.branchName).toBe("overstory/test-agent/bead-123");
54
- expect(entry.beadId).toBe("bead-123");
55
+ expect(entry.taskId).toBe("bead-123");
55
56
  expect(entry.agentName).toBe("test-agent");
56
57
  expect(entry.filesModified).toEqual(["src/test.ts"]);
57
58
  expect(entry.enqueuedAt).toBeDefined();
@@ -64,7 +65,7 @@ describe("createMergeQueue", () => {
64
65
  const queue = createMergeQueue(queuePath);
65
66
  const input = makeInput({
66
67
  branchName: "overstory/builder-1/bead-xyz",
67
- beadId: "bead-xyz",
68
+ taskId: "bead-xyz",
68
69
  agentName: "builder-1",
69
70
  filesModified: ["src/a.ts", "src/b.ts"],
70
71
  });
@@ -72,7 +73,7 @@ describe("createMergeQueue", () => {
72
73
  const entry = queue.enqueue(input);
73
74
 
74
75
  expect(entry.branchName).toBe("overstory/builder-1/bead-xyz");
75
- expect(entry.beadId).toBe("bead-xyz");
76
+ expect(entry.taskId).toBe("bead-xyz");
76
77
  expect(entry.agentName).toBe("builder-1");
77
78
  expect(entry.filesModified).toEqual(["src/a.ts", "src/b.ts"]);
78
79
  });
@@ -81,8 +82,8 @@ describe("createMergeQueue", () => {
81
82
  describe("dequeue", () => {
82
83
  test("returns first pending entry (FIFO)", () => {
83
84
  const queue = createMergeQueue(queuePath);
84
- queue.enqueue(makeInput({ branchName: "branch-a", beadId: "bead-a" }));
85
- queue.enqueue(makeInput({ branchName: "branch-b", beadId: "bead-b" }));
85
+ queue.enqueue(makeInput({ branchName: "branch-a", taskId: "bead-a" }));
86
+ queue.enqueue(makeInput({ branchName: "branch-b", taskId: "bead-b" }));
86
87
 
87
88
  const dequeued = queue.dequeue();
88
89
 
@@ -333,6 +334,71 @@ describe("createMergeQueue", () => {
333
334
  });
334
335
  });
335
336
 
337
+ describe("bead_id → task_id migration", () => {
338
+ test("renames bead_id column to task_id on existing databases", () => {
339
+ // Create a database with the old bead_id schema
340
+ const db = new Database(queuePath);
341
+ db.exec("PRAGMA journal_mode = WAL");
342
+ db.exec(`
343
+ CREATE TABLE merge_queue (
344
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
345
+ branch_name TEXT NOT NULL,
346
+ bead_id TEXT NOT NULL,
347
+ agent_name TEXT NOT NULL,
348
+ files_modified TEXT NOT NULL DEFAULT '[]',
349
+ enqueued_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
350
+ status TEXT NOT NULL DEFAULT 'pending'
351
+ CHECK(status IN ('pending','merging','merged','conflict','failed')),
352
+ resolved_tier TEXT
353
+ CHECK(resolved_tier IS NULL OR resolved_tier IN ('clean-merge','auto-resolve','ai-resolve','reimagine'))
354
+ )
355
+ `);
356
+ db.exec(
357
+ "INSERT INTO merge_queue (branch_name, bead_id, agent_name, files_modified, status, enqueued_at) VALUES ('overstory/test/bead-1', 'bead-1', 'test', '[\"src/a.ts\"]', 'pending', '2026-01-01T00:00:00.000')",
358
+ );
359
+ db.close();
360
+
361
+ // Opening with createMergeQueue should migrate and work
362
+ const queue = createMergeQueue(queuePath);
363
+ const entries = queue.list();
364
+
365
+ expect(entries).toHaveLength(1);
366
+ expect(entries[0]?.taskId).toBe("bead-1");
367
+ expect(entries[0]?.branchName).toBe("overstory/test/bead-1");
368
+
369
+ // New inserts should also work
370
+ const newEntry = queue.enqueue({
371
+ branchName: "overstory/test/bead-2",
372
+ taskId: "bead-2",
373
+ agentName: "test",
374
+ filesModified: ["src/b.ts"],
375
+ });
376
+ expect(newEntry.taskId).toBe("bead-2");
377
+
378
+ queue.close();
379
+ });
380
+
381
+ test("no-op when task_id column already exists", () => {
382
+ // Create with current schema (has task_id)
383
+ const queue1 = createMergeQueue(queuePath);
384
+ queue1.enqueue({
385
+ branchName: "overstory/test/bead-1",
386
+ taskId: "bead-1",
387
+ agentName: "test",
388
+ filesModified: [],
389
+ });
390
+ queue1.close();
391
+
392
+ // Re-opening should not error
393
+ const queue2 = createMergeQueue(queuePath);
394
+ const entries = queue2.list();
395
+
396
+ expect(entries).toHaveLength(1);
397
+ expect(entries[0]?.taskId).toBe("bead-1");
398
+ queue2.close();
399
+ });
400
+ });
401
+
336
402
  describe("close", () => {
337
403
  test("closes the database connection", () => {
338
404
  const queue = createMergeQueue(queuePath);
@@ -74,7 +74,7 @@ function rowToEntry(row: MergeQueueRow): MergeEntry {
74
74
 
75
75
  return {
76
76
  branchName: row.branch_name,
77
- beadId: row.task_id,
77
+ taskId: row.task_id,
78
78
  agentName: row.agent_name,
79
79
  filesModified,
80
80
  enqueuedAt: row.enqueued_at,
@@ -83,6 +83,18 @@ function rowToEntry(row: MergeQueueRow): MergeEntry {
83
83
  };
84
84
  }
85
85
 
86
+ /**
87
+ * Migrate an existing merge_queue table from bead_id to task_id column.
88
+ * Safe to call multiple times — only renames if bead_id exists and task_id does not.
89
+ */
90
+ function migrateBeadIdToTaskId(db: Database): void {
91
+ const rows = db.prepare("PRAGMA table_info(merge_queue)").all() as Array<{ name: string }>;
92
+ const existingColumns = new Set(rows.map((r) => r.name));
93
+ if (existingColumns.has("bead_id") && !existingColumns.has("task_id")) {
94
+ db.exec("ALTER TABLE merge_queue RENAME COLUMN bead_id TO task_id");
95
+ }
96
+ }
97
+
86
98
  /**
87
99
  * Create a new MergeQueue backed by a SQLite database at the given path.
88
100
  *
@@ -101,6 +113,9 @@ export function createMergeQueue(dbPath: string): MergeQueue {
101
113
  db.exec(CREATE_TABLE);
102
114
  db.exec(CREATE_INDEXES);
103
115
 
116
+ // Migrate: rename bead_id → task_id on existing tables
117
+ migrateBeadIdToTaskId(db);
118
+
104
119
  // Prepare statements for frequent operations
105
120
  const insertStmt = db.prepare<
106
121
  MergeQueueRow,
@@ -157,7 +172,7 @@ export function createMergeQueue(dbPath: string): MergeQueue {
157
172
 
158
173
  const row = insertStmt.get({
159
174
  $branch_name: input.branchName,
160
- $task_id: input.beadId,
175
+ $task_id: input.taskId,
161
176
  $agent_name: input.agentName,
162
177
  $files_modified: filesModifiedJson,
163
178
  $enqueued_at: enqueuedAt,