@os-eco/overstory-cli 0.6.1

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/agents/builder.md +137 -0
  4. package/agents/coordinator.md +263 -0
  5. package/agents/lead.md +301 -0
  6. package/agents/merger.md +160 -0
  7. package/agents/monitor.md +214 -0
  8. package/agents/reviewer.md +140 -0
  9. package/agents/scout.md +119 -0
  10. package/agents/supervisor.md +423 -0
  11. package/package.json +47 -0
  12. package/src/agents/checkpoint.test.ts +88 -0
  13. package/src/agents/checkpoint.ts +101 -0
  14. package/src/agents/hooks-deployer.test.ts +2040 -0
  15. package/src/agents/hooks-deployer.ts +607 -0
  16. package/src/agents/identity.test.ts +603 -0
  17. package/src/agents/identity.ts +384 -0
  18. package/src/agents/lifecycle.test.ts +196 -0
  19. package/src/agents/lifecycle.ts +183 -0
  20. package/src/agents/manifest.test.ts +746 -0
  21. package/src/agents/manifest.ts +354 -0
  22. package/src/agents/overlay.test.ts +676 -0
  23. package/src/agents/overlay.ts +308 -0
  24. package/src/beads/client.test.ts +217 -0
  25. package/src/beads/client.ts +202 -0
  26. package/src/beads/molecules.test.ts +338 -0
  27. package/src/beads/molecules.ts +198 -0
  28. package/src/commands/agents.test.ts +322 -0
  29. package/src/commands/agents.ts +287 -0
  30. package/src/commands/clean.test.ts +670 -0
  31. package/src/commands/clean.ts +618 -0
  32. package/src/commands/completions.test.ts +342 -0
  33. package/src/commands/completions.ts +887 -0
  34. package/src/commands/coordinator.test.ts +1530 -0
  35. package/src/commands/coordinator.ts +733 -0
  36. package/src/commands/costs.test.ts +1119 -0
  37. package/src/commands/costs.ts +564 -0
  38. package/src/commands/dashboard.test.ts +308 -0
  39. package/src/commands/dashboard.ts +838 -0
  40. package/src/commands/doctor.test.ts +294 -0
  41. package/src/commands/doctor.ts +213 -0
  42. package/src/commands/errors.test.ts +647 -0
  43. package/src/commands/errors.ts +248 -0
  44. package/src/commands/feed.test.ts +578 -0
  45. package/src/commands/feed.ts +361 -0
  46. package/src/commands/group.test.ts +262 -0
  47. package/src/commands/group.ts +511 -0
  48. package/src/commands/hooks.test.ts +458 -0
  49. package/src/commands/hooks.ts +253 -0
  50. package/src/commands/init.test.ts +347 -0
  51. package/src/commands/init.ts +650 -0
  52. package/src/commands/inspect.test.ts +670 -0
  53. package/src/commands/inspect.ts +431 -0
  54. package/src/commands/log.test.ts +1454 -0
  55. package/src/commands/log.ts +724 -0
  56. package/src/commands/logs.test.ts +379 -0
  57. package/src/commands/logs.ts +546 -0
  58. package/src/commands/mail.test.ts +1270 -0
  59. package/src/commands/mail.ts +771 -0
  60. package/src/commands/merge.test.ts +670 -0
  61. package/src/commands/merge.ts +355 -0
  62. package/src/commands/metrics.test.ts +444 -0
  63. package/src/commands/metrics.ts +143 -0
  64. package/src/commands/monitor.test.ts +191 -0
  65. package/src/commands/monitor.ts +390 -0
  66. package/src/commands/nudge.test.ts +230 -0
  67. package/src/commands/nudge.ts +372 -0
  68. package/src/commands/prime.test.ts +470 -0
  69. package/src/commands/prime.ts +381 -0
  70. package/src/commands/replay.test.ts +741 -0
  71. package/src/commands/replay.ts +360 -0
  72. package/src/commands/run.test.ts +431 -0
  73. package/src/commands/run.ts +351 -0
  74. package/src/commands/sling.test.ts +657 -0
  75. package/src/commands/sling.ts +661 -0
  76. package/src/commands/spec.test.ts +203 -0
  77. package/src/commands/spec.ts +168 -0
  78. package/src/commands/status.test.ts +430 -0
  79. package/src/commands/status.ts +398 -0
  80. package/src/commands/stop.test.ts +420 -0
  81. package/src/commands/stop.ts +151 -0
  82. package/src/commands/supervisor.test.ts +187 -0
  83. package/src/commands/supervisor.ts +535 -0
  84. package/src/commands/trace.test.ts +745 -0
  85. package/src/commands/trace.ts +325 -0
  86. package/src/commands/watch.test.ts +145 -0
  87. package/src/commands/watch.ts +247 -0
  88. package/src/commands/worktree.test.ts +786 -0
  89. package/src/commands/worktree.ts +311 -0
  90. package/src/config.test.ts +822 -0
  91. package/src/config.ts +829 -0
  92. package/src/doctor/agents.test.ts +454 -0
  93. package/src/doctor/agents.ts +396 -0
  94. package/src/doctor/config-check.test.ts +190 -0
  95. package/src/doctor/config-check.ts +183 -0
  96. package/src/doctor/consistency.test.ts +651 -0
  97. package/src/doctor/consistency.ts +294 -0
  98. package/src/doctor/databases.test.ts +290 -0
  99. package/src/doctor/databases.ts +218 -0
  100. package/src/doctor/dependencies.test.ts +184 -0
  101. package/src/doctor/dependencies.ts +175 -0
  102. package/src/doctor/logs.test.ts +251 -0
  103. package/src/doctor/logs.ts +295 -0
  104. package/src/doctor/merge-queue.test.ts +216 -0
  105. package/src/doctor/merge-queue.ts +144 -0
  106. package/src/doctor/structure.test.ts +291 -0
  107. package/src/doctor/structure.ts +198 -0
  108. package/src/doctor/types.ts +37 -0
  109. package/src/doctor/version.test.ts +136 -0
  110. package/src/doctor/version.ts +129 -0
  111. package/src/e2e/init-sling-lifecycle.test.ts +277 -0
  112. package/src/errors.ts +217 -0
  113. package/src/events/store.test.ts +660 -0
  114. package/src/events/store.ts +369 -0
  115. package/src/events/tool-filter.test.ts +330 -0
  116. package/src/events/tool-filter.ts +126 -0
  117. package/src/index.ts +316 -0
  118. package/src/insights/analyzer.test.ts +466 -0
  119. package/src/insights/analyzer.ts +203 -0
  120. package/src/logging/color.test.ts +142 -0
  121. package/src/logging/color.ts +71 -0
  122. package/src/logging/logger.test.ts +813 -0
  123. package/src/logging/logger.ts +266 -0
  124. package/src/logging/reporter.test.ts +259 -0
  125. package/src/logging/reporter.ts +109 -0
  126. package/src/logging/sanitizer.test.ts +190 -0
  127. package/src/logging/sanitizer.ts +57 -0
  128. package/src/mail/broadcast.test.ts +203 -0
  129. package/src/mail/broadcast.ts +92 -0
  130. package/src/mail/client.test.ts +773 -0
  131. package/src/mail/client.ts +223 -0
  132. package/src/mail/store.test.ts +705 -0
  133. package/src/mail/store.ts +387 -0
  134. package/src/merge/queue.test.ts +359 -0
  135. package/src/merge/queue.ts +231 -0
  136. package/src/merge/resolver.test.ts +1345 -0
  137. package/src/merge/resolver.ts +645 -0
  138. package/src/metrics/store.test.ts +667 -0
  139. package/src/metrics/store.ts +445 -0
  140. package/src/metrics/summary.test.ts +398 -0
  141. package/src/metrics/summary.ts +178 -0
  142. package/src/metrics/transcript.test.ts +356 -0
  143. package/src/metrics/transcript.ts +175 -0
  144. package/src/mulch/client.test.ts +671 -0
  145. package/src/mulch/client.ts +332 -0
  146. package/src/sessions/compat.test.ts +280 -0
  147. package/src/sessions/compat.ts +104 -0
  148. package/src/sessions/store.test.ts +873 -0
  149. package/src/sessions/store.ts +494 -0
  150. package/src/test-helpers.test.ts +124 -0
  151. package/src/test-helpers.ts +126 -0
  152. package/src/tracker/beads.ts +56 -0
  153. package/src/tracker/factory.test.ts +80 -0
  154. package/src/tracker/factory.ts +64 -0
  155. package/src/tracker/seeds.ts +182 -0
  156. package/src/tracker/types.ts +52 -0
  157. package/src/types.ts +724 -0
  158. package/src/watchdog/daemon.test.ts +1975 -0
  159. package/src/watchdog/daemon.ts +671 -0
  160. package/src/watchdog/health.test.ts +431 -0
  161. package/src/watchdog/health.ts +264 -0
  162. package/src/watchdog/triage.test.ts +164 -0
  163. package/src/watchdog/triage.ts +179 -0
  164. package/src/worktree/manager.test.ts +439 -0
  165. package/src/worktree/manager.ts +198 -0
  166. package/src/worktree/tmux.test.ts +1009 -0
  167. package/src/worktree/tmux.ts +509 -0
  168. package/templates/CLAUDE.md.tmpl +89 -0
  169. package/templates/hooks.json.tmpl +105 -0
  170. package/templates/overlay.md.tmpl +81 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Tests for the `overstory spec` command.
