@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.
- package/README.md +8 -7
- package/package.json +12 -4
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +131 -16
- package/src/agents/hooks-deployer.ts +33 -1
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/manifest.test.ts +86 -0
- package/src/agents/overlay.test.ts +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +8 -8
- package/src/commands/agents.ts +62 -91
- package/src/commands/clean.test.ts +36 -51
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +133 -26
- package/src/commands/coordinator.ts +101 -64
- package/src/commands/costs.test.ts +47 -47
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +75 -95
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +18 -18
- package/src/commands/inspect.ts +55 -58
- package/src/commands/log.test.ts +26 -31
- package/src/commands/log.ts +97 -91
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +28 -66
- package/src/commands/merge.ts +21 -51
- package/src/commands/metrics.test.ts +8 -8
- package/src/commands/metrics.ts +34 -35
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +57 -62
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +19 -51
- package/src/commands/prime.ts +13 -50
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +201 -5
- package/src/commands/sling.ts +37 -64
- package/src/commands/spec.test.ts +14 -40
- package/src/commands/spec.ts +32 -101
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +63 -58
- package/src/commands/stop.test.ts +22 -40
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +12 -14
- package/src/commands/supervisor.ts +144 -165
- package/src/commands/trace.test.ts +15 -15
- package/src/commands/trace.ts +59 -82
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +213 -37
- package/src/commands/worktree.ts +110 -55
- package/src/config.test.ts +96 -0
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +8 -8
- package/src/errors.ts +1 -1
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +73 -7
- package/src/merge/queue.ts +17 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +2 -2
- package/src/sessions/store.test.ts +41 -4
- package/src/sessions/store.ts +13 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +98 -9
- 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
|
-
|
|
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("
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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("
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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("
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
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("
|
|
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
|
|
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:
|
|
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.
|
|
91
|
-
expect(result.reset).toBe("\x1b[0m");
|
|
89
|
+
expect(result.level).toBe(0);
|
|
92
90
|
});
|
|
93
91
|
|
|
94
|
-
test("FORCE_COLOR
|
|
92
|
+
test("FORCE_COLOR overrides NO_COLOR", async () => {
|
|
95
93
|
const proc = Bun.spawn(
|
|
96
94
|
[
|
|
97
95
|
"bun",
|
|
98
96
|
"-e",
|
|
99
|
-
'import
|
|
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,
|
|
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.
|
|
109
|
+
expect(result.level).toBeGreaterThan(0);
|
|
112
110
|
});
|
|
113
111
|
|
|
114
|
-
test("
|
|
115
|
-
const {
|
|
116
|
-
expect(
|
|
117
|
-
|
|
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("
|
|
118
|
+
test("ColorFn type: color functions accept strings and return strings", async () => {
|
|
124
119
|
const { color } = await import("./color.ts");
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
});
|
package/src/logging/color.ts
CHANGED
|
@@ -1,63 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Central
|
|
2
|
+
* Central color and output control using Chalk.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
10
|
+
// --- Brand palette (os-eco brand colors) ---
|
|
34
11
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
*
|
|
41
|
-
*
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
export
|
|
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("
|
|
109
|
+
test("output is a formatted string with level label and timestamp", () => {
|
|
110
110
|
const result = formatLogLine(makeEvent());
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
expect(result).toContain("
|
|
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
|
|
124
|
-
expect(debugResult).
|
|
125
|
-
expect(infoResult).
|
|
126
|
-
expect(warnResult).
|
|
127
|
-
expect(
|
|
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()", () => {
|
package/src/logging/reporter.ts
CHANGED
|
@@ -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
|
|
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"],
|
|
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
|
|
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
|
|
43
|
+
const dataSuffix = dataPart.length > 0 ? ` ${color.dim(dataPart)}` : "";
|
|
43
44
|
|
|
44
|
-
return `${color.dim
|
|
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
|
-
|
|
33
|
+
taskId: "bead-001",
|
|
34
34
|
tmuxSession: `overstory-test-${agentName}`,
|
|
35
35
|
state: "working",
|
|
36
36
|
pid: 12345,
|
package/src/mail/client.test.ts
CHANGED
|
@@ -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
|
-
|
|
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",
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
730
|
+
taskId: "beads-abc",
|
|
731
731
|
branch: "agent/builder-1",
|
|
732
732
|
exitCode: 0,
|
|
733
733
|
filesModified: ["src/foo.ts"],
|
package/src/mail/store.test.ts
CHANGED
|
@@ -638,7 +638,7 @@ describe("createMailStore", () => {
|
|
|
638
638
|
|
|
639
639
|
test("stores JSON payload string", () => {
|
|
640
640
|
const payload = JSON.stringify({
|
|
641
|
-
|
|
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",
|
|
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
|
-
|
|
685
|
+
taskId: "beads-xyz",
|
|
686
686
|
tier: "clean-merge",
|
|
687
687
|
});
|
|
688
688
|
store.insert({
|
package/src/merge/queue.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|
|
85
|
-
queue.enqueue(makeInput({ branchName: "branch-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);
|
package/src/merge/queue.ts
CHANGED
|
@@ -74,7 +74,7 @@ function rowToEntry(row: MergeQueueRow): MergeEntry {
|
|
|
74
74
|
|
|
75
75
|
return {
|
|
76
76
|
branchName: row.branch_name,
|
|
77
|
-
|
|
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.
|
|
175
|
+
$task_id: input.taskId,
|
|
161
176
|
$agent_name: input.agentName,
|
|
162
177
|
$files_modified: filesModifiedJson,
|
|
163
178
|
$enqueued_at: enqueuedAt,
|