@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.
Files changed (219) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +555 -0
  4. package/agents/builder.md +141 -0
  5. package/agents/coordinator.md +351 -0
  6. package/agents/cto.md +196 -0
  7. package/agents/gateway.md +276 -0
  8. package/agents/lead.md +281 -0
  9. package/agents/merger.md +156 -0
  10. package/agents/monitor.md +212 -0
  11. package/agents/reviewer.md +142 -0
  12. package/agents/scout.md +131 -0
  13. package/agents/supervisor.md +416 -0
  14. package/bin/legio.mjs +38 -0
  15. package/package.json +77 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +102 -0
  18. package/src/agents/hooks-deployer.test.ts +1820 -0
  19. package/src/agents/hooks-deployer.ts +574 -0
  20. package/src/agents/identity.test.ts +614 -0
  21. package/src/agents/identity.ts +385 -0
  22. package/src/agents/lifecycle.test.ts +202 -0
  23. package/src/agents/lifecycle.ts +184 -0
  24. package/src/agents/manifest.test.ts +558 -0
  25. package/src/agents/manifest.ts +297 -0
  26. package/src/agents/overlay.test.ts +592 -0
  27. package/src/agents/overlay.ts +316 -0
  28. package/src/beads/client.test.ts +210 -0
  29. package/src/beads/client.ts +227 -0
  30. package/src/beads/molecules.test.ts +320 -0
  31. package/src/beads/molecules.ts +209 -0
  32. package/src/commands/agents.test.ts +325 -0
  33. package/src/commands/agents.ts +286 -0
  34. package/src/commands/clean.test.ts +730 -0
  35. package/src/commands/clean.ts +653 -0
  36. package/src/commands/completions.test.ts +346 -0
  37. package/src/commands/completions.ts +950 -0
  38. package/src/commands/coordinator.test.ts +1524 -0
  39. package/src/commands/coordinator.ts +880 -0
  40. package/src/commands/costs.test.ts +1015 -0
  41. package/src/commands/costs.ts +473 -0
  42. package/src/commands/dashboard.test.ts +94 -0
  43. package/src/commands/dashboard.ts +607 -0
  44. package/src/commands/doctor.test.ts +295 -0
  45. package/src/commands/doctor.ts +213 -0
  46. package/src/commands/down.test.ts +308 -0
  47. package/src/commands/down.ts +124 -0
  48. package/src/commands/errors.test.ts +648 -0
  49. package/src/commands/errors.ts +255 -0
  50. package/src/commands/feed.test.ts +579 -0
  51. package/src/commands/feed.ts +368 -0
  52. package/src/commands/gateway.test.ts +698 -0
  53. package/src/commands/gateway.ts +419 -0
  54. package/src/commands/group.test.ts +262 -0
  55. package/src/commands/group.ts +539 -0
  56. package/src/commands/hooks.test.ts +292 -0
  57. package/src/commands/hooks.ts +210 -0
  58. package/src/commands/init.test.ts +211 -0
  59. package/src/commands/init.ts +622 -0
  60. package/src/commands/inspect.test.ts +670 -0
  61. package/src/commands/inspect.ts +455 -0
  62. package/src/commands/log.test.ts +1556 -0
  63. package/src/commands/log.ts +752 -0
  64. package/src/commands/logs.test.ts +379 -0
  65. package/src/commands/logs.ts +544 -0
  66. package/src/commands/mail.test.ts +1726 -0
  67. package/src/commands/mail.ts +926 -0
  68. package/src/commands/merge.test.ts +676 -0
  69. package/src/commands/merge.ts +374 -0
  70. package/src/commands/metrics.test.ts +444 -0
  71. package/src/commands/metrics.ts +150 -0
  72. package/src/commands/monitor.test.ts +151 -0
  73. package/src/commands/monitor.ts +394 -0
  74. package/src/commands/nudge.test.ts +230 -0
  75. package/src/commands/nudge.ts +373 -0
  76. package/src/commands/prime.test.ts +467 -0
  77. package/src/commands/prime.ts +386 -0
  78. package/src/commands/replay.test.ts +742 -0
  79. package/src/commands/replay.ts +367 -0
  80. package/src/commands/run.test.ts +443 -0
  81. package/src/commands/run.ts +365 -0
  82. package/src/commands/server.test.ts +626 -0
  83. package/src/commands/server.ts +298 -0
  84. package/src/commands/sling.test.ts +810 -0
  85. package/src/commands/sling.ts +700 -0
  86. package/src/commands/spec.test.ts +206 -0
  87. package/src/commands/spec.ts +171 -0
  88. package/src/commands/status.test.ts +276 -0
  89. package/src/commands/status.ts +339 -0
  90. package/src/commands/stop.test.ts +357 -0
  91. package/src/commands/stop.ts +119 -0
  92. package/src/commands/supervisor.test.ts +186 -0
  93. package/src/commands/supervisor.ts +544 -0
  94. package/src/commands/trace.test.ts +746 -0
  95. package/src/commands/trace.ts +332 -0
  96. package/src/commands/up.test.ts +597 -0
  97. package/src/commands/up.ts +275 -0
  98. package/src/commands/watch.test.ts +152 -0
  99. package/src/commands/watch.ts +238 -0
  100. package/src/commands/worktree.test.ts +648 -0
  101. package/src/commands/worktree.ts +266 -0
  102. package/src/config.test.ts +496 -0
  103. package/src/config.ts +616 -0
  104. package/src/doctor/agents.test.ts +448 -0
  105. package/src/doctor/agents.ts +396 -0
  106. package/src/doctor/config-check.test.ts +184 -0
  107. package/src/doctor/config-check.ts +185 -0
  108. package/src/doctor/consistency.test.ts +645 -0
  109. package/src/doctor/consistency.ts +294 -0
  110. package/src/doctor/databases.test.ts +284 -0
  111. package/src/doctor/databases.ts +211 -0
  112. package/src/doctor/dependencies.test.ts +150 -0
  113. package/src/doctor/dependencies.ts +179 -0
  114. package/src/doctor/logs.test.ts +244 -0
  115. package/src/doctor/logs.ts +295 -0
  116. package/src/doctor/merge-queue.test.ts +210 -0
  117. package/src/doctor/merge-queue.ts +144 -0
  118. package/src/doctor/structure.test.ts +285 -0
  119. package/src/doctor/structure.ts +195 -0
  120. package/src/doctor/types.ts +37 -0
  121. package/src/doctor/version.test.ts +130 -0
  122. package/src/doctor/version.ts +131 -0
  123. package/src/e2e/chat-flow.test.ts +346 -0
  124. package/src/e2e/init-sling-lifecycle.test.ts +288 -0
  125. package/src/errors.test.ts +21 -0
  126. package/src/errors.ts +246 -0
  127. package/src/events/store.test.ts +660 -0
  128. package/src/events/store.ts +344 -0
  129. package/src/events/tool-filter.test.ts +330 -0
  130. package/src/events/tool-filter.ts +126 -0
  131. package/src/global-setup.ts +14 -0
  132. package/src/index.ts +339 -0
  133. package/src/insights/analyzer.test.ts +466 -0
  134. package/src/insights/analyzer.ts +203 -0
  135. package/src/logging/color.test.ts +118 -0
  136. package/src/logging/color.ts +71 -0
  137. package/src/logging/logger.test.ts +812 -0
  138. package/src/logging/logger.ts +266 -0
  139. package/src/logging/reporter.test.ts +258 -0
  140. package/src/logging/reporter.ts +109 -0
  141. package/src/logging/sanitizer.test.ts +190 -0
  142. package/src/logging/sanitizer.ts +57 -0
  143. package/src/mail/broadcast.test.ts +203 -0
  144. package/src/mail/broadcast.ts +92 -0
  145. package/src/mail/client.test.ts +873 -0
  146. package/src/mail/client.ts +236 -0
  147. package/src/mail/store.test.ts +815 -0
  148. package/src/mail/store.ts +402 -0
  149. package/src/merge/queue.test.ts +449 -0
  150. package/src/merge/queue.ts +262 -0
  151. package/src/merge/resolver.test.ts +1453 -0
  152. package/src/merge/resolver.ts +759 -0
  153. package/src/metrics/store.test.ts +1167 -0
  154. package/src/metrics/store.ts +511 -0
  155. package/src/metrics/summary.test.ts +397 -0
  156. package/src/metrics/summary.ts +178 -0
  157. package/src/metrics/transcript.test.ts +643 -0
  158. package/src/metrics/transcript.ts +351 -0
  159. package/src/mulch/client.test.ts +547 -0
  160. package/src/mulch/client.ts +416 -0
  161. package/src/server/audit-store.test.ts +384 -0
  162. package/src/server/audit-store.ts +257 -0
  163. package/src/server/headless.test.ts +180 -0
  164. package/src/server/headless.ts +151 -0
  165. package/src/server/index.test.ts +241 -0
  166. package/src/server/index.ts +317 -0
  167. package/src/server/public/app.js +187 -0
  168. package/src/server/public/apple-touch-icon.png +0 -0
  169. package/src/server/public/components/agent-badge.js +37 -0
  170. package/src/server/public/components/data-table.js +114 -0
  171. package/src/server/public/components/gateway-chat.js +256 -0
  172. package/src/server/public/components/issue-card.js +96 -0
  173. package/src/server/public/components/layout.js +88 -0
  174. package/src/server/public/components/message-bubble.js +120 -0
  175. package/src/server/public/components/stat-card.js +26 -0
  176. package/src/server/public/components/terminal-panel.js +140 -0
  177. package/src/server/public/favicon-16.png +0 -0
  178. package/src/server/public/favicon-32.png +0 -0
  179. package/src/server/public/favicon.ico +0 -0
  180. package/src/server/public/favicon.png +0 -0
  181. package/src/server/public/index.html +64 -0
  182. package/src/server/public/lib/api.js +35 -0
  183. package/src/server/public/lib/markdown.js +8 -0
  184. package/src/server/public/lib/preact-setup.js +8 -0
  185. package/src/server/public/lib/state.js +99 -0
  186. package/src/server/public/lib/utils.js +309 -0
  187. package/src/server/public/lib/ws.js +79 -0
  188. package/src/server/public/views/chat.js +983 -0
  189. package/src/server/public/views/costs.js +692 -0
  190. package/src/server/public/views/dashboard.js +781 -0
  191. package/src/server/public/views/gateway-chat.js +622 -0
  192. package/src/server/public/views/inspect.js +399 -0
  193. package/src/server/public/views/issues.js +470 -0
  194. package/src/server/public/views/setup.js +94 -0
  195. package/src/server/public/views/task-detail.js +422 -0
  196. package/src/server/routes.test.ts +3816 -0
  197. package/src/server/routes.ts +1964 -0
  198. package/src/server/websocket.test.ts +288 -0
  199. package/src/server/websocket.ts +196 -0
  200. package/src/sessions/compat.test.ts +109 -0
  201. package/src/sessions/compat.ts +17 -0
  202. package/src/sessions/store.test.ts +969 -0
  203. package/src/sessions/store.ts +480 -0
  204. package/src/test-helpers.test.ts +97 -0
  205. package/src/test-helpers.ts +143 -0
  206. package/src/types.ts +708 -0
  207. package/src/watchdog/daemon.test.ts +1233 -0
  208. package/src/watchdog/daemon.ts +533 -0
  209. package/src/watchdog/health.test.ts +371 -0
  210. package/src/watchdog/health.ts +248 -0
  211. package/src/watchdog/triage.test.ts +162 -0
  212. package/src/watchdog/triage.ts +193 -0
  213. package/src/worktree/manager.test.ts +444 -0
  214. package/src/worktree/manager.ts +224 -0
  215. package/src/worktree/tmux.test.ts +1238 -0
  216. package/src/worktree/tmux.ts +644 -0
  217. package/templates/CLAUDE.md.tmpl +89 -0
  218. package/templates/hooks.json.tmpl +132 -0
  219. package/templates/overlay.md.tmpl +79 -0
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Tests for agents doctor checks.
3
+ *
4
+ * Uses temp directories with real filesystem operations.
5
+ * No mocks needed -- all operations are cheap and local.
6
+ */
7
+
8
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
12
+ import type { LegioConfig } from "../types.ts";
13
+ import { checkAgents } from "./agents.ts";
14
+
15
+ describe("checkAgents", () => {
16
+ let tempDir: string;
17
+ let legioDir: string;
18
+ let mockConfig: LegioConfig;
19
+
20
+ beforeEach(async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), "agents-test-"));
22
+ legioDir = join(tempDir, ".legio");
23
+ await mkdir(legioDir, { recursive: true });
24
+
25
+ mockConfig = {
26
+ project: {
27
+ name: "test-project",
28
+ root: tempDir,
29
+ canonicalBranch: "main",
30
+ },
31
+ agents: {
32
+ manifestPath: ".legio/agent-manifest.json",
33
+ baseDir: ".legio/agent-defs",
34
+ maxConcurrent: 5,
35
+ staggerDelayMs: 1000,
36
+ maxDepth: 2,
37
+ },
38
+ worktrees: {
39
+ baseDir: ".legio/worktrees",
40
+ },
41
+ beads: {
42
+ enabled: true,
43
+ },
44
+ mulch: {
45
+ enabled: true,
46
+ domains: [],
47
+ primeFormat: "markdown",
48
+ },
49
+ merge: {
50
+ aiResolveEnabled: false,
51
+ reimagineEnabled: false,
52
+ },
53
+ watchdog: {
54
+ tier0Enabled: true,
55
+ tier0IntervalMs: 30000,
56
+ tier1Enabled: false,
57
+ tier2Enabled: false,
58
+ zombieThresholdMs: 600000,
59
+ nudgeIntervalMs: 60000,
60
+ },
61
+ models: {},
62
+ logging: {
63
+ verbose: false,
64
+ redactSecrets: true,
65
+ },
66
+ };
67
+ });
68
+
69
+ afterEach(async () => {
70
+ await rm(tempDir, { recursive: true, force: true });
71
+ });
72
+
73
+ test("fails when manifest is missing", async () => {
74
+ const checks = await checkAgents(mockConfig, legioDir);
75
+
76
+ const parseCheck = checks.find((c) => c.name === "Manifest parsing");
77
+ expect(parseCheck).toBeDefined();
78
+ expect(parseCheck?.status).toBe("fail");
79
+ });
80
+
81
+ test("fails when manifest has invalid JSON", async () => {
82
+ await writeFile(join(legioDir, "agent-manifest.json"), "invalid json{");
83
+
84
+ const checks = await checkAgents(mockConfig, legioDir);
85
+
86
+ const parseCheck = checks.find((c) => c.name === "Manifest parsing");
87
+ expect(parseCheck?.status).toBe("fail");
88
+ expect(parseCheck?.details?.some((d) => d.includes("JSON"))).toBe(true);
89
+ });
90
+
91
+ test("passes when manifest is valid", async () => {
92
+ const manifest = {
93
+ version: "1.0",
94
+ agents: {
95
+ scout: {
96
+ file: "scout.md",
97
+ model: "haiku",
98
+ tools: ["Read"],
99
+ capabilities: ["explore"],
100
+ canSpawn: false,
101
+ constraints: [],
102
+ },
103
+ },
104
+ capabilityIndex: {
105
+ explore: ["scout"],
106
+ },
107
+ };
108
+
109
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
110
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
111
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
112
+
113
+ const checks = await checkAgents(mockConfig, legioDir);
114
+
115
+ const parseCheck = checks.find((c) => c.name === "Manifest parsing");
116
+ expect(parseCheck?.status).toBe("pass");
117
+ });
118
+
119
+ test("fails when agent has invalid model", async () => {
120
+ const manifest = {
121
+ version: "1.0",
122
+ agents: {
123
+ scout: {
124
+ file: "scout.md",
125
+ model: "invalid-model",
126
+ tools: ["Read"],
127
+ capabilities: ["explore"],
128
+ canSpawn: false,
129
+ constraints: [],
130
+ },
131
+ },
132
+ capabilityIndex: {
133
+ explore: ["scout"],
134
+ },
135
+ };
136
+
137
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
138
+
139
+ const checks = await checkAgents(mockConfig, legioDir);
140
+
141
+ const parseCheck = checks.find((c) => c.name === "Manifest parsing");
142
+ expect(parseCheck?.status).toBe("fail");
143
+ expect(parseCheck?.details?.some((d) => d.includes("model"))).toBe(true);
144
+ });
145
+
146
+ test("fails when agent has zero capabilities", async () => {
147
+ const manifest = {
148
+ version: "1.0",
149
+ agents: {
150
+ scout: {
151
+ file: "scout.md",
152
+ model: "haiku",
153
+ tools: ["Read"],
154
+ capabilities: [],
155
+ canSpawn: false,
156
+ constraints: [],
157
+ },
158
+ },
159
+ capabilityIndex: {},
160
+ };
161
+
162
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
163
+
164
+ const checks = await checkAgents(mockConfig, legioDir);
165
+
166
+ const parseCheck = checks.find((c) => c.name === "Manifest parsing");
167
+ expect(parseCheck?.status).toBe("fail");
168
+ expect(parseCheck?.details?.some((d) => d.includes("capability"))).toBe(true);
169
+ });
170
+
171
+ test("fails when referenced .md file is missing", async () => {
172
+ const manifest = {
173
+ version: "1.0",
174
+ agents: {
175
+ scout: {
176
+ file: "scout.md",
177
+ model: "haiku",
178
+ tools: ["Read"],
179
+ capabilities: ["explore"],
180
+ canSpawn: false,
181
+ constraints: [],
182
+ },
183
+ },
184
+ capabilityIndex: {
185
+ explore: ["scout"],
186
+ },
187
+ };
188
+
189
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
190
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
191
+ // Don't create scout.md
192
+
193
+ const checks = await checkAgents(mockConfig, legioDir);
194
+
195
+ const filesCheck = checks.find((c) => c.name === "Agent definition files");
196
+ expect(filesCheck?.status).toBe("fail");
197
+ expect(filesCheck?.details?.some((d) => d.includes("scout.md"))).toBe(true);
198
+ });
199
+
200
+ test("warns when capability index is inconsistent", async () => {
201
+ const manifest = {
202
+ version: "1.0",
203
+ agents: {
204
+ scout: {
205
+ file: "scout.md",
206
+ model: "haiku",
207
+ tools: ["Read"],
208
+ capabilities: ["explore", "research"],
209
+ canSpawn: false,
210
+ constraints: [],
211
+ },
212
+ },
213
+ capabilityIndex: {
214
+ explore: ["scout"],
215
+ // Missing "research" from index
216
+ },
217
+ };
218
+
219
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
220
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
221
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
222
+
223
+ const checks = await checkAgents(mockConfig, legioDir);
224
+
225
+ const indexCheck = checks.find((c) => c.name === "Capability index");
226
+ expect(indexCheck?.status).toBe("warn");
227
+ expect(indexCheck?.details?.some((d) => d.includes("research"))).toBe(true);
228
+ });
229
+
230
+ test("passes when no identity files exist", async () => {
231
+ const manifest = {
232
+ version: "1.0",
233
+ agents: {
234
+ scout: {
235
+ file: "scout.md",
236
+ model: "haiku",
237
+ tools: ["Read"],
238
+ capabilities: ["explore"],
239
+ canSpawn: false,
240
+ constraints: [],
241
+ },
242
+ },
243
+ capabilityIndex: {
244
+ explore: ["scout"],
245
+ },
246
+ };
247
+
248
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
249
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
250
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
251
+
252
+ const checks = await checkAgents(mockConfig, legioDir);
253
+
254
+ const identityCheck = checks.find((c) => c.name === "Agent identities");
255
+ expect(identityCheck?.status).toBe("pass");
256
+ });
257
+
258
+ test("validates identity files correctly", async () => {
259
+ const manifest = {
260
+ version: "1.0",
261
+ agents: {
262
+ scout: {
263
+ file: "scout.md",
264
+ model: "haiku",
265
+ tools: ["Read"],
266
+ capabilities: ["explore"],
267
+ canSpawn: false,
268
+ constraints: [],
269
+ },
270
+ },
271
+ capabilityIndex: {
272
+ explore: ["scout"],
273
+ },
274
+ };
275
+
276
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
277
+ await mkdir(join(legioDir, "agents", "scout"), { recursive: true });
278
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
279
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
280
+
281
+ const identity = `name: scout
282
+ capability: explore
283
+ created: "2024-01-01T00:00:00Z"
284
+ sessionsCompleted: 5
285
+ expertiseDomains: []
286
+ recentTasks: []
287
+ `;
288
+
289
+ await writeFile(join(legioDir, "agents", "scout", "identity.yaml"), identity);
290
+
291
+ const checks = await checkAgents(mockConfig, legioDir);
292
+
293
+ const identityCheck = checks.find((c) => c.name === "Identity validation");
294
+ expect(identityCheck?.status).toBe("pass");
295
+ });
296
+
297
+ test("warns when identity has invalid timestamp", async () => {
298
+ const manifest = {
299
+ version: "1.0",
300
+ agents: {
301
+ scout: {
302
+ file: "scout.md",
303
+ model: "haiku",
304
+ tools: ["Read"],
305
+ capabilities: ["explore"],
306
+ canSpawn: false,
307
+ constraints: [],
308
+ },
309
+ },
310
+ capabilityIndex: {
311
+ explore: ["scout"],
312
+ },
313
+ };
314
+
315
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
316
+ await mkdir(join(legioDir, "agents", "scout"), { recursive: true });
317
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
318
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
319
+
320
+ const identity = `name: scout
321
+ capability: explore
322
+ created: "invalid-timestamp"
323
+ sessionsCompleted: 5
324
+ `;
325
+
326
+ await writeFile(join(legioDir, "agents", "scout", "identity.yaml"), identity);
327
+
328
+ const checks = await checkAgents(mockConfig, legioDir);
329
+
330
+ const identityCheck = checks.find((c) => c.name === "Identity validation");
331
+ expect(identityCheck?.status).toBe("warn");
332
+ expect(identityCheck?.details?.some((d) => d.includes("timestamp"))).toBe(true);
333
+ });
334
+
335
+ test("warns when identity has negative sessionsCompleted", async () => {
336
+ const manifest = {
337
+ version: "1.0",
338
+ agents: {
339
+ scout: {
340
+ file: "scout.md",
341
+ model: "haiku",
342
+ tools: ["Read"],
343
+ capabilities: ["explore"],
344
+ canSpawn: false,
345
+ constraints: [],
346
+ },
347
+ },
348
+ capabilityIndex: {
349
+ explore: ["scout"],
350
+ },
351
+ };
352
+
353
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
354
+ await mkdir(join(legioDir, "agents", "scout"), { recursive: true });
355
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
356
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
357
+
358
+ const identity = `name: scout
359
+ capability: explore
360
+ created: "2024-01-01T00:00:00Z"
361
+ sessionsCompleted: -5
362
+ `;
363
+
364
+ await writeFile(join(legioDir, "agents", "scout", "identity.yaml"), identity);
365
+
366
+ const checks = await checkAgents(mockConfig, legioDir);
367
+
368
+ const identityCheck = checks.find((c) => c.name === "Identity validation");
369
+ expect(identityCheck?.status).toBe("warn");
370
+ expect(identityCheck?.details?.some((d) => d.includes("sessionsCompleted"))).toBe(true);
371
+ });
372
+
373
+ test("warns about stale identity files", async () => {
374
+ const manifest = {
375
+ version: "1.0",
376
+ agents: {
377
+ scout: {
378
+ file: "scout.md",
379
+ model: "haiku",
380
+ tools: ["Read"],
381
+ capabilities: ["explore"],
382
+ canSpawn: false,
383
+ constraints: [],
384
+ },
385
+ },
386
+ capabilityIndex: {
387
+ explore: ["scout"],
388
+ },
389
+ };
390
+
391
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
392
+ await mkdir(join(legioDir, "agents", "old-agent"), { recursive: true });
393
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
394
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
395
+
396
+ const staleIdentity = `name: old-agent
397
+ capability: obsolete
398
+ created: "2024-01-01T00:00:00Z"
399
+ sessionsCompleted: 5
400
+ `;
401
+
402
+ await writeFile(join(legioDir, "agents", "old-agent", "identity.yaml"), staleIdentity);
403
+
404
+ const checks = await checkAgents(mockConfig, legioDir);
405
+
406
+ const staleCheck = checks.find((c) => c.name === "Stale identities");
407
+ expect(staleCheck?.status).toBe("warn");
408
+ expect(staleCheck?.details?.some((d) => d.includes("old-agent"))).toBe(true);
409
+ });
410
+
411
+ test("warns when identity name contains invalid characters", async () => {
412
+ const manifest = {
413
+ version: "1.0",
414
+ agents: {
415
+ scout: {
416
+ file: "scout.md",
417
+ model: "haiku",
418
+ tools: ["Read"],
419
+ capabilities: ["explore"],
420
+ canSpawn: false,
421
+ constraints: [],
422
+ },
423
+ },
424
+ capabilityIndex: {
425
+ explore: ["scout"],
426
+ },
427
+ };
428
+
429
+ await mkdir(join(legioDir, "agent-defs"), { recursive: true });
430
+ await mkdir(join(legioDir, "agents", "scout"), { recursive: true });
431
+ await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
432
+ await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
433
+
434
+ const identity = `name: "scout@invalid!"
435
+ capability: explore
436
+ created: "2024-01-01T00:00:00Z"
437
+ sessionsCompleted: 5
438
+ `;
439
+
440
+ await writeFile(join(legioDir, "agents", "scout", "identity.yaml"), identity);
441
+
442
+ const checks = await checkAgents(mockConfig, legioDir);
443
+
444
+ const identityCheck = checks.find((c) => c.name === "Identity validation");
445
+ expect(identityCheck?.status).toBe("warn");
446
+ expect(identityCheck?.details?.some((d) => d.includes("invalid characters"))).toBe(true);
447
+ });
448
+ });