3
+ *
4
+ * Uses real filesystem (temp dirs) for all tests. No mocks.
5
+ * Philosophy: "never mock what you can use for real" (mx-252b16).
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+ import { mkdir } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
12
+ import { specCommand, writeSpec } from "./spec.ts";
13
+
14
+ let tempDir: string;
15
+ let overstoryDir: string;
16
+ let originalCwd: string;
17
+ let stdoutOutput: string;
18
+ let _stderrOutput: string;
19
+ let originalStdoutWrite: typeof process.stdout.write;
20
+ let originalStderrWrite: typeof process.stderr.write;
21
+
22
+ beforeEach(async () => {
23
+ tempDir = await createTempGitRepo();
24
+ overstoryDir = join(tempDir, ".overstory");
25
+ await mkdir(overstoryDir, { recursive: true });
26
+
27
+ // Write minimal config.yaml so resolveProjectRoot works
28
+ await Bun.write(
29
+ join(overstoryDir, "config.yaml"),
30
+ `project:\n name: test-project\n root: ${tempDir}\n canonicalBranch: main\n`,
31
+ );
32
+
33
+ originalCwd = process.cwd();
34
+ process.chdir(tempDir);
35
+
36
+ // Capture stdout/stderr
37
+ stdoutOutput = "";
38
+ _stderrOutput = "";
39
+ originalStdoutWrite = process.stdout.write;
40
+ originalStderrWrite = process.stderr.write;
41
+ process.stdout.write = ((chunk: string) => {
42
+ stdoutOutput += chunk;
43
+ return true;
44
+ }) as typeof process.stdout.write;
45
+ process.stderr.write = ((chunk: string) => {
46
+ _stderrOutput += chunk;
47
+ return true;
48
+ }) as typeof process.stderr.write;
49
+ });
50
+
51
+ afterEach(async () => {
52
+ process.chdir(originalCwd);
53
+ process.stdout.write = originalStdoutWrite;
54
+ process.stderr.write = originalStderrWrite;
55
+ await cleanupTempDir(tempDir);
56
+ });
57
+
58
+ // === help ===
59
+
60
+ describe("help", () => {
61
+ test("--help shows usage", async () => {
62
+ await specCommand(["--help"]);
63
+ expect(stdoutOutput).toContain("overstory spec");
64
+ expect(stdoutOutput).toContain("write");
65
+ expect(stdoutOutput).toContain("--body");
66
+ expect(stdoutOutput).toContain("--agent");
67
+ });
68
+
69
+ test("-h shows usage", async () => {
70
+ await specCommand(["-h"]);
71
+ expect(stdoutOutput).toContain("overstory spec");
72
+ });
73
+
74
+ test("no args shows help", async () => {
75
+ await specCommand([]);
76
+ expect(stdoutOutput).toContain("overstory spec");
77
+ });
78
+ });
79
+
80
+ // === validation ===
81
+
82
+ describe("validation", () => {
83
+ test("unknown subcommand throws ValidationError", async () => {
84
+ await expect(specCommand(["unknown"])).rejects.toThrow("Unknown spec subcommand");
85
+ });
86
+
87
+ test("write without bead-id throws ValidationError", async () => {
88
+ await expect(specCommand(["write"])).rejects.toThrow("Bead ID is required");
89
+ });
90
+
91
+ test("write without body throws ValidationError", async () => {
92
+ await expect(specCommand(["write", "task-abc", "--agent", "scout-1"])).rejects.toThrow(
93
+ "Spec body is required",
94
+ );
95
+ });
96
+
97
+ test("write with empty body throws ValidationError", async () => {
98
+ await expect(specCommand(["write", "task-abc", "--body", " "])).rejects.toThrow(
99
+ "Spec body is required",
100
+ );
101
+ });
102
+ });
103
+
104
+ // === writeSpec (core function) ===
105
+
106
+ describe("writeSpec", () => {
107
+ test("writes spec file to .overstory/specs/<bead-id>.md", async () => {
108
+ const specPath = await writeSpec(tempDir, "task-abc", "# My Spec\n\nDetails here.");
109
+
110
+ expect(specPath).toBe(join(tempDir, ".overstory", "specs", "task-abc.md"));
111
+
112
+ const content = await Bun.file(specPath).text();
113
+ expect(content).toBe("# My Spec\n\nDetails here.\n");
114
+ });
115
+
116
+ test("creates specs directory if it does not exist", async () => {
117
+ // Verify specs dir does not exist yet
118
+ const specsDir = join(overstoryDir, "specs");
119
+ expect(await Bun.file(join(specsDir, ".gitkeep")).exists()).toBe(false);
120
+
121
+ await writeSpec(tempDir, "task-xyz", "content");
122
+
123
+ const content = await Bun.file(join(specsDir, "task-xyz.md")).text();
124
+ expect(content).toBe("content\n");
125
+ });
126
+
127
+ test("adds attribution header when agent is provided", async () => {
128
+ const specPath = await writeSpec(tempDir, "task-123", "# Spec body", "scout-1");
129
+
130
+ const content = await Bun.file(specPath).text();
131
+ expect(content).toContain("<!-- written-by: scout-1 -->");
132
+ expect(content).toContain("# Spec body");
133
+ });
134
+
135
+ test("does not add attribution header when agent is omitted", async () => {
136
+ const specPath = await writeSpec(tempDir, "task-456", "# Spec body");
137
+
138
+ const content = await Bun.file(specPath).text();
139
+ expect(content).not.toContain("written-by");
140
+ expect(content).toBe("# Spec body\n");
141
+ });
142
+
143
+ test("ensures trailing newline", async () => {
144
+ const specPath = await writeSpec(tempDir, "task-nl", "no newline at end");
145
+
146
+ const content = await Bun.file(specPath).text();
147
+ expect(content.endsWith("\n")).toBe(true);
148
+ });
149
+
150
+ test("does not double trailing newline", async () => {
151
+ const specPath = await writeSpec(tempDir, "task-nl2", "already has newline\n");
152
+
153
+ const content = await Bun.file(specPath).text();
154
+ expect(content).toBe("already has newline\n");
155
+ expect(content.endsWith("\n\n")).toBe(false);
156
+ });
157
+
158
+ test("overwrites existing spec file", async () => {
159
+ await writeSpec(tempDir, "task-ow", "version 1");
160
+ await writeSpec(tempDir, "task-ow", "version 2");
161
+
162
+ const specPath = join(overstoryDir, "specs", "task-ow.md");
163
+ const content = await Bun.file(specPath).text();
164
+ expect(content).toBe("version 2\n");
165
+ });
166
+ });
167
+
168
+ // === specCommand (CLI integration) ===
169
+
170
+ describe("specCommand write", () => {
171
+ test("writes spec and prints path", async () => {
172
+ await specCommand(["write", "task-cmd", "--body", "# CLI Spec"]);
173
+
174
+ // Path may differ due to macOS /var -> /private/var symlink resolution
175
+ expect(stdoutOutput.trim()).toContain(".overstory/specs/task-cmd.md");
176
+
177
+ const specPath = stdoutOutput.trim();
178
+ const content = await Bun.file(specPath).text();
179
+ expect(content).toBe("# CLI Spec\n");
180
+ });
181
+
182
+ test("writes spec with agent attribution", async () => {
183
+ await specCommand(["write", "task-attr", "--body", "# Attributed", "--agent", "scout-2"]);
184
+
185
+ expect(stdoutOutput.trim()).toContain(".overstory/specs/task-attr.md");
186
+
187
+ const specPath = stdoutOutput.trim();
188
+ const content = await Bun.file(specPath).text();
189
+ expect(content).toContain("<!-- written-by: scout-2 -->");
190
+ expect(content).toContain("# Attributed");
191
+ });
192
+
193
+ test("flags can appear in any order", async () => {
194
+ await specCommand(["write", "--agent", "scout-3", "--body", "# Content", "task-order"]);
195
+
196
+ expect(stdoutOutput.trim()).toContain(".overstory/specs/task-order.md");
197
+
198
+ const specPath = stdoutOutput.trim();
199
+ const content = await Bun.file(specPath).text();
200
+ expect(content).toContain("<!-- written-by: scout-3 -->");
201
+ expect(content).toContain("# Content");
202
+ });
203
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * CLI command: overstory spec write <bead-id> --body <content>
3
+ *
4
+ * Writes a task specification to `.overstory/specs/<bead-id>.md`.
5
+ * Scouts use this to persist spec documents as files instead of
6
+ * sending entire specs via mail messages.
7
+ *
8
+ * Supports reading body content from --body flag or stdin.
9
+ */
10
+
11
+ import { mkdir } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { ValidationError } from "../errors.ts";
14
+
15
+ /** Boolean flags that do NOT consume the next arg. */
16
+ const BOOLEAN_FLAGS = new Set(["--help", "-h"]);
17
+
18
+ /**
19
+ * Parse a named flag value from args.
20
+ */
21
+ function getFlag(args: string[], flag: string): string | undefined {
22
+ const idx = args.indexOf(flag);
23
+ if (idx === -1 || idx + 1 >= args.length) {
24
+ return undefined;
25
+ }
26
+ return args[idx + 1];
27
+ }
28
+
29
+ /**
30
+ * Extract positional arguments, skipping flag-value pairs.
31
+ */
32
+ function getPositionalArgs(args: string[]): string[] {
33
+ const positional: string[] = [];
34
+ let i = 0;
35
+ while (i < args.length) {
36
+ const arg = args[i];
37
+ if (arg?.startsWith("-")) {
38
+ if (BOOLEAN_FLAGS.has(arg)) {
39
+ i += 1;
40
+ } else {
41
+ i += 2;
42
+ }
43
+ } else {
44
+ if (arg !== undefined) {
45
+ positional.push(arg);
46
+ }
47
+ i += 1;
48
+ }
49
+ }
50
+ return positional;
51
+ }
52
+
53
+ /**
54
+ * Read all of stdin as a string. Returns empty string if stdin is a TTY
55
+ * (no piped input).
56
+ */
57
+ async function readStdin(): Promise<string> {
58
+ // Bun.stdin is a ReadableStream when piped, a TTY otherwise
59
+ if (process.stdin.isTTY) {
60
+ return "";
61
+ }
62
+ return await new Response(Bun.stdin.stream()).text();
63
+ }
64
+
65
+ const SPEC_HELP = `overstory spec -- Manage task specifications
66
+
67
+ Usage: overstory spec <subcommand> [args...]
68
+
69
+ Subcommands:
70
+ write <bead-id> Write a spec file to .overstory/specs/<bead-id>.md
71
+
72
+ Options for 'write':
73
+ --body <content> Spec content (or pipe via stdin)
74
+ --agent <name> Agent writing the spec (for attribution)
75
+ --help, -h Show this help
76
+
77
+ Examples:
78
+ overstory spec write task-abc --body "# Spec\\nDetails here..."
79
+ echo "# Spec" | overstory spec write task-abc
80
+ overstory spec write task-abc --body "..." --agent scout-1`;
81
+
82
+ /**
83
+ * Write a spec file to .overstory/specs/<bead-id>.md.
84
+ *
85
+ * Exported for direct use in tests.
86
+ */
87
+ export async function writeSpec(
88
+ projectRoot: string,
89
+ beadId: string,
90
+ body: string,
91
+ agent?: string,
92
+ ): Promise<string> {
93
+ const specsDir = join(projectRoot, ".overstory", "specs");
94
+ await mkdir(specsDir, { recursive: true });
95
+
96
+ // Build the spec content with optional attribution header
97
+ let content = "";
98
+ if (agent) {
99
+ content += `<!-- written-by: ${agent} -->\n`;
100
+ }
101
+ content += body;
102
+
103
+ // Ensure trailing newline
104
+ if (!content.endsWith("\n")) {
105
+ content += "\n";
106
+ }
107
+
108
+ const specPath = join(specsDir, `${beadId}.md`);
109
+ await Bun.write(specPath, content);
110
+
111
+ return specPath;
112
+ }
113
+
114
+ /**
115
+ * Entry point for `overstory spec <subcommand>`.
116
+ */
117
+ export async function specCommand(args: string[]): Promise<void> {
118
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
119
+ process.stdout.write(`${SPEC_HELP}\n`);
120
+ return;
121
+ }
122
+
123
+ const subcommand = args[0];
124
+ const subArgs = args.slice(1);
125
+
126
+ switch (subcommand) {
127
+ case "write": {
128
+ const positional = getPositionalArgs(subArgs);
129
+ const beadId = positional[0];
130
+ if (!beadId || beadId.trim().length === 0) {
131
+ throw new ValidationError(
132
+ "Bead ID is required: overstory spec write <bead-id> --body <content>",
133
+ { field: "beadId" },
134
+ );
135
+ }
136
+
137
+ const agent = getFlag(subArgs, "--agent");
138
+ let body = getFlag(subArgs, "--body");
139
+
140
+ // If no --body flag, try reading from stdin
141
+ if (body === undefined) {
142
+ const stdinContent = await readStdin();
143
+ if (stdinContent.trim().length > 0) {
144
+ body = stdinContent;
145
+ }
146
+ }
147
+
148
+ if (body === undefined || body.trim().length === 0) {
149
+ throw new ValidationError("Spec body is required: use --body <content> or pipe via stdin", {
150
+ field: "body",
151
+ });
152
+ }
153
+
154
+ const { resolveProjectRoot } = await import("../config.ts");
155
+ const projectRoot = await resolveProjectRoot(process.cwd());
156
+
157
+ const specPath = await writeSpec(projectRoot, beadId, body, agent);
158
+ process.stdout.write(`${specPath}\n`);
159
+ break;
160
+ }
161
+
162
+ default:
163
+ throw new ValidationError(
164
+ `Unknown spec subcommand: ${subcommand}. Run 'overstory spec --help' for usage.`,
165
+ { field: "subcommand", value: subcommand },
166
+ );
167
+ }
168
+ }