@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,297 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { AgentError } from "../errors.ts";
4
+ import type { AgentDefinition, AgentManifest, LegioConfig } from "../types.ts";
5
+
6
+ /**
7
+ * Interface for loading, querying, and validating an agent manifest.
8
+ */
9
+ export interface ManifestLoader {
10
+ /** Load the manifest from disk, parse, validate, and build indexes. */
11
+ load(): Promise<AgentManifest>;
12
+ /** Get an agent definition by name. Returns undefined if not found. */
13
+ getAgent(name: string): AgentDefinition | undefined;
14
+ /** Find all agent names whose capabilities include the given capability. */
15
+ findByCapability(capability: string): AgentDefinition[];
16
+ /** Validate the manifest. Returns a list of errors (empty = valid). */
17
+ validate(): string[];
18
+ }
19
+
20
+ /**
21
+ * Raw manifest shape as read from JSON before validation.
22
+ * Used internally to validate structure before casting to AgentManifest.
23
+ */
24
+ interface RawManifest {
25
+ version?: unknown;
26
+ agents?: unknown;
27
+ capabilityIndex?: unknown;
28
+ }
29
+
30
+ const VALID_MODELS = new Set(["sonnet", "opus", "haiku"]);
31
+
32
+ /**
33
+ * Validate that a raw parsed object conforms to the AgentDefinition shape.
34
+ * Returns a list of error messages for any violations.
35
+ */
36
+ function validateAgentDefinition(name: string, raw: unknown): string[] {
37
+ const errors: string[] = [];
38
+
39
+ if (raw === null || typeof raw !== "object") {
40
+ errors.push(`Agent "${name}": definition must be an object`);
41
+ return errors;
42
+ }
43
+
44
+ const def = raw as Record<string, unknown>;
45
+
46
+ if (typeof def.file !== "string" || def.file.length === 0) {
47
+ errors.push(`Agent "${name}": "file" must be a non-empty string`);
48
+ }
49
+
50
+ if (typeof def.model !== "string" || !VALID_MODELS.has(def.model)) {
51
+ errors.push(`Agent "${name}": "model" must be one of: sonnet, opus, haiku`);
52
+ }
53
+
54
+ if (!Array.isArray(def.tools)) {
55
+ errors.push(`Agent "${name}": "tools" must be an array`);
56
+ } else {
57
+ for (let i = 0; i < def.tools.length; i++) {
58
+ if (typeof def.tools[i] !== "string") {
59
+ errors.push(`Agent "${name}": "tools[${i}]" must be a string`);
60
+ }
61
+ }
62
+ }
63
+
64
+ if (!Array.isArray(def.capabilities)) {
65
+ errors.push(`Agent "${name}": "capabilities" must be an array`);
66
+ } else {
67
+ for (let i = 0; i < def.capabilities.length; i++) {
68
+ if (typeof def.capabilities[i] !== "string") {
69
+ errors.push(`Agent "${name}": "capabilities[${i}]" must be a string`);
70
+ }
71
+ }
72
+ }
73
+
74
+ if (typeof def.canSpawn !== "boolean") {
75
+ errors.push(`Agent "${name}": "canSpawn" must be a boolean`);
76
+ }
77
+
78
+ if (!Array.isArray(def.constraints)) {
79
+ errors.push(`Agent "${name}": "constraints" must be an array`);
80
+ } else {
81
+ for (let i = 0; i < def.constraints.length; i++) {
82
+ if (typeof def.constraints[i] !== "string") {
83
+ errors.push(`Agent "${name}": "constraints[${i}]" must be a string`);
84
+ }
85
+ }
86
+ }
87
+
88
+ return errors;
89
+ }
90
+
91
+ /**
92
+ * Build a capability index: maps each capability string to the list of
93
+ * agent names that declare that capability.
94
+ */
95
+ function buildCapabilityIndex(agents: Record<string, AgentDefinition>): Record<string, string[]> {
96
+ const index: Record<string, string[]> = {};
97
+
98
+ for (const [name, def] of Object.entries(agents)) {
99
+ for (const cap of def.capabilities) {
100
+ const existing = index[cap];
101
+ if (existing) {
102
+ existing.push(name);
103
+ } else {
104
+ index[cap] = [name];
105
+ }
106
+ }
107
+ }
108
+
109
+ return index;
110
+ }
111
+
112
+ /**
113
+ * Create a ManifestLoader that reads from the given manifest JSON path
114
+ * and resolves agent .md files relative to the given base directory.
115
+ *
116
+ * @param manifestPath - Absolute path to the agent-manifest.json file
117
+ * @param agentBaseDir - Absolute path to the directory containing agent .md files
118
+ */
119
+ export function createManifestLoader(manifestPath: string, agentBaseDir: string): ManifestLoader {
120
+ let manifest: AgentManifest | null = null;
121
+
122
+ return {
123
+ async load(): Promise<AgentManifest> {
124
+ const exists = await access(manifestPath)
125
+ .then(() => true)
126
+ .catch(() => false);
127
+
128
+ if (!exists) {
129
+ throw new AgentError(`Agent manifest not found: ${manifestPath}`);
130
+ }
131
+
132
+ let text: string;
133
+ try {
134
+ text = await readFile(manifestPath, "utf-8");
135
+ } catch (err) {
136
+ throw new AgentError(`Failed to read agent manifest: ${manifestPath}`, {
137
+ cause: err instanceof Error ? err : undefined,
138
+ });
139
+ }
140
+
141
+ let raw: RawManifest;
142
+ try {
143
+ raw = JSON.parse(text) as RawManifest;
144
+ } catch (err) {
145
+ throw new AgentError(`Failed to parse agent manifest as JSON: ${manifestPath}`, {
146
+ cause: err instanceof Error ? err : undefined,
147
+ });
148
+ }
149
+
150
+ // Validate top-level structure
151
+ if (typeof raw.version !== "string" || raw.version.length === 0) {
152
+ throw new AgentError(
153
+ 'Agent manifest missing or invalid "version" field (must be a non-empty string)',
154
+ );
155
+ }
156
+
157
+ if (raw.agents === null || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
158
+ throw new AgentError(
159
+ 'Agent manifest missing or invalid "agents" field (must be an object)',
160
+ );
161
+ }
162
+
163
+ const rawAgents = raw.agents as Record<string, unknown>;
164
+
165
+ // Validate each agent definition
166
+ const allErrors: string[] = [];
167
+ for (const [name, def] of Object.entries(rawAgents)) {
168
+ const defErrors = validateAgentDefinition(name, def);
169
+ allErrors.push(...defErrors);
170
+ }
171
+
172
+ if (allErrors.length > 0) {
173
+ throw new AgentError(`Agent manifest validation failed:\n${allErrors.join("\n")}`);
174
+ }
175
+
176
+ // At this point, all agent definitions have been validated
177
+ const agents = rawAgents as Record<string, AgentDefinition>;
178
+
179
+ // Verify that all referenced .md files exist
180
+ for (const [name, def] of Object.entries(agents)) {
181
+ const filePath = join(agentBaseDir, def.file);
182
+ const mdExists = await access(filePath)
183
+ .then(() => true)
184
+ .catch(() => false);
185
+
186
+ if (!mdExists) {
187
+ throw new AgentError(
188
+ `Agent "${name}" references file "${def.file}" which does not exist at: ${filePath}`,
189
+ { agentName: name },
190
+ );
191
+ }
192
+ }
193
+
194
+ // Build the capability index
195
+ const capabilityIndex = buildCapabilityIndex(agents);
196
+
197
+ manifest = {
198
+ version: raw.version,
199
+ agents,
200
+ capabilityIndex,
201
+ };
202
+
203
+ return manifest;
204
+ },
205
+
206
+ getAgent(name: string): AgentDefinition | undefined {
207
+ if (!manifest) {
208
+ return undefined;
209
+ }
210
+ return manifest.agents[name];
211
+ },
212
+
213
+ findByCapability(capability: string): AgentDefinition[] {
214
+ if (!manifest) {
215
+ return [];
216
+ }
217
+
218
+ const agentNames = manifest.capabilityIndex[capability];
219
+ if (!agentNames) {
220
+ return [];
221
+ }
222
+
223
+ const results: AgentDefinition[] = [];
224
+ for (const name of agentNames) {
225
+ const def = manifest.agents[name];
226
+ if (def) {
227
+ results.push(def);
228
+ }
229
+ }
230
+ return results;
231
+ },
232
+
233
+ validate(): string[] {
234
+ if (!manifest) {
235
+ return ["Manifest not loaded. Call load() first."];
236
+ }
237
+
238
+ const errors: string[] = [];
239
+
240
+ // Re-validate each agent definition structurally
241
+ for (const [name, def] of Object.entries(manifest.agents)) {
242
+ const defErrors = validateAgentDefinition(name, def);
243
+ errors.push(...defErrors);
244
+ }
245
+
246
+ // Verify capability index consistency
247
+ for (const [cap, names] of Object.entries(manifest.capabilityIndex)) {
248
+ for (const name of names) {
249
+ const def = manifest.agents[name];
250
+ if (!def) {
251
+ errors.push(
252
+ `Capability index references agent "${name}" for capability "${cap}", but agent does not exist`,
253
+ );
254
+ } else if (!def.capabilities.includes(cap)) {
255
+ errors.push(
256
+ `Capability index lists agent "${name}" under "${cap}", but agent does not declare that capability`,
257
+ );
258
+ }
259
+ }
260
+ }
261
+
262
+ // Check that every agent capability is present in the index
263
+ for (const [name, def] of Object.entries(manifest.agents)) {
264
+ for (const cap of def.capabilities) {
265
+ const indexed = manifest.capabilityIndex[cap];
266
+ if (!indexed || !indexed.includes(name)) {
267
+ errors.push(
268
+ `Agent "${name}" declares capability "${cap}" but is not listed in the capability index`,
269
+ );
270
+ }
271
+ }
272
+ }
273
+
274
+ return errors;
275
+ },
276
+ };
277
+ }
278
+
279
+ type ModelName = "sonnet" | "opus" | "haiku";
280
+
281
+ /**
282
+ * Resolve the model for an agent role.
283
+ *
284
+ * Resolution order: config.models override > manifest default > fallback.
285
+ */
286
+ export function resolveModel(
287
+ config: LegioConfig,
288
+ manifest: AgentManifest,
289
+ role: string,
290
+ fallback: ModelName,
291
+ ): ModelName {
292
+ const configModel = config.models[role];
293
+ if (configModel) return configModel;
294
+ const manifestModel = manifest.agents[role]?.model;
295
+ if (manifestModel) return manifestModel;
296
+ return fallback;
297
+ }