@os-eco/overstory-cli 0.8.7 → 0.9.2

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 (98) hide show
  1. package/README.md +26 -8
  2. package/agents/coordinator.md +30 -6
  3. package/agents/lead.md +11 -1
  4. package/agents/ov-co-creation.md +90 -0
  5. package/package.json +1 -1
  6. package/src/agents/hooks-deployer.test.ts +9 -1
  7. package/src/agents/hooks-deployer.ts +2 -1
  8. package/src/agents/overlay.test.ts +26 -0
  9. package/src/agents/overlay.ts +31 -4
  10. package/src/canopy/client.test.ts +107 -0
  11. package/src/canopy/client.ts +179 -0
  12. package/src/commands/agents.ts +1 -1
  13. package/src/commands/clean.test.ts +3 -0
  14. package/src/commands/clean.ts +1 -58
  15. package/src/commands/completions.test.ts +18 -6
  16. package/src/commands/completions.ts +40 -1
  17. package/src/commands/coordinator.test.ts +77 -4
  18. package/src/commands/coordinator.ts +304 -146
  19. package/src/commands/dashboard.ts +47 -10
  20. package/src/commands/discover.test.ts +288 -0
  21. package/src/commands/discover.ts +202 -0
  22. package/src/commands/doctor.ts +3 -1
  23. package/src/commands/ecosystem.test.ts +126 -1
  24. package/src/commands/ecosystem.ts +7 -53
  25. package/src/commands/feed.test.ts +117 -2
  26. package/src/commands/feed.ts +46 -30
  27. package/src/commands/group.test.ts +274 -155
  28. package/src/commands/group.ts +11 -5
  29. package/src/commands/init.test.ts +2 -1
  30. package/src/commands/init.ts +8 -0
  31. package/src/commands/log.test.ts +35 -0
  32. package/src/commands/log.ts +10 -6
  33. package/src/commands/logs.test.ts +423 -1
  34. package/src/commands/logs.ts +99 -104
  35. package/src/commands/orchestrator.ts +42 -0
  36. package/src/commands/prime.test.ts +177 -2
  37. package/src/commands/prime.ts +4 -2
  38. package/src/commands/sling.ts +23 -3
  39. package/src/commands/update.test.ts +1 -0
  40. package/src/commands/upgrade.test.ts +2 -0
  41. package/src/commands/upgrade.ts +1 -17
  42. package/src/commands/watch.test.ts +67 -1
  43. package/src/commands/watch.ts +13 -88
  44. package/src/config.test.ts +250 -0
  45. package/src/config.ts +43 -0
  46. package/src/doctor/agents.test.ts +72 -5
  47. package/src/doctor/agents.ts +10 -10
  48. package/src/doctor/consistency.test.ts +35 -0
  49. package/src/doctor/consistency.ts +7 -3
  50. package/src/doctor/dependencies.test.ts +58 -1
  51. package/src/doctor/dependencies.ts +4 -2
  52. package/src/doctor/providers.test.ts +41 -5
  53. package/src/doctor/types.ts +2 -1
  54. package/src/doctor/version.test.ts +106 -2
  55. package/src/doctor/version.ts +4 -2
  56. package/src/doctor/watchdog.test.ts +167 -0
  57. package/src/doctor/watchdog.ts +158 -0
  58. package/src/e2e/init-sling-lifecycle.test.ts +4 -2
  59. package/src/errors.test.ts +350 -0
  60. package/src/events/tailer.test.ts +25 -0
  61. package/src/events/tailer.ts +8 -1
  62. package/src/index.ts +9 -1
  63. package/src/mail/store.test.ts +110 -0
  64. package/src/mail/store.ts +2 -1
  65. package/src/runtimes/aider.test.ts +124 -0
  66. package/src/runtimes/aider.ts +147 -0
  67. package/src/runtimes/amp.test.ts +164 -0
  68. package/src/runtimes/amp.ts +154 -0
  69. package/src/runtimes/claude.test.ts +4 -2
  70. package/src/runtimes/goose.test.ts +133 -0
  71. package/src/runtimes/goose.ts +157 -0
  72. package/src/runtimes/pi-guards.ts +2 -1
  73. package/src/runtimes/pi.test.ts +9 -9
  74. package/src/runtimes/pi.ts +6 -7
  75. package/src/runtimes/registry.test.ts +1 -1
  76. package/src/runtimes/registry.ts +13 -4
  77. package/src/runtimes/sapling.ts +2 -1
  78. package/src/runtimes/types.ts +2 -2
  79. package/src/schema-consistency.test.ts +1 -0
  80. package/src/sessions/store.ts +25 -4
  81. package/src/types.ts +65 -1
  82. package/src/utils/bin.test.ts +10 -0
  83. package/src/utils/bin.ts +37 -0
  84. package/src/utils/fs.test.ts +119 -0
  85. package/src/utils/fs.ts +62 -0
  86. package/src/utils/pid.test.ts +68 -0
  87. package/src/utils/pid.ts +45 -0
  88. package/src/utils/time.test.ts +43 -0
  89. package/src/utils/time.ts +37 -0
  90. package/src/utils/version.test.ts +33 -0
  91. package/src/utils/version.ts +70 -0
  92. package/src/watchdog/daemon.test.ts +255 -1
  93. package/src/watchdog/daemon.ts +87 -9
  94. package/src/watchdog/health.test.ts +15 -1
  95. package/src/watchdog/health.ts +1 -1
  96. package/src/watchdog/triage.test.ts +49 -9
  97. package/src/watchdog/triage.ts +21 -5
  98. package/templates/overlay.md.tmpl +2 -0
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Canopy CLI client.
3
+ *
4
+ * Wraps the `cn` command-line tool for prompt management operations.
5
+ * All methods use Bun.spawn to invoke the CLI directly.
6
+ */
7
+
8
+ import { AgentError } from "../errors.ts";
9
+ import type {
10
+ CanopyListResult,
11
+ CanopyRenderResult,
12
+ CanopyShowResult,
13
+ CanopyValidateResult,
14
+ } from "../types.ts";
15
+
16
+ export interface CanopyClient {
17
+ /** Render a prompt, resolving inheritance. */
18
+ render(name: string, options?: { format?: "md" | "json" }): Promise<CanopyRenderResult>;
19
+
20
+ /** Validate a prompt (or all prompts) against its schema. */
21
+ validate(name?: string, options?: { all?: boolean }): Promise<CanopyValidateResult>;
22
+
23
+ /** List all prompts. */
24
+ list(options?: {
25
+ tag?: string;
26
+ status?: string;
27
+ extends?: string;
28
+ mixin?: string;
29
+ }): Promise<CanopyListResult>;
30
+
31
+ /** Show a prompt record. */
32
+ show(name: string): Promise<CanopyShowResult>;
33
+ }
34
+
35
+ /**
36
+ * Run a shell command and capture its output.
37
+ */
38
+ async function runCommand(
39
+ cmd: string[],
40
+ cwd: string,
41
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
42
+ const proc = Bun.spawn(cmd, {
43
+ cwd,
44
+ stdout: "pipe",
45
+ stderr: "pipe",
46
+ });
47
+ const stdout = await new Response(proc.stdout).text();
48
+ const stderr = await new Response(proc.stderr).text();
49
+ const exitCode = await proc.exited;
50
+ return { stdout, stderr, exitCode };
51
+ }
52
+
53
+ /**
54
+ * Create a CanopyClient bound to the given working directory.
55
+ *
56
+ * @param cwd - Working directory where cn commands should run
57
+ * @returns A CanopyClient instance wrapping the cn CLI
58
+ */
59
+ export function createCanopyClient(cwd: string): CanopyClient {
60
+ async function runCanopy(
61
+ args: string[],
62
+ context: string,
63
+ ): Promise<{ stdout: string; stderr: string }> {
64
+ const { stdout, stderr, exitCode } = await runCommand(["cn", ...args], cwd);
65
+ if (exitCode !== 0) {
66
+ throw new AgentError(`canopy ${context} failed (exit ${exitCode}): ${stderr.trim()}`);
67
+ }
68
+ return { stdout, stderr };
69
+ }
70
+
71
+ return {
72
+ async render(name, _options) {
73
+ // Always use --json for structured output; format param reserved for future use
74
+ const { stdout } = await runCanopy(["render", name, "--json"], `render ${name}`);
75
+ const trimmed = stdout.trim();
76
+ try {
77
+ const raw = JSON.parse(trimmed) as {
78
+ success: boolean;
79
+ name: string;
80
+ version: number;
81
+ sections: Array<{ name: string; body: string }>;
82
+ };
83
+ return {
84
+ success: raw.success,
85
+ name: raw.name,
86
+ version: raw.version,
87
+ sections: raw.sections,
88
+ };
89
+ } catch {
90
+ throw new AgentError(
91
+ `Failed to parse JSON from cn render ${name}: ${trimmed.slice(0, 200)}`,
92
+ );
93
+ }
94
+ },
95
+
96
+ async validate(name, options) {
97
+ const args = ["validate"];
98
+ if (options?.all) {
99
+ args.push("--all");
100
+ } else if (name) {
101
+ args.push(name);
102
+ }
103
+ // cn validate does not support --json; parse exit code and stdout/stderr
104
+ const { stdout, stderr, exitCode } = await runCommand(["cn", ...args], cwd);
105
+ const output = (stdout + stderr).trim();
106
+ const errors: string[] = [];
107
+ if (exitCode !== 0) {
108
+ // Extract error lines from output (lines containing "error:")
109
+ for (const line of output.split("\n")) {
110
+ const trimmedLine = line.trim();
111
+ if (trimmedLine.includes("error:")) {
112
+ errors.push(trimmedLine);
113
+ }
114
+ }
115
+ if (errors.length === 0 && output) {
116
+ errors.push(output);
117
+ }
118
+ }
119
+ return { success: exitCode === 0, errors };
120
+ },
121
+
122
+ async list(options) {
123
+ const args = ["list", "--json"];
124
+ if (options?.tag) {
125
+ args.push("--tag", options.tag);
126
+ }
127
+ if (options?.status) {
128
+ args.push("--status", options.status);
129
+ }
130
+ if (options?.extends) {
131
+ args.push("--extends", options.extends);
132
+ }
133
+ if (options?.mixin) {
134
+ args.push("--mixin", options.mixin);
135
+ }
136
+ const { stdout } = await runCanopy(args, "list");
137
+ const trimmed = stdout.trim();
138
+ try {
139
+ const raw = JSON.parse(trimmed) as {
140
+ success: boolean;
141
+ prompts: Array<{
142
+ id: string;
143
+ name: string;
144
+ version: number;
145
+ sections: Array<{ name: string; body: string }>;
146
+ }>;
147
+ };
148
+ return {
149
+ success: raw.success,
150
+ prompts: raw.prompts,
151
+ };
152
+ } catch {
153
+ throw new AgentError(`Failed to parse JSON from cn list: ${trimmed.slice(0, 200)}`);
154
+ }
155
+ },
156
+
157
+ async show(name) {
158
+ const { stdout } = await runCanopy(["show", name, "--json"], `show ${name}`);
159
+ const trimmed = stdout.trim();
160
+ try {
161
+ const raw = JSON.parse(trimmed) as {
162
+ success: boolean;
163
+ prompt: {
164
+ id: string;
165
+ name: string;
166
+ version: number;
167
+ sections: Array<{ name: string; body: string }>;
168
+ };
169
+ };
170
+ return {
171
+ success: raw.success,
172
+ prompt: raw.prompt,
173
+ };
174
+ } catch {
175
+ throw new AgentError(`Failed to parse JSON from cn show ${name}: ${trimmed.slice(0, 200)}`);
176
+ }
177
+ },
178
+ };
179
+ }
@@ -223,7 +223,7 @@ export function createAgentsCommand(): Command {
223
223
  .description("Find active agents by capability")
224
224
  .option(
225
225
  "--capability <type>",
226
- "Filter by capability (builder, scout, reviewer, lead, merger, coordinator, supervisor)",
226
+ "Filter by capability (builder, scout, reviewer, lead, merger, orchestrator, coordinator, supervisor)",
227
227
  )
228
228
  .option("--all", "Include completed and zombie agents (default: active only)")
229
229
  .option("--json", "Output as JSON")
@@ -792,3 +792,6 @@ describe("--agent", () => {
792
792
  expect(stdoutOutput).toContain("Agent cleaned");
793
793
  });
794
794
  });
