@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.
- package/README.md +26 -8
- package/agents/coordinator.md +30 -6
- package/agents/lead.md +11 -1
- package/agents/ov-co-creation.md +90 -0
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +9 -1
- package/src/agents/hooks-deployer.ts +2 -1
- package/src/agents/overlay.test.ts +26 -0
- package/src/agents/overlay.ts +31 -4
- package/src/canopy/client.test.ts +107 -0
- package/src/canopy/client.ts +179 -0
- package/src/commands/agents.ts +1 -1
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/clean.ts +1 -58
- package/src/commands/completions.test.ts +18 -6
- package/src/commands/completions.ts +40 -1
- package/src/commands/coordinator.test.ts +77 -4
- package/src/commands/coordinator.ts +304 -146
- package/src/commands/dashboard.ts +47 -10
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.ts +3 -1
- package/src/commands/ecosystem.test.ts +126 -1
- package/src/commands/ecosystem.ts +7 -53
- package/src/commands/feed.test.ts +117 -2
- package/src/commands/feed.ts +46 -30
- package/src/commands/group.test.ts +274 -155
- package/src/commands/group.ts +11 -5
- package/src/commands/init.test.ts +2 -1
- package/src/commands/init.ts +8 -0
- package/src/commands/log.test.ts +35 -0
- package/src/commands/log.ts +10 -6
- package/src/commands/logs.test.ts +423 -1
- package/src/commands/logs.ts +99 -104
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +177 -2
- package/src/commands/prime.ts +4 -2
- package/src/commands/sling.ts +23 -3
- package/src/commands/update.test.ts +1 -0
- package/src/commands/upgrade.test.ts +2 -0
- package/src/commands/upgrade.ts +1 -17
- package/src/commands/watch.test.ts +67 -1
- package/src/commands/watch.ts +13 -88
- package/src/config.test.ts +250 -0
- package/src/config.ts +43 -0
- package/src/doctor/agents.test.ts +72 -5
- package/src/doctor/agents.ts +10 -10
- package/src/doctor/consistency.test.ts +35 -0
- package/src/doctor/consistency.ts +7 -3
- package/src/doctor/dependencies.test.ts +58 -1
- package/src/doctor/dependencies.ts +4 -2
- package/src/doctor/providers.test.ts +41 -5
- package/src/doctor/types.ts +2 -1
- package/src/doctor/version.test.ts +106 -2
- package/src/doctor/version.ts +4 -2
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +158 -0
- package/src/e2e/init-sling-lifecycle.test.ts +4 -2
- package/src/errors.test.ts +350 -0
- package/src/events/tailer.test.ts +25 -0
- package/src/events/tailer.ts +8 -1
- package/src/index.ts +9 -1
- package/src/mail/store.test.ts +110 -0
- package/src/mail/store.ts +2 -1
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +4 -2
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/pi-guards.ts +2 -1
- package/src/runtimes/pi.test.ts +9 -9
- package/src/runtimes/pi.ts +6 -7
- package/src/runtimes/registry.test.ts +1 -1
- package/src/runtimes/registry.ts +13 -4
- package/src/runtimes/sapling.ts +2 -1
- package/src/runtimes/types.ts +2 -2
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/store.ts +25 -4
- package/src/types.ts +65 -1
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +68 -0
- package/src/utils/pid.ts +45 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/watchdog/daemon.test.ts +255 -1
- package/src/watchdog/daemon.ts +87 -9
- package/src/watchdog/health.test.ts +15 -1
- package/src/watchdog/health.ts +1 -1
- package/src/watchdog/triage.test.ts +49 -9
- package/src/watchdog/triage.ts +21 -5
- 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
|
+
}
|
package/src/commands/agents.ts
CHANGED
|
@@ -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")
|
package/src/commands/clean.ts
CHANGED
|
@@ -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
|
|
16
|
-
expect(COMMANDS).toHaveLength(
|
|
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
|
|
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
|
|
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
|
|
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: [
|
|
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("
|
|
1259
|
-
expect(beacon).toContain("
|
|
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
|
|
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);
|