@katyella/legio 0.1.0
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/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
5
|
+
import { AgentError } from "../errors.ts";
|
|
6
|
+
import type { AgentManifest, LegioConfig } from "../types.ts";
|
|
7
|
+
import { createManifestLoader, resolveModel } from "./manifest.ts";
|
|
8
|
+
|
|
9
|
+
const VALID_MANIFEST = {
|
|
10
|
+
version: "1.0",
|
|
11
|
+
agents: {
|
|
12
|
+
scout: {
|
|
13
|
+
file: "scout.md",
|
|
14
|
+
model: "sonnet",
|
|
15
|
+
tools: ["Read", "Grep", "Glob"],
|
|
16
|
+
capabilities: ["explore", "review"],
|
|
17
|
+
canSpawn: false,
|
|
18
|
+
constraints: ["read-only"],
|
|
19
|
+
},
|
|
20
|
+
builder: {
|
|
21
|
+
file: "builder.md",
|
|
22
|
+
model: "opus",
|
|
23
|
+
tools: ["Read", "Write", "Edit", "Bash"],
|
|
24
|
+
capabilities: ["implement", "refactor"],
|
|
25
|
+
canSpawn: false,
|
|
26
|
+
constraints: [],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("createManifestLoader", () => {
|
|
32
|
+
let tempDir: string;
|
|
33
|
+
let manifestPath: string;
|
|
34
|
+
let agentBaseDir: string;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
tempDir = await mkdtemp(join(tmpdir(), "legio-manifest-test-"));
|
|
38
|
+
manifestPath = join(tempDir, "agent-manifest.json");
|
|
39
|
+
agentBaseDir = join(tempDir, "agents");
|
|
40
|
+
await mkdir(agentBaseDir, { recursive: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Write the manifest JSON and create matching .md files. */
|
|
48
|
+
async function writeManifest(
|
|
49
|
+
data: Record<string, unknown>,
|
|
50
|
+
options?: { skipMdFiles?: boolean; mdFilesToSkip?: string[] },
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
await writeFile(manifestPath, JSON.stringify(data));
|
|
53
|
+
if (options?.skipMdFiles) return;
|
|
54
|
+
|
|
55
|
+
const agents = data.agents;
|
|
56
|
+
if (agents && typeof agents === "object" && !Array.isArray(agents)) {
|
|
57
|
+
for (const [, def] of Object.entries(agents as Record<string, Record<string, unknown>>)) {
|
|
58
|
+
const file = def.file;
|
|
59
|
+
if (
|
|
60
|
+
typeof file === "string" &&
|
|
61
|
+
file.length > 0 &&
|
|
62
|
+
!options?.mdFilesToSkip?.includes(file)
|
|
63
|
+
) {
|
|
64
|
+
await writeFile(join(agentBaseDir, file), `# ${file}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("load", () => {
|
|
71
|
+
test("reads manifest, validates structure, verifies .md files, builds capability index", async () => {
|
|
72
|
+
await writeManifest(VALID_MANIFEST);
|
|
73
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
74
|
+
|
|
75
|
+
const manifest = await loader.load();
|
|
76
|
+
|
|
77
|
+
expect(manifest.version).toBe("1.0");
|
|
78
|
+
expect(Object.keys(manifest.agents)).toHaveLength(2);
|
|
79
|
+
expect(manifest.agents.scout).toBeDefined();
|
|
80
|
+
expect(manifest.agents.builder).toBeDefined();
|
|
81
|
+
expect(manifest.capabilityIndex).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("builds capability index mapping capabilities to agent names", async () => {
|
|
85
|
+
await writeManifest(VALID_MANIFEST);
|
|
86
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
87
|
+
|
|
88
|
+
const manifest = await loader.load();
|
|
89
|
+
|
|
90
|
+
expect(manifest.capabilityIndex.explore).toEqual(["scout"]);
|
|
91
|
+
expect(manifest.capabilityIndex.review).toEqual(["scout"]);
|
|
92
|
+
expect(manifest.capabilityIndex.implement).toEqual(["builder"]);
|
|
93
|
+
expect(manifest.capabilityIndex.refactor).toEqual(["builder"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("capability index includes multiple agents for shared capabilities", async () => {
|
|
97
|
+
const data = {
|
|
98
|
+
version: "1.0",
|
|
99
|
+
agents: {
|
|
100
|
+
scout: {
|
|
101
|
+
file: "scout.md",
|
|
102
|
+
model: "sonnet",
|
|
103
|
+
tools: ["Read"],
|
|
104
|
+
capabilities: ["review"],
|
|
105
|
+
canSpawn: false,
|
|
106
|
+
constraints: [],
|
|
107
|
+
},
|
|
108
|
+
reviewer: {
|
|
109
|
+
file: "reviewer.md",
|
|
110
|
+
model: "sonnet",
|
|
111
|
+
tools: ["Read"],
|
|
112
|
+
capabilities: ["review"],
|
|
113
|
+
canSpawn: false,
|
|
114
|
+
constraints: [],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
await writeManifest(data);
|
|
119
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
120
|
+
|
|
121
|
+
const manifest = await loader.load();
|
|
122
|
+
|
|
123
|
+
expect(manifest.capabilityIndex.review).toEqual(["scout", "reviewer"]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("getAgent", () => {
|
|
128
|
+
test("returns undefined before load is called", async () => {
|
|
129
|
+
await writeManifest(VALID_MANIFEST);
|
|
130
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
131
|
+
|
|
132
|
+
expect(loader.getAgent("scout")).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("returns agent definition after load", async () => {
|
|
136
|
+
await writeManifest(VALID_MANIFEST);
|
|
137
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
138
|
+
await loader.load();
|
|
139
|
+
|
|
140
|
+
const scout = loader.getAgent("scout");
|
|
141
|
+
expect(scout).toBeDefined();
|
|
142
|
+
expect(scout?.model).toBe("sonnet");
|
|
143
|
+
expect(scout?.tools).toEqual(["Read", "Grep", "Glob"]);
|
|
144
|
+
expect(scout?.capabilities).toEqual(["explore", "review"]);
|
|
145
|
+
expect(scout?.canSpawn).toBe(false);
|
|
146
|
+
expect(scout?.constraints).toEqual(["read-only"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns undefined for non-existent agent after load", async () => {
|
|
150
|
+
await writeManifest(VALID_MANIFEST);
|
|
151
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
152
|
+
await loader.load();
|
|
153
|
+
|
|
154
|
+
expect(loader.getAgent("nonexistent")).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("findByCapability", () => {
|
|
159
|
+
test("returns empty array before load is called", async () => {
|
|
160
|
+
await writeManifest(VALID_MANIFEST);
|
|
161
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
162
|
+
|
|
163
|
+
expect(loader.findByCapability("explore")).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("returns matching agents after load", async () => {
|
|
167
|
+
await writeManifest(VALID_MANIFEST);
|
|
168
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
169
|
+
await loader.load();
|
|
170
|
+
|
|
171
|
+
const explorers = loader.findByCapability("explore");
|
|
172
|
+
expect(explorers).toHaveLength(1);
|
|
173
|
+
expect(explorers[0]?.file).toBe("scout.md");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("returns empty array for non-existent capability", async () => {
|
|
177
|
+
await writeManifest(VALID_MANIFEST);
|
|
178
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
179
|
+
await loader.load();
|
|
180
|
+
|
|
181
|
+
expect(loader.findByCapability("nonexistent")).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("returns multiple agents sharing a capability", async () => {
|
|
185
|
+
const data = {
|
|
186
|
+
version: "1.0",
|
|
187
|
+
agents: {
|
|
188
|
+
scout: {
|
|
189
|
+
file: "scout.md",
|
|
190
|
+
model: "sonnet",
|
|
191
|
+
tools: ["Read"],
|
|
192
|
+
capabilities: ["review"],
|
|
193
|
+
canSpawn: false,
|
|
194
|
+
constraints: [],
|
|
195
|
+
},
|
|
196
|
+
reviewer: {
|
|
197
|
+
file: "reviewer.md",
|
|
198
|
+
model: "sonnet",
|
|
199
|
+
tools: ["Read"],
|
|
200
|
+
capabilities: ["review"],
|
|
201
|
+
canSpawn: false,
|
|
202
|
+
constraints: [],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
await writeManifest(data);
|
|
207
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
208
|
+
await loader.load();
|
|
209
|
+
|
|
210
|
+
const reviewers = loader.findByCapability("review");
|
|
211
|
+
expect(reviewers).toHaveLength(2);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("validate", () => {
|
|
216
|
+
test("returns error message if not loaded", () => {
|
|
217
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
218
|
+
|
|
219
|
+
const errors = loader.validate();
|
|
220
|
+
expect(errors).toHaveLength(1);
|
|
221
|
+
expect(errors[0]).toContain("not loaded");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("returns empty array for valid manifest", async () => {
|
|
225
|
+
await writeManifest(VALID_MANIFEST);
|
|
226
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
227
|
+
await loader.load();
|
|
228
|
+
|
|
229
|
+
const errors = loader.validate();
|
|
230
|
+
expect(errors).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("error handling", () => {
|
|
235
|
+
test("throws AgentError for missing manifest file", async () => {
|
|
236
|
+
const loader = createManifestLoader(join(tempDir, "nonexistent.json"), agentBaseDir);
|
|
237
|
+
|
|
238
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
239
|
+
await expect(loader.load()).rejects.toThrow("not found");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("throws AgentError for invalid JSON", async () => {
|
|
243
|
+
await writeFile(manifestPath, "not valid json {{{");
|
|
244
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
245
|
+
|
|
246
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
247
|
+
await expect(loader.load()).rejects.toThrow("Failed to parse");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("throws AgentError for missing version field", async () => {
|
|
251
|
+
await writeFile(manifestPath, JSON.stringify({ agents: {} }));
|
|
252
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
253
|
+
|
|
254
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
255
|
+
await expect(loader.load()).rejects.toThrow("version");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("throws AgentError for empty version string", async () => {
|
|
259
|
+
await writeFile(manifestPath, JSON.stringify({ version: "", agents: {} }));
|
|
260
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
261
|
+
|
|
262
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
263
|
+
await expect(loader.load()).rejects.toThrow("version");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("throws AgentError when agents field is missing", async () => {
|
|
267
|
+
await writeFile(manifestPath, JSON.stringify({ version: "1.0" }));
|
|
268
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
269
|
+
|
|
270
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
271
|
+
await expect(loader.load()).rejects.toThrow("agents");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("throws AgentError when agents field is an array", async () => {
|
|
275
|
+
await writeFile(manifestPath, JSON.stringify({ version: "1.0", agents: [] }));
|
|
276
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
277
|
+
|
|
278
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
279
|
+
await expect(loader.load()).rejects.toThrow("agents");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("throws AgentError for invalid model value", async () => {
|
|
283
|
+
const data = {
|
|
284
|
+
version: "1.0",
|
|
285
|
+
agents: {
|
|
286
|
+
bad: {
|
|
287
|
+
file: "bad.md",
|
|
288
|
+
model: "gpt-4",
|
|
289
|
+
tools: ["Read"],
|
|
290
|
+
capabilities: ["test"],
|
|
291
|
+
canSpawn: false,
|
|
292
|
+
constraints: [],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
await writeManifest(data);
|
|
297
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
298
|
+
|
|
299
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
300
|
+
await expect(loader.load()).rejects.toThrow("model");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("throws AgentError for missing tools array", async () => {
|
|
304
|
+
const data = {
|
|
305
|
+
version: "1.0",
|
|
306
|
+
agents: {
|
|
307
|
+
bad: {
|
|
308
|
+
file: "bad.md",
|
|
309
|
+
model: "sonnet",
|
|
310
|
+
capabilities: ["test"],
|
|
311
|
+
canSpawn: false,
|
|
312
|
+
constraints: [],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
await writeManifest(data);
|
|
317
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
318
|
+
|
|
319
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
320
|
+
await expect(loader.load()).rejects.toThrow("tools");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("throws AgentError for missing capabilities array", async () => {
|
|
324
|
+
const data = {
|
|
325
|
+
version: "1.0",
|
|
326
|
+
agents: {
|
|
327
|
+
bad: {
|
|
328
|
+
file: "bad.md",
|
|
329
|
+
model: "sonnet",
|
|
330
|
+
tools: ["Read"],
|
|
331
|
+
canSpawn: false,
|
|
332
|
+
constraints: [],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
await writeManifest(data);
|
|
337
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
338
|
+
|
|
339
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
340
|
+
await expect(loader.load()).rejects.toThrow("capabilities");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("throws AgentError when canSpawn is not boolean", async () => {
|
|
344
|
+
const data = {
|
|
345
|
+
version: "1.0",
|
|
346
|
+
agents: {
|
|
347
|
+
bad: {
|
|
348
|
+
file: "bad.md",
|
|
349
|
+
model: "sonnet",
|
|
350
|
+
tools: ["Read"],
|
|
351
|
+
capabilities: ["test"],
|
|
352
|
+
canSpawn: "yes",
|
|
353
|
+
constraints: [],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
await writeManifest(data);
|
|
358
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
359
|
+
|
|
360
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
361
|
+
await expect(loader.load()).rejects.toThrow("canSpawn");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("throws AgentError when constraints is not an array", async () => {
|
|
365
|
+
const data = {
|
|
366
|
+
version: "1.0",
|
|
367
|
+
agents: {
|
|
368
|
+
bad: {
|
|
369
|
+
file: "bad.md",
|
|
370
|
+
model: "sonnet",
|
|
371
|
+
tools: ["Read"],
|
|
372
|
+
capabilities: ["test"],
|
|
373
|
+
canSpawn: false,
|
|
374
|
+
constraints: "read-only",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
await writeManifest(data);
|
|
379
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
380
|
+
|
|
381
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
382
|
+
await expect(loader.load()).rejects.toThrow("constraints");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("throws AgentError when agent definition is not an object", async () => {
|
|
386
|
+
const data = {
|
|
387
|
+
version: "1.0",
|
|
388
|
+
agents: {
|
|
389
|
+
bad: "not an object",
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
await writeFile(manifestPath, JSON.stringify(data));
|
|
393
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
394
|
+
|
|
395
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
396
|
+
await expect(loader.load()).rejects.toThrow("definition must be an object");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("throws AgentError when referenced .md file does not exist", async () => {
|
|
400
|
+
await writeManifest(VALID_MANIFEST, { mdFilesToSkip: ["scout.md"] });
|
|
401
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
402
|
+
|
|
403
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
404
|
+
await expect(loader.load()).rejects.toThrow("does not exist");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("throws AgentError when file field is empty string", async () => {
|
|
408
|
+
const data = {
|
|
409
|
+
version: "1.0",
|
|
410
|
+
agents: {
|
|
411
|
+
bad: {
|
|
412
|
+
file: "",
|
|
413
|
+
model: "sonnet",
|
|
414
|
+
tools: ["Read"],
|
|
415
|
+
capabilities: ["test"],
|
|
416
|
+
canSpawn: false,
|
|
417
|
+
constraints: [],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
await writeManifest(data);
|
|
422
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
423
|
+
|
|
424
|
+
await expect(loader.load()).rejects.toThrow(AgentError);
|
|
425
|
+
await expect(loader.load()).rejects.toThrow("file");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("aggregates multiple validation errors", async () => {
|
|
429
|
+
const data = {
|
|
430
|
+
version: "1.0",
|
|
431
|
+
agents: {
|
|
432
|
+
bad: {
|
|
433
|
+
file: "",
|
|
434
|
+
model: "invalid",
|
|
435
|
+
tools: "not-array",
|
|
436
|
+
capabilities: "not-array",
|
|
437
|
+
canSpawn: "not-bool",
|
|
438
|
+
constraints: "not-array",
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
await writeFile(manifestPath, JSON.stringify(data));
|
|
443
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
await loader.load();
|
|
447
|
+
expect(true).toBe(false); // Should not reach here
|
|
448
|
+
} catch (err) {
|
|
449
|
+
expect(err).toBeInstanceOf(AgentError);
|
|
450
|
+
const message = (err as AgentError).message;
|
|
451
|
+
expect(message).toContain("file");
|
|
452
|
+
expect(message).toContain("model");
|
|
453
|
+
expect(message).toContain("tools");
|
|
454
|
+
expect(message).toContain("capabilities");
|
|
455
|
+
expect(message).toContain("canSpawn");
|
|
456
|
+
expect(message).toContain("constraints");
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("agent with all valid models", () => {
|
|
462
|
+
for (const model of ["sonnet", "opus", "haiku"] as const) {
|
|
463
|
+
test(`accepts model "${model}"`, async () => {
|
|
464
|
+
const data = {
|
|
465
|
+
version: "1.0",
|
|
466
|
+
agents: {
|
|
467
|
+
agent: {
|
|
468
|
+
file: "agent.md",
|
|
469
|
+
model,
|
|
470
|
+
tools: ["Read"],
|
|
471
|
+
capabilities: ["test"],
|
|
472
|
+
canSpawn: false,
|
|
473
|
+
constraints: [],
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
await writeManifest(data);
|
|
478
|
+
const loader = createManifestLoader(manifestPath, agentBaseDir);
|
|
479
|
+
|
|
480
|
+
const manifest = await loader.load();
|
|
481
|
+
expect(manifest.agents.agent).toBeDefined();
|
|
482
|
+
expect(manifest.agents.agent?.model).toBe(model);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
describe("resolveModel", () => {
|
|
489
|
+
const baseManifest: AgentManifest = {
|
|
490
|
+
version: "1.0",
|
|
491
|
+
agents: {
|
|
492
|
+
coordinator: {
|
|
493
|
+
file: "coordinator.md",
|
|
494
|
+
model: "opus",
|
|
495
|
+
tools: ["Read", "Bash"],
|
|
496
|
+
capabilities: ["coordinate"],
|
|
497
|
+
canSpawn: true,
|
|
498
|
+
constraints: [],
|
|
499
|
+
},
|
|
500
|
+
monitor: {
|
|
501
|
+
file: "monitor.md",
|
|
502
|
+
model: "sonnet",
|
|
503
|
+
tools: ["Read", "Bash"],
|
|
504
|
+
capabilities: ["monitor"],
|
|
505
|
+
canSpawn: false,
|
|
506
|
+
constraints: [],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
capabilityIndex: { coordinate: ["coordinator"], monitor: ["monitor"] },
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
function makeConfig(models: LegioConfig["models"] = {}): LegioConfig {
|
|
513
|
+
return {
|
|
514
|
+
project: { name: "test", root: "/tmp/test", canonicalBranch: "main" },
|
|
515
|
+
agents: {
|
|
516
|
+
manifestPath: ".legio/agent-manifest.json",
|
|
517
|
+
baseDir: ".legio/agent-defs",
|
|
518
|
+
maxConcurrent: 5,
|
|
519
|
+
staggerDelayMs: 1000,
|
|
520
|
+
maxDepth: 2,
|
|
521
|
+
},
|
|
522
|
+
worktrees: { baseDir: ".legio/worktrees" },
|
|
523
|
+
beads: { enabled: false },
|
|
524
|
+
mulch: { enabled: false, domains: [], primeFormat: "markdown" },
|
|
525
|
+
merge: { aiResolveEnabled: false, reimagineEnabled: false },
|
|
526
|
+
watchdog: {
|
|
527
|
+
tier0Enabled: false,
|
|
528
|
+
tier0IntervalMs: 30000,
|
|
529
|
+
tier1Enabled: false,
|
|
530
|
+
tier2Enabled: false,
|
|
531
|
+
zombieThresholdMs: 600000,
|
|
532
|
+
nudgeIntervalMs: 60000,
|
|
533
|
+
},
|
|
534
|
+
models,
|
|
535
|
+
logging: { verbose: false, redactSecrets: true },
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
test("returns manifest model when no config override", () => {
|
|
540
|
+
const config = makeConfig();
|
|
541
|
+
expect(resolveModel(config, baseManifest, "coordinator", "haiku")).toBe("opus");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("config override takes precedence over manifest", () => {
|
|
545
|
+
const config = makeConfig({ coordinator: "sonnet" });
|
|
546
|
+
expect(resolveModel(config, baseManifest, "coordinator", "haiku")).toBe("sonnet");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("falls back to default when role is not in manifest or config", () => {
|
|
550
|
+
const config = makeConfig();
|
|
551
|
+
expect(resolveModel(config, baseManifest, "unknown-role", "haiku")).toBe("haiku");
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("config override works for roles not in manifest", () => {
|
|
555
|
+
const config = makeConfig({ supervisor: "opus" });
|
|
556
|
+
expect(resolveModel(config, baseManifest, "supervisor", "sonnet")).toBe("opus");
|
|
557
|
+
});
|
|
558
|
+
});
|