795
+
796
+ // fs utility tests (wipeSqliteDb, resetJsonFile, clearDirectory, deleteFile)
797
+ // moved to src/utils/fs.test.ts
@@ -20,7 +20,6 @@
20
20
  */
21
21
 
22
22
  import { existsSync } from "node:fs";
23
- import { readdir, rm, unlink } from "node:fs/promises";
24
23
  import { join } from "node:path";
25
24
  import { loadConfig } from "../config.ts";
26
25
  import { AgentError, ValidationError } from "../errors.ts";
@@ -30,6 +29,7 @@ import { printHint, printSuccess } from "../logging/color.ts";
30
29
  import { createMulchClient } from "../mulch/client.ts";
31
30
  import { openSessionStore } from "../sessions/compat.ts";
32
31
  import type { AgentSession, MulchDoctorResult, MulchPruneResult, MulchStatus } from "../types.ts";
32
+ import { clearDirectory, deleteFile, resetJsonFile, wipeSqliteDb } from "../utils/fs.ts";
33
33
  import { listWorktrees, removeWorktree } from "../worktree/manager.ts";
34
34
  import {
35
35
  isProcessAlive,
@@ -274,63 +274,6 @@ async function deleteOrphanedBranches(root: string): Promise<number> {
274
274
  return deleted;
275
275
  }
276
276
 
277
- /**
278
- * Delete a SQLite database file and its WAL/SHM companions.
279
- */
280
- async function wipeSqliteDb(dbPath: string): Promise<boolean> {
281
- const extensions = ["", "-wal", "-shm"];
282
- let wiped = false;
283
- for (const ext of extensions) {
284
- try {
285
- await unlink(`${dbPath}${ext}`);
286
- if (ext === "") wiped = true;
287
- } catch {
288
- // File may not exist
289
- }
290
- }
291
- return wiped;
292
- }
293
-
294
- /**
295
- * Reset a JSON file to an empty array.
296
- */
297
- async function resetJsonFile(path: string): Promise<boolean> {
298
- const file = Bun.file(path);
299
- if (await file.exists()) {
300
- await Bun.write(path, "[]\n");
301
- return true;
302
- }
303
- return false;
304
- }
305
-
306
- /**
307
- * Clear all entries inside a directory but keep the directory itself.
308
- */
309
- async function clearDirectory(dirPath: string): Promise<boolean> {
310
- try {
311
- const entries = await readdir(dirPath);
312
- for (const entry of entries) {
313
- await rm(join(dirPath, entry), { recursive: true, force: true });
314
- }
315
- return entries.length > 0;
316
- } catch {
317
- // Directory may not exist
318
- return false;
319
- }
320
- }
321
-
322
- /**
323
- * Delete a single file if it exists.
324
- */
325
- async function deleteFile(path: string): Promise<boolean> {
326
- try {
327
- await unlink(path);
328
- return true;
329
- } catch {
330
- return false;
331
- }
332
- }
333
-
334
277
  /**
335
278
  * Check mulch repository health and return diagnostic information.
336
279
  *
@@ -2,7 +2,7 @@
2
2
  * Tests for shell completion generation.
3
3
  */
4
4
 
5
- import { describe, expect, it, mock } from "bun:test";
5
+ import { afterEach, describe, expect, it, mock } from "bun:test";
6
6
  import {
7
7
  COMMANDS,
8
8
  completionsCommand,
@@ -11,9 +11,13 @@ import {
11
11
  generateZsh,
12
12
  } from "./completions.ts";
13
13
 
14
+ afterEach(() => {
15
+ process.exitCode = undefined;
16
+ });
17
+
14
18
  describe("COMMANDS array", () => {
15
- it("should have exactly 33 commands", () => {
16
- expect(COMMANDS).toHaveLength(33);
19
+ it("should have exactly 34 commands", () => {
20
+ expect(COMMANDS).toHaveLength(34);
17
21
  });
18
22
 
19
23
  it("should include all expected command names", () => {
@@ -37,6 +41,7 @@ describe("COMMANDS array", () => {
37
41
  expect(names).toContain("costs");
38
42
  expect(names).toContain("metrics");
39
43
  expect(names).toContain("spec");
44
+ expect(names).toContain("orchestrator");
40
45
  expect(names).toContain("coordinator");
41
46
  expect(names).toContain("supervisor");
42
47
  expect(names).toContain("hooks");
@@ -62,7 +67,7 @@ describe("generateBash", () => {
62
67
  expect(script).toContain("_init_completion");
63
68
  });
64
69
 
65
- it("should include all 33 command names", () => {
70
+ it("should include all 34 command names", () => {
66
71
  const script = generateBash();
67
72
  for (const cmd of COMMANDS) {
68
73
  expect(script).toContain(cmd.name);
@@ -96,7 +101,7 @@ describe("generateZsh", () => {
96
101
  expect(script).toContain("_arguments");
97
102
  });
98
103
 
99
- it("should include all 33 command names", () => {
104
+ it("should include all 34 command names", () => {
100
105
  const script = generateZsh();
101
106
  for (const cmd of COMMANDS) {
102
107
  expect(script).toContain(cmd.name);
@@ -126,7 +131,7 @@ describe("generateFish", () => {
126
131
  expect(script).toContain("__fish_use_subcommand");
127
132
  });
128
133
 
129
- it("should include all 33 command names", () => {
134
+ it("should include all 34 command names", () => {
130
135
  const script = generateFish();
131
136
  for (const cmd of COMMANDS) {
132
137
  expect(script).toContain(cmd.name);
@@ -148,6 +153,13 @@ describe("generateFish", () => {
148
153
  expect(script).toContain("error");
149
154
  expect(script).toContain("worker_done");
150
155
  });
156
+
157
+ it("should include orchestrator command with start/stop/status subcommands", () => {
158
+ const orchestrator = COMMANDS.find((c) => c.name === "orchestrator");
159
+ expect(orchestrator).toBeDefined();
160
+ const subcommands = orchestrator?.subcommands?.map((s) => s.name);
161
+ expect(subcommands).toEqual(["start", "stop", "status"]);
162
+ });
151
163
  });
152
164
 
153
165
  describe("completionsCommand", () => {
@@ -44,7 +44,16 @@ export const COMMANDS: readonly CommandDef[] = [
44
44
  name: "--capability",
45
45
  desc: "Filter by capability",
46
46
  takesValue: true,
47
- values: ["builder", "scout", "reviewer", "lead", "merger", "coordinator", "supervisor"],
47
+ values: [
48
+ "builder",
49
+ "scout",
50
+ "reviewer",
51
+ "lead",
52
+ "merger",
53
+ "orchestrator",
54
+ "coordinator",
55
+ "supervisor",
56
+ ],
48
57
  },
49
58
  { name: "--all", desc: "Include completed and zombie agents" },
50
59
  { name: "--json", desc: "JSON output" },
@@ -343,6 +352,36 @@ export const COMMANDS: readonly CommandDef[] = [
343
352
  },
344
353
  ],
345
354
  },
355
+ {
356
+ name: "orchestrator",
357
+ desc: "Persistent ecosystem orchestrator agent",
358
+ flags: [
359
+ { name: "--json", desc: "JSON output" },
360
+ { name: "--help", desc: "Show help" },
361
+ ],
362
+ subcommands: [
363
+ {
364
+ name: "start",
365
+ desc: "Start orchestrator",
366
+ flags: [
367
+ { name: "--attach", desc: "Attach to tmux session" },
368
+ { name: "--no-attach", desc: "Do not attach to tmux session" },
369
+ { name: "--watchdog", desc: "Auto-start watchdog daemon" },
370
+ { name: "--json", desc: "JSON output" },
371
+ ],
372
+ },
373
+ {
374
+ name: "stop",
375
+ desc: "Stop orchestrator",
376
+ flags: [{ name: "--json", desc: "JSON output" }],
377
+ },
378
+ {
379
+ name: "status",
380
+ desc: "Show orchestrator state",
381
+ flags: [{ name: "--json", desc: "JSON output" }],
382
+ },
383
+ ],
384
+ },
346
385
  {
347
386
  name: "coordinator",
348
387
  desc: "Persistent coordinator agent",
@@ -29,6 +29,11 @@ import {
29
29
  createCoordinatorCommand,
30
30
  resolveAttach,
31
31
  } from "./coordinator.ts";
32
+ import {
33
+ buildOrchestratorBeacon,
34
+ createOrchestratorCommand,
35
+ orchestratorCommand,
36
+ } from "./orchestrator.ts";
32
37
 
33
38
  // --- Fake Tmux ---
34
39
 
@@ -272,14 +277,26 @@ beforeEach(async () => {
272
277
  canSpawn: true,
273
278
  constraints: [],
274
279
  },
280
+ orchestrator: {
281
+ file: "orchestrator.md",
282
+ model: "opus",
283
+ tools: ["Read", "Bash"],
284
+ capabilities: ["orchestrate", "coordinate"],
285
+ canSpawn: true,
286
+ constraints: [],
287
+ },
288
+ },
289
+ capabilityIndex: {
290
+ coordinate: ["coordinator", "orchestrator"],
291
+ orchestrate: ["orchestrator"],
275
292
  },
276
- capabilityIndex: { coordinate: ["coordinator"] },
277
293
  };
278
294
  await Bun.write(
279
295
  join(overstoryDir, "agent-manifest.json"),
280
296
  `${JSON.stringify(manifest, null, "\t")}\n`,
281
297
  );
282
298
  await Bun.write(join(agentDefsDir, "coordinator.md"), "# Coordinator\n");
299
+ await Bun.write(join(agentDefsDir, "orchestrator.md"), "# Orchestrator\n");
283
300
 
284
301
  // Override cwd so coordinator commands find our temp project
285
302
  process.chdir(tempDir);
@@ -1255,14 +1272,16 @@ describe("buildCoordinatorBeacon", () => {
1255
1272
 
1256
1273
  test("includes hierarchy enforcement instruction", () => {
1257
1274
  const beacon = buildCoordinatorBeacon();
1258
- expect(beacon).toContain("ONLY spawn leads");
1259
- expect(beacon).toContain("NEVER spawn non-lead agents directly");
1275
+ expect(beacon).toContain("Default to leads");
1276
+ expect(beacon).toContain("spawn scout/builder directly");
1277
+ expect(beacon).toContain("NEVER spawn reviewer or merger directly");
1260
1278
  });
1261
1279
 
1262
1280
  test("includes delegation instruction", () => {
1263
1281
  const beacon = buildCoordinatorBeacon();
1264
1282
  expect(beacon).toContain("DELEGATION");
1265
- expect(beacon).toContain("spawn a lead who will spawn scouts");
1283
+ expect(beacon).toContain("spawn a lead who will handle scouts/builders/reviewers");
1284
+ expect(beacon).toContain("--dispatch-max-agents 1/2");
1266
1285
  });
1267
1286
 
1268
1287
  test("parts are joined with em-dash separator", () => {
@@ -1273,6 +1292,60 @@ describe("buildCoordinatorBeacon", () => {
1273
1292
  });
1274
1293
  });
1275
1294
 
1295
+ describe("orchestratorCommand", () => {
1296
+ test("help shows orchestrator command name", async () => {
1297
+ const output = await captureStdout(() => orchestratorCommand(["--help"]));
1298
+ expect(output).toContain("orchestrator");
1299
+ });
1300
+
1301
+ test("start creates orchestrator session with orchestrator capability", async () => {
1302
+ const { deps, calls } = makeDeps({ "overstory-test-project-orchestrator": true });
1303
+ const originalSleep = Bun.sleep;
1304
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1305
+
1306
+ try {
1307
+ const output = await captureStdout(() =>
1308
+ orchestratorCommand(["start", "--no-attach", "--json"], deps),
1309
+ );
1310
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1311
+
1312
+ expect(parsed.agentName).toBe("orchestrator");
1313
+ expect(parsed.capability).toBe("orchestrator");
1314
+ expect(parsed.tmuxSession).toBe("overstory-test-project-orchestrator");
1315
+ expect(calls.createSession[0]?.name).toBe("overstory-test-project-orchestrator");
1316
+ expect(calls.createSession[0]?.command).toContain("orchestrator.md");
1317
+
1318
+ const session = loadSessionsFromDb().find((entry) => entry.agentName === "orchestrator");
1319
+ expect(session?.capability).toBe("orchestrator");
1320
+ } finally {
1321
+ Bun.sleep = originalSleep;
1322
+ }
1323
+ });
1324
+
1325
+ test("command registration includes orchestrator start/stop/status", () => {
1326
+ const cmd = createOrchestratorCommand({});
1327
+ const subcommandNames = cmd.commands.map((c) => c.name());
1328
+ expect(subcommandNames).toContain("start");
1329
+ expect(subcommandNames).toContain("stop");
1330
+ expect(subcommandNames).toContain("status");
1331
+ expect(subcommandNames).not.toContain("check-complete");
1332
+ });
1333
+ });
1334
+
1335
+ describe("buildOrchestratorBeacon", () => {
1336
+ test("includes orchestrator identity in header", () => {
1337
+ const beacon = buildOrchestratorBeacon();
1338
+ expect(beacon).toContain("[OVERSTORY] orchestrator (orchestrator)");
1339
+ });
1340
+
1341
+ test("includes ecosystem startup instructions", () => {
1342
+ const beacon = buildOrchestratorBeacon("sd");
1343
+ expect(beacon).toContain("ov mail check --agent orchestrator");
1344
+ expect(beacon).toContain("sd ready");
1345
+ expect(beacon).toContain("inspect ecosystem status");
1346
+ });
1347
+ });
1348
+
1276
1349
  describe("resolveAttach", () => {
1277
1350
  test("--attach flag forces attach regardless of TTY", () => {
1278
1351
  expect(resolveAttach(["--attach"], false)).toBe(true);