@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,126 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * Git environment variables for test repos.
7
+ * Using env vars instead of per-repo `git config` eliminates 2 subprocess
8
+ * spawns per repo creation.
9
+ */
10
+ const GIT_TEST_ENV = {
11
+ GIT_AUTHOR_NAME: "Overstory Test",
12
+ GIT_AUTHOR_EMAIL: "test@overstory.dev",
13
+ GIT_COMMITTER_NAME: "Overstory Test",
14
+ GIT_COMMITTER_EMAIL: "test@overstory.dev",
15
+ };
16
+
17
+ /** Cached template repo path. Created lazily on first call. */
18
+ let _templateDir: string | null = null;
19
+
20
+ /**
21
+ * Get or create a template git repo with an initial commit.
22
+ * All test repos clone from this template (1 subprocess instead of 5).
23
+ */
24
+ async function getTemplateRepo(): Promise<string> {
25
+ if (_templateDir) return _templateDir;
26
+
27
+ const dir = await mkdtemp(join(tmpdir(), "overstory-template-"));
28
+ await runGitInDir(dir, ["init", "-b", "main"]);
29
+ await Bun.write(join(dir, ".gitkeep"), "");
30
+ await runGitInDir(dir, ["add", ".gitkeep"]);
31
+ await runGitInDir(dir, ["commit", "-m", "initial commit"]);
32
+
33
+ _templateDir = dir;
34
+ return dir;
35
+ }
36
+
37
+ /**
38
+ * Create a temporary directory with a real git repo initialized.
39
+ * Includes an initial commit so branches can be created immediately.
40
+ *
41
+ * Uses a cached template repo + `git clone --local` for speed:
42
+ * 1 subprocess per call instead of 5.
43
+ *
44
+ * @returns The absolute path to the temp git repo.
45
+ */
46
+ export async function createTempGitRepo(): Promise<string> {
47
+ const template = await getTemplateRepo();
48
+ const dir = await mkdtemp(join(tmpdir(), "overstory-test-"));
49
+ // Clone into the empty dir. Avoid --local (hardlinks trigger EFAULT in Bun's rm).
50
+ await runGitInDir(".", ["clone", template, dir]);
51
+ // Set git identity at repo level so code that doesn't use GIT_TEST_ENV
52
+ // (e.g., resolver's runGit) can still commit. Locally this is covered by
53
+ // ~/.gitconfig, but CI runners have no global git identity.
54
+ await runGitInDir(dir, ["config", "user.name", "Overstory Test"]);
55
+ await runGitInDir(dir, ["config", "user.email", "test@overstory.dev"]);
56
+ return dir;
57
+ }
58
+
59
+ /**
60
+ * Add and commit a file to a git repo.
61
+ *
62
+ * @param repoDir - Absolute path to the git repo
63
+ * @param filePath - Relative path within the repo (e.g. "src/foo.ts")
64
+ * @param content - File content to write
65
+ * @param message - Commit message (defaults to "add {filePath}")
66
+ */
67
+ export async function commitFile(
68
+ repoDir: string,
69
+ filePath: string,
70
+ content: string,
71
+ message?: string,
72
+ ): Promise<void> {
73
+ const fullPath = join(repoDir, filePath);
74
+
75
+ // Ensure parent directories exist
76
+ const parentDir = join(fullPath, "..");
77
+ const { mkdir } = await import("node:fs/promises");
78
+ await mkdir(parentDir, { recursive: true });
79
+
80
+ await Bun.write(fullPath, content);
81
+ await runGitInDir(repoDir, ["add", filePath]);
82
+ await runGitInDir(repoDir, ["commit", "-m", message ?? `add ${filePath}`]);
83
+ }
84
+
85
+ /**
86
+ * Get the default branch name of a git repo (e.g., "main" or "master").
87
+ * Uses `git symbolic-ref --short HEAD` to read the current branch.
88
+ *
89
+ * Useful in tests to avoid hardcoding "main" -- CI runners may default to "master".
90
+ */
91
+ export async function getDefaultBranch(repoDir: string): Promise<string> {
92
+ const stdout = await runGitInDir(repoDir, ["symbolic-ref", "--short", "HEAD"]);
93
+ return stdout.trim();
94
+ }
95
+
96
+ /**
97
+ * Remove a temp directory. Safe to call even if the directory doesn't exist.
98
+ */
99
+ export async function cleanupTempDir(dir: string): Promise<void> {
100
+ await rm(dir, { recursive: true, force: true });
101
+ }
102
+
103
+ /**
104
+ * Run a git command in the given directory. Throws on non-zero exit.
105
+ * Passes GIT_AUTHOR/COMMITTER env vars so repos don't need per-repo config.
106
+ */
107
+ export async function runGitInDir(cwd: string, args: string[]): Promise<string> {
108
+ const proc = Bun.spawn(["git", ...args], {
109
+ cwd,
110
+ stdout: "pipe",
111
+ stderr: "pipe",
112
+ env: { ...process.env, ...GIT_TEST_ENV },
113
+ });
114
+
115
+ const [stdout, stderr, exitCode] = await Promise.all([
116
+ new Response(proc.stdout).text(),
117
+ new Response(proc.stderr).text(),
118
+ proc.exited,
119
+ ]);
120
+
121
+ if (exitCode !== 0) {
122
+ throw new Error(`git ${args.join(" ")} failed (exit ${exitCode}): ${stderr.trim()}`);
123
+ }
124
+
125
+ return stdout;
126
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Beads tracker adapter.
3
+ *
4
+ * Wraps src/beads/client.ts to implement the unified TrackerClient interface.
5
+ */
6
+
7
+ import { createBeadsClient } from "../beads/client.ts";
8
+ import { AgentError } from "../errors.ts";
9
+ import type { TrackerClient, TrackerIssue } from "./types.ts";
10
+
11
+ /**
12
+ * Create a TrackerClient backed by the beads (bd) CLI.
13
+ *
14
+ * @param cwd - Working directory for bd commands
15
+ */
16
+ export function createBeadsTracker(cwd: string): TrackerClient {
17
+ const client = createBeadsClient(cwd);
18
+
19
+ return {
20
+ async ready() {
21
+ const issues = await client.ready();
22
+ return issues as TrackerIssue[];
23
+ },
24
+
25
+ async show(id) {
26
+ const issue = await client.show(id);
27
+ return issue as TrackerIssue;
28
+ },
29
+
30
+ async create(title, options) {
31
+ return client.create(title, options);
32
+ },
33
+
34
+ async claim(id) {
35
+ return client.claim(id);
36
+ },
37
+
38
+ async close(id, reason) {
39
+ return client.close(id, reason);
40
+ },
41
+
42
+ async list(options) {
43
+ const issues = await client.list(options);
44
+ return issues as TrackerIssue[];
45
+ },
46
+
47
+ async sync() {
48
+ const proc = Bun.spawn(["bd", "sync"], { cwd, stdout: "pipe", stderr: "pipe" });
49
+ const exitCode = await proc.exited;
50
+ if (exitCode !== 0) {
51
+ const stderr = await new Response(proc.stderr).text();
52
+ throw new AgentError(`bd sync failed (exit ${exitCode}): ${stderr.trim()}`);
53
+ }
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createTrackerClient, resolveBackend, trackerCliName } from "./factory.ts";
6
+
7
+ describe("createTrackerClient", () => {
8
+ test("creates beads tracker for beads backend", () => {
9
+ const client = createTrackerClient("beads", "/tmp");
10
+ expect(client).toBeDefined();
11
+ expect(client.ready).toBeTypeOf("function");
12
+ expect(client.show).toBeTypeOf("function");
13
+ expect(client.create).toBeTypeOf("function");
14
+ expect(client.claim).toBeTypeOf("function");
15
+ expect(client.close).toBeTypeOf("function");
16
+ expect(client.list).toBeTypeOf("function");
17
+ expect(client.sync).toBeTypeOf("function");
18
+ });
19
+
20
+ test("creates seeds tracker for seeds backend", () => {
21
+ const client = createTrackerClient("seeds", "/tmp");
22
+ expect(client).toBeDefined();
23
+ expect(client.ready).toBeTypeOf("function");
24
+ expect(client.show).toBeTypeOf("function");
25
+ expect(client.create).toBeTypeOf("function");
26
+ expect(client.claim).toBeTypeOf("function");
27
+ expect(client.close).toBeTypeOf("function");
28
+ expect(client.list).toBeTypeOf("function");
29
+ expect(client.sync).toBeTypeOf("function");
30
+ });
31
+
32
+ test("throws for invalid backend", () => {
33
+ // @ts-expect-error - intentionally testing runtime guard
34
+ expect(() => createTrackerClient("invalid", "/tmp")).toThrow();
35
+ });
36
+ });
37
+
38
+ describe("resolveBackend", () => {
39
+ test("returns beads for beads backend", async () => {
40
+ expect(await resolveBackend("beads", "/tmp")).toBe("beads");
41
+ });
42
+ test("returns seeds for seeds backend", async () => {
43
+ expect(await resolveBackend("seeds", "/tmp")).toBe("seeds");
44
+ });
45
+ test("returns seeds for auto when no tracker dirs exist", async () => {
46
+ const tempDir = await mkdtemp(join(tmpdir(), "tracker-test-"));
47
+ try {
48
+ expect(await resolveBackend("auto", tempDir)).toBe("seeds");
49
+ } finally {
50
+ await rm(tempDir, { recursive: true });
51
+ }
52
+ });
53
+ test("returns seeds for auto when .seeds/ exists", async () => {
54
+ const tempDir = await mkdtemp(join(tmpdir(), "tracker-test-"));
55
+ try {
56
+ await mkdir(join(tempDir, ".seeds"));
57
+ expect(await resolveBackend("auto", tempDir)).toBe("seeds");
58
+ } finally {
59
+ await rm(tempDir, { recursive: true });
60
+ }
61
+ });
62
+ test("returns beads for auto when .beads/ exists", async () => {
63
+ const tempDir = await mkdtemp(join(tmpdir(), "tracker-test-"));
64
+ try {
65
+ await mkdir(join(tempDir, ".beads"));
66
+ expect(await resolveBackend("auto", tempDir)).toBe("beads");
67
+ } finally {
68
+ await rm(tempDir, { recursive: true });
69
+ }
70
+ });
71
+ });
72
+
73
+ describe("trackerCliName", () => {
74
+ test("returns bd for beads", () => {
75
+ expect(trackerCliName("beads")).toBe("bd");
76
+ });
77
+ test("returns sd for seeds", () => {
78
+ expect(trackerCliName("seeds")).toBe("sd");
79
+ });
80
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Tracker factory — creates the right backend client based on configuration.
3
+ */
4
+
5
+ import { stat } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import type { TaskTrackerBackend } from "../types.ts";
8
+ import { createBeadsTracker } from "./beads.ts";
9
+ import { createSeedsTracker } from "./seeds.ts";
10
+ import type { TrackerBackend, TrackerClient } from "./types.ts";
11
+
12
+ /**
13
+ * Create a tracker client for the specified backend.
14
+ *
15
+ * @param backend - Which backend to use ("beads" or "seeds")
16
+ * @param cwd - Working directory for CLI commands
17
+ */
18
+ export function createTrackerClient(backend: TrackerBackend, cwd: string): TrackerClient {
19
+ switch (backend) {
20
+ case "beads":
21
+ return createBeadsTracker(cwd);
22
+ case "seeds":
23
+ return createSeedsTracker(cwd);
24
+ default: {
25
+ const _exhaustive: never = backend;
26
+ throw new Error(`Unknown tracker backend: ${_exhaustive}`);
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Resolve "auto" to a concrete backend by probing the filesystem.
33
+ * Explicit "beads" or "seeds" values pass through unchanged.
34
+ */
35
+ export async function resolveBackend(
36
+ configBackend: TaskTrackerBackend,
37
+ cwd: string,
38
+ ): Promise<TrackerBackend> {
39
+ if (configBackend === "beads") return "beads";
40
+ if (configBackend === "seeds") return "seeds";
41
+ // "auto" detection: check for .seeds/ directory first (newer tool), then .beads/
42
+ const dirExists = async (path: string): Promise<boolean> => {
43
+ try {
44
+ const s = await stat(path);
45
+ return s.isDirectory();
46
+ } catch {
47
+ return false;
48
+ }
49
+ };
50
+ if (await dirExists(join(cwd, ".seeds"))) return "seeds";
51
+ if (await dirExists(join(cwd, ".beads"))) return "beads";
52
+ // Default fallback — seeds is the preferred tracker
53
+ return "seeds";
54
+ }
55
+
56
+ /**
57
+ * Return the CLI tool name for a resolved backend.
58
+ */
59
+ export function trackerCliName(backend: TrackerBackend): string {
60
+ return backend === "seeds" ? "sd" : "bd";
61
+ }
62
+
63
+ // Re-export types for convenience
64
+ export type { TrackerBackend, TrackerClient, TrackerIssue } from "./types.ts";
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Seeds tracker adapter.
3
+ *
4
+ * Implements the unified TrackerClient interface by calling the `sd` CLI directly
5
+ * via Bun.spawn. Seeds uses a { success, command, ...data } JSON envelope.
6
+ */
7
+
8
+ import { AgentError } from "../errors.ts";
9
+ import type { TrackerClient, TrackerIssue } from "./types.ts";
10
+
11
+ /**
12
+ * Run an sd command and return its output.
13
+ */
14
+ async function runSd(
15
+ args: string[],
16
+ cwd: string,
17
+ context: string,
18
+ ): Promise<{ stdout: string; stderr: string }> {
19
+ const proc = Bun.spawn(["sd", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
20
+ const stdout = await new Response(proc.stdout).text();
21
+ const stderr = await new Response(proc.stderr).text();
22
+ const exitCode = await proc.exited;
23
+ if (exitCode !== 0) {
24
+ throw new AgentError(`sd ${context} failed (exit ${exitCode}): ${stderr.trim()}`);
25
+ }
26
+ return { stdout, stderr };
27
+ }
28
+
29
+ /**
30
+ * Parse JSON from sd output, stripping any non-JSON prefix lines.
31
+ */
32
+ function parseSdJson<T>(stdout: string, context: string): T {
33
+ const trimmed = stdout.trim();
34
+ if (trimmed === "") {
35
+ throw new AgentError(`Empty output from sd ${context}`);
36
+ }
37
+ // Seeds may emit non-JSON lines before the JSON object; find the first '{' or '['
38
+ const jsonStart = trimmed.search(/[{[]/);
39
+ const jsonStr = jsonStart >= 0 ? trimmed.slice(jsonStart) : trimmed;
40
+ try {
41
+ return JSON.parse(jsonStr) as T;
42
+ } catch {
43
+ throw new AgentError(
44
+ `Failed to parse JSON output from sd ${context}: ${trimmed.slice(0, 200)}`,
45
+ );
46
+ }
47
+ }
48
+
49
+ /** Base envelope shape shared by all sd JSON responses. */
50
+ interface SdEnvelopeBase {
51
+ success: boolean;
52
+ command: string;
53
+ error?: string;
54
+ }
55
+
56
+ /** Seeds JSON envelope for list-style responses. */
57
+ interface SdListEnvelope extends SdEnvelopeBase {
58
+ issues: SdRawIssue[];
59
+ }
60
+
61
+ /** Seeds JSON envelope for single-issue responses. */
62
+ interface SdShowEnvelope extends SdEnvelopeBase {
63
+ issue: SdRawIssue;
64
+ }
65
+
66
+ /** Seeds JSON envelope for create responses. */
67
+ interface SdCreateEnvelope extends SdEnvelopeBase {
68
+ id?: string;
69
+ issue?: { id: string };
70
+ }
71
+
72
+ /**
73
+ * Validate that an sd envelope indicates success.
74
+ * Throws AgentError if the envelope reports failure.
75
+ */
76
+ function assertEnvelopeSuccess(envelope: SdEnvelopeBase, context: string): void {
77
+ if (envelope.success === false) {
78
+ const detail = envelope.error ?? "unknown error";
79
+ throw new AgentError(`sd ${context} returned failure: ${detail}`);
80
+ }
81
+ }
82
+
83
+ /** Raw issue shape from the sd CLI. Seeds uses `type` directly (no issue_type mapping). */
84
+ interface SdRawIssue {
85
+ id: string;
86
+ title: string;
87
+ status: string;
88
+ priority: number;
89
+ type: string;
90
+ assignee?: string;
91
+ description?: string;
92
+ blocks?: string[];
93
+ blockedBy?: string[];
94
+ }
95
+
96
+ function normalizeIssue(raw: SdRawIssue): TrackerIssue {
97
+ return {
98
+ id: raw.id,
99
+ title: raw.title,
100
+ status: raw.status,
101
+ priority: raw.priority,
102
+ type: raw.type ?? "unknown",
103
+ assignee: raw.assignee,
104
+ description: raw.description,
105
+ blocks: raw.blocks,
106
+ blockedBy: raw.blockedBy,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Create a TrackerClient backed by the seeds (sd) CLI.
112
+ *
113
+ * @param cwd - Working directory for sd commands
114
+ */
115
+ export function createSeedsTracker(cwd: string): TrackerClient {
116
+ return {
117
+ async ready() {
118
+ const { stdout } = await runSd(["ready", "--json"], cwd, "ready");
119
+ const envelope = parseSdJson<SdListEnvelope>(stdout, "ready");
120
+ assertEnvelopeSuccess(envelope, "ready");
121
+ return envelope.issues.map(normalizeIssue);
122
+ },
123
+
124
+ async show(id) {
125
+ const { stdout } = await runSd(["show", id, "--json"], cwd, `show ${id}`);
126
+ const envelope = parseSdJson<SdShowEnvelope>(stdout, `show ${id}`);
127
+ assertEnvelopeSuccess(envelope, `show ${id}`);
128
+ return normalizeIssue(envelope.issue);
129
+ },
130
+
131
+ async create(title, options) {
132
+ const args = ["create", "--title", title, "--json"];
133
+ if (options?.type) {
134
+ args.push("--type", options.type);
135
+ }
136
+ if (options?.priority !== undefined) {
137
+ args.push("--priority", String(options.priority));
138
+ }
139
+ if (options?.description) {
140
+ args.push("--description", options.description);
141
+ }
142
+ const { stdout } = await runSd(args, cwd, "create");
143
+ const envelope = parseSdJson<SdCreateEnvelope>(stdout, "create");
144
+ assertEnvelopeSuccess(envelope, "create");
145
+ const id = envelope.id ?? envelope.issue?.id;
146
+ if (!id) {
147
+ throw new AgentError("sd create did not return an issue ID");
148
+ }
149
+ return id;
150
+ },
151
+
152
+ async claim(id) {
153
+ await runSd(["update", id, "--status", "in_progress"], cwd, `claim ${id}`);
154
+ },
155
+
156
+ async close(id, reason) {
157
+ const args = ["close", id];
158
+ if (reason) {
159
+ args.push("--reason", reason);
160
+ }
161
+ await runSd(args, cwd, `close ${id}`);
162
+ },
163
+
164
+ async list(options) {
165
+ const args = ["list", "--json"];
166
+ if (options?.status) {
167
+ args.push("--status", options.status);
168
+ }
169
+ if (options?.limit !== undefined) {
170
+ args.push("--limit", String(options.limit));
171
+ }
172
+ const { stdout } = await runSd(args, cwd, "list");
173
+ const envelope = parseSdJson<SdListEnvelope>(stdout, "list");
174
+ assertEnvelopeSuccess(envelope, "list");
175
+ return envelope.issues.map(normalizeIssue);
176
+ },
177
+
178
+ async sync() {
179
+ await runSd(["sync"], cwd, "sync");
180
+ },
181
+ };
182
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Unified tracker types — shared across beads and seeds backends.
3
+ * This module is self-contained and does NOT import from src/types.ts.
4
+ */
5
+
6
+ /**
7
+ * A tracker issue — unified across beads and seeds backends.
8
+ */
9
+ export interface TrackerIssue {
10
+ id: string;
11
+ title: string;
12
+ status: string;
13
+ priority: number;
14
+ type: string;
15
+ assignee?: string;
16
+ description?: string;
17
+ blocks?: string[];
18
+ blockedBy?: string[];
19
+ }
20
+
21
+ /**
22
+ * Unified tracker client interface.
23
+ * Both beads and seeds backends implement this.
24
+ */
25
+ export interface TrackerClient {
26
+ /** List issues that are ready for work (open, unblocked). */
27
+ ready(): Promise<TrackerIssue[]>;
28
+
29
+ /** Show details for a specific issue. */
30
+ show(id: string): Promise<TrackerIssue>;
31
+
32
+ /** Create a new issue. Returns the new issue ID. */
33
+ create(
34
+ title: string,
35
+ options?: { type?: string; priority?: number; description?: string },
36
+ ): Promise<string>;
37
+
38
+ /** Claim an issue (mark as in_progress). */
39
+ claim(id: string): Promise<void>;
40
+
41
+ /** Close an issue with an optional reason. */
42
+ close(id: string, reason?: string): Promise<void>;
43
+
44
+ /** List issues with optional filters. */
45
+ list(options?: { status?: string; limit?: number }): Promise<TrackerIssue[]>;
46
+
47
+ /** Sync tracker state with git (if supported). */
48
+ sync(): Promise<void>;
49
+ }
50
+
51
+ /** Which tracker backend to use. */
52
+ export type TrackerBackend = "beads" | "seeds";