@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,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: legio init [--force]
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds the `.legio/` directory in the current project with:
|
|
5
|
+
* - config.yaml (serialized from DEFAULT_CONFIG)
|
|
6
|
+
* - agent-manifest.json (starter agent definitions)
|
|
7
|
+
* - hooks.json (central hooks config)
|
|
8
|
+
* - Required subdirectories (agents/, worktrees/, specs/, logs/)
|
|
9
|
+
* - .gitignore entries for transient files
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
14
|
+
import { basename, join } from "node:path";
|
|
15
|
+
import Database from "better-sqlite3";
|
|
16
|
+
import { DEFAULT_CONFIG } from "../config.ts";
|
|
17
|
+
import { ValidationError } from "../errors.ts";
|
|
18
|
+
import type { AgentManifest, LegioConfig } from "../types.ts";
|
|
19
|
+
|
|
20
|
+
const LEGIO_DIR = ".legio";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a file exists using access().
|
|
24
|
+
*/
|
|
25
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
26
|
+
try {
|
|
27
|
+
await access(path);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run an external command and collect stdout/stderr + exit code.
|
|
36
|
+
*/
|
|
37
|
+
async function runCommand(
|
|
38
|
+
cmd: string[],
|
|
39
|
+
opts?: { cwd?: string },
|
|
40
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
41
|
+
const [command, ...args] = cmd;
|
|
42
|
+
if (!command) {
|
|
43
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 1 });
|
|
44
|
+
}
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const proc = spawn(command, args, {
|
|
47
|
+
cwd: opts?.cwd,
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
49
|
+
});
|
|
50
|
+
const stdoutChunks: Buffer[] = [];
|
|
51
|
+
const stderrChunks: Buffer[] = [];
|
|
52
|
+
proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
53
|
+
proc.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
54
|
+
proc.on("close", (code) => {
|
|
55
|
+
resolve({
|
|
56
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
57
|
+
stderr: Buffer.concat(stderrChunks).toString(),
|
|
58
|
+
exitCode: code ?? 1,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect the project name from git or fall back to directory name.
|
|
66
|
+
*/
|
|
67
|
+
async function detectProjectName(root: string): Promise<string> {
|
|
68
|
+
// Try git remote origin
|
|
69
|
+
try {
|
|
70
|
+
const { stdout, exitCode } = await runCommand(["git", "remote", "get-url", "origin"], {
|
|
71
|
+
cwd: root,
|
|
72
|
+
});
|
|
73
|
+
if (exitCode === 0) {
|
|
74
|
+
const url = stdout.trim();
|
|
75
|
+
// Extract repo name from URL: git@host:user/repo.git or https://host/user/repo.git
|
|
76
|
+
const match = url.match(/\/([^/]+?)(?:\.git)?$/);
|
|
77
|
+
if (match?.[1]) {
|
|
78
|
+
return match[1];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Git not available or not a git repo
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return basename(root);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Detect the canonical branch name from git.
|
|
90
|
+
*/
|
|
91
|
+
async function detectCanonicalBranch(root: string): Promise<string> {
|
|
92
|
+
try {
|
|
93
|
+
const { stdout, exitCode } = await runCommand(
|
|
94
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
95
|
+
{ cwd: root },
|
|
96
|
+
);
|
|
97
|
+
if (exitCode === 0) {
|
|
98
|
+
const ref = stdout.trim();
|
|
99
|
+
// refs/remotes/origin/main -> main
|
|
100
|
+
const branch = ref.split("/").pop();
|
|
101
|
+
if (branch) {
|
|
102
|
+
return branch;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Not available
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fall back to checking current branch
|
|
110
|
+
try {
|
|
111
|
+
const { stdout, exitCode } = await runCommand(["git", "branch", "--show-current"], {
|
|
112
|
+
cwd: root,
|
|
113
|
+
});
|
|
114
|
+
if (exitCode === 0) {
|
|
115
|
+
const branch = stdout.trim();
|
|
116
|
+
if (branch === "main" || branch === "master" || branch === "develop") {
|
|
117
|
+
return branch;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Not available
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return "main";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Serialize an LegioConfig to YAML format.
|
|
129
|
+
*
|
|
130
|
+
* Handles nested objects with indentation, scalar values,
|
|
131
|
+
* arrays with `- item` syntax, and empty arrays as `[]`.
|
|
132
|
+
*/
|
|
133
|
+
function serializeConfigToYaml(config: LegioConfig): string {
|
|
134
|
+
const lines: string[] = [];
|
|
135
|
+
lines.push("# Legio configuration");
|
|
136
|
+
lines.push("# See: https://github.com/legio/legio");
|
|
137
|
+
lines.push("");
|
|
138
|
+
|
|
139
|
+
serializeObject(config as unknown as Record<string, unknown>, lines, 0);
|
|
140
|
+
|
|
141
|
+
return `${lines.join("\n")}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Recursively serialize an object to YAML lines.
|
|
146
|
+
*/
|
|
147
|
+
function serializeObject(obj: Record<string, unknown>, lines: string[], depth: number): void {
|
|
148
|
+
const indent = " ".repeat(depth);
|
|
149
|
+
|
|
150
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
151
|
+
if (value === null || value === undefined) {
|
|
152
|
+
lines.push(`${indent}${key}: null`);
|
|
153
|
+
} else if (typeof value === "object" && !Array.isArray(value)) {
|
|
154
|
+
lines.push(`${indent}${key}:`);
|
|
155
|
+
serializeObject(value as Record<string, unknown>, lines, depth + 1);
|
|
156
|
+
} else if (Array.isArray(value)) {
|
|
157
|
+
if (value.length === 0) {
|
|
158
|
+
lines.push(`${indent}${key}: []`);
|
|
159
|
+
} else {
|
|
160
|
+
lines.push(`${indent}${key}:`);
|
|
161
|
+
const itemIndent = " ".repeat(depth + 1);
|
|
162
|
+
for (const item of value) {
|
|
163
|
+
lines.push(`${itemIndent}- ${formatYamlValue(item)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
lines.push(`${indent}${key}: ${formatYamlValue(value)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Format a scalar value for YAML output.
|
|
174
|
+
*/
|
|
175
|
+
function formatYamlValue(value: unknown): string {
|
|
176
|
+
if (typeof value === "string") {
|
|
177
|
+
// Quote strings that could be misinterpreted
|
|
178
|
+
if (
|
|
179
|
+
value === "" ||
|
|
180
|
+
value === "true" ||
|
|
181
|
+
value === "false" ||
|
|
182
|
+
value === "null" ||
|
|
183
|
+
value.includes(":") ||
|
|
184
|
+
value.includes("#") ||
|
|
185
|
+
value.includes("'") ||
|
|
186
|
+
value.includes('"') ||
|
|
187
|
+
value.includes("\n") ||
|
|
188
|
+
/^\d/.test(value)
|
|
189
|
+
) {
|
|
190
|
+
// Use double quotes, escaping inner double quotes
|
|
191
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof value === "number") {
|
|
197
|
+
return String(value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof value === "boolean") {
|
|
201
|
+
return value ? "true" : "false";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (value === null || value === undefined) {
|
|
205
|
+
return "null";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return String(value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build the starter agent manifest.
|
|
213
|
+
*/
|
|
214
|
+
function buildAgentManifest(): AgentManifest {
|
|
215
|
+
const agents: AgentManifest["agents"] = {
|
|
216
|
+
scout: {
|
|
217
|
+
file: "scout.md",
|
|
218
|
+
model: "haiku",
|
|
219
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
220
|
+
capabilities: ["explore", "research"],
|
|
221
|
+
canSpawn: false,
|
|
222
|
+
constraints: ["read-only"],
|
|
223
|
+
},
|
|
224
|
+
builder: {
|
|
225
|
+
file: "builder.md",
|
|
226
|
+
model: "sonnet",
|
|
227
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
|
228
|
+
capabilities: ["implement", "refactor", "fix"],
|
|
229
|
+
canSpawn: false,
|
|
230
|
+
constraints: [],
|
|
231
|
+
},
|
|
232
|
+
reviewer: {
|
|
233
|
+
file: "reviewer.md",
|
|
234
|
+
model: "sonnet",
|
|
235
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
236
|
+
capabilities: ["review", "validate"],
|
|
237
|
+
canSpawn: false,
|
|
238
|
+
constraints: ["read-only"],
|
|
239
|
+
},
|
|
240
|
+
lead: {
|
|
241
|
+
file: "lead.md",
|
|
242
|
+
model: "sonnet",
|
|
243
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Task"],
|
|
244
|
+
capabilities: ["coordinate", "implement", "review"],
|
|
245
|
+
canSpawn: true,
|
|
246
|
+
constraints: [],
|
|
247
|
+
},
|
|
248
|
+
merger: {
|
|
249
|
+
file: "merger.md",
|
|
250
|
+
model: "sonnet",
|
|
251
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
|
252
|
+
capabilities: ["merge", "resolve-conflicts"],
|
|
253
|
+
canSpawn: false,
|
|
254
|
+
constraints: [],
|
|
255
|
+
},
|
|
256
|
+
coordinator: {
|
|
257
|
+
file: "coordinator.md",
|
|
258
|
+
model: "opus",
|
|
259
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
260
|
+
capabilities: ["coordinate", "dispatch", "escalate"],
|
|
261
|
+
canSpawn: true,
|
|
262
|
+
constraints: ["read-only", "no-worktree"],
|
|
263
|
+
},
|
|
264
|
+
supervisor: {
|
|
265
|
+
file: "supervisor.md",
|
|
266
|
+
model: "opus",
|
|
267
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Task"],
|
|
268
|
+
capabilities: ["coordinate", "supervise"],
|
|
269
|
+
canSpawn: true,
|
|
270
|
+
constraints: [],
|
|
271
|
+
},
|
|
272
|
+
monitor: {
|
|
273
|
+
file: "monitor.md",
|
|
274
|
+
model: "sonnet",
|
|
275
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
276
|
+
capabilities: ["monitor", "patrol"],
|
|
277
|
+
canSpawn: false,
|
|
278
|
+
constraints: ["read-only", "no-worktree"],
|
|
279
|
+
},
|
|
280
|
+
cto: {
|
|
281
|
+
file: "cto.md",
|
|
282
|
+
model: "opus",
|
|
283
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
284
|
+
capabilities: ["strategy", "analyze", "advise"],
|
|
285
|
+
canSpawn: false,
|
|
286
|
+
constraints: ["read-only"],
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Build capability index: map each capability to agent names that declare it
|
|
291
|
+
const capabilityIndex: Record<string, string[]> = {};
|
|
292
|
+
for (const [name, def] of Object.entries(agents)) {
|
|
293
|
+
for (const cap of def.capabilities) {
|
|
294
|
+
const existing = capabilityIndex[cap];
|
|
295
|
+
if (existing) {
|
|
296
|
+
existing.push(name);
|
|
297
|
+
} else {
|
|
298
|
+
capabilityIndex[cap] = [name];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { version: "1.0", agents, capabilityIndex };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Build the hooks.json content for the project orchestrator.
|
|
308
|
+
*
|
|
309
|
+
* Always generates from scratch (not from the agent template, which contains
|
|
310
|
+
* {{AGENT_NAME}} placeholders and space indentation). Uses tab indentation
|
|
311
|
+
* to match Biome formatting rules.
|
|
312
|
+
*/
|
|
313
|
+
function buildHooksJson(): string {
|
|
314
|
+
// Tool name extraction: reads hook stdin JSON and extracts tool_name field.
|
|
315
|
+
// Claude Code sends {"tool_name":"Bash","tool_input":{...}} on stdin for
|
|
316
|
+
// PreToolUse/PostToolUse hooks.
|
|
317
|
+
const toolNameExtract =
|
|
318
|
+
'read -r INPUT; TOOL_NAME=$(echo "$INPUT" | sed \'s/.*"tool_name": *"\\([^"]*\\)".*/\\1/\');';
|
|
319
|
+
|
|
320
|
+
const hooks = {
|
|
321
|
+
hooks: {
|
|
322
|
+
SessionStart: [
|
|
323
|
+
{
|
|
324
|
+
matcher: "",
|
|
325
|
+
hooks: [
|
|
326
|
+
{
|
|
327
|
+
type: "command",
|
|
328
|
+
command: "legio prime --agent orchestrator",
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
UserPromptSubmit: [
|
|
334
|
+
{
|
|
335
|
+
matcher: "",
|
|
336
|
+
hooks: [
|
|
337
|
+
{
|
|
338
|
+
type: "command",
|
|
339
|
+
command: "legio mail check --inject --agent orchestrator",
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
PreToolUse: [
|
|
345
|
+
{
|
|
346
|
+
matcher: "Bash",
|
|
347
|
+
hooks: [
|
|
348
|
+
{
|
|
349
|
+
type: "command",
|
|
350
|
+
command:
|
|
351
|
+
'read -r INPUT; CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\'); if echo "$CMD" | grep -qE \'\\bgit\\s+push\\b\'; then echo \'{"decision":"block","reason":"git push is blocked by legio — merge locally, push manually when ready"}\'; exit 0; fi;',
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
matcher: "",
|
|
357
|
+
hooks: [
|
|
358
|
+
{
|
|
359
|
+
type: "command",
|
|
360
|
+
command: `${toolNameExtract} legio log tool-start --agent orchestrator --tool-name "$TOOL_NAME"`,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
PostToolUse: [
|
|
366
|
+
{
|
|
367
|
+
matcher: "",
|
|
368
|
+
hooks: [
|
|
369
|
+
{
|
|
370
|
+
type: "command",
|
|
371
|
+
command: `${toolNameExtract} legio log tool-end --agent orchestrator --tool-name "$TOOL_NAME"`,
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
Stop: [
|
|
377
|
+
{
|
|
378
|
+
matcher: "",
|
|
379
|
+
hooks: [
|
|
380
|
+
{
|
|
381
|
+
type: "command",
|
|
382
|
+
command: "legio log session-end --agent orchestrator",
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: "command",
|
|
386
|
+
command: "mulch learn",
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
PreCompact: [
|
|
392
|
+
{
|
|
393
|
+
matcher: "",
|
|
394
|
+
hooks: [
|
|
395
|
+
{
|
|
396
|
+
type: "command",
|
|
397
|
+
command: "legio prime --agent orchestrator --compact",
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
return `${JSON.stringify(hooks, null, "\t")}\n`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Migrate existing SQLite databases on --force reinit.
|
|
410
|
+
*
|
|
411
|
+
* Opens each DB, enables WAL mode, and re-runs CREATE TABLE/INDEX IF NOT EXISTS
|
|
412
|
+
* to apply any schema additions without losing existing data.
|
|
413
|
+
*/
|
|
414
|
+
async function migrateExistingDatabases(legioPath: string): Promise<string[]> {
|
|
415
|
+
const migrated: string[] = [];
|
|
416
|
+
|
|
417
|
+
// Migrate mail.db
|
|
418
|
+
const mailDbPath = join(legioPath, "mail.db");
|
|
419
|
+
if (await fileExists(mailDbPath)) {
|
|
420
|
+
const db = new Database(mailDbPath);
|
|
421
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
422
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
423
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
424
|
+
db.exec(`
|
|
425
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
426
|
+
id TEXT PRIMARY KEY,
|
|
427
|
+
from_agent TEXT NOT NULL,
|
|
428
|
+
to_agent TEXT NOT NULL,
|
|
429
|
+
subject TEXT NOT NULL,
|
|
430
|
+
body TEXT NOT NULL,
|
|
431
|
+
type TEXT NOT NULL DEFAULT 'status',
|
|
432
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
433
|
+
thread_id TEXT,
|
|
434
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
435
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
436
|
+
)`);
|
|
437
|
+
db.exec(`
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_inbox ON messages(to_agent, read);
|
|
439
|
+
CREATE INDEX IF NOT EXISTS idx_thread ON messages(thread_id)`);
|
|
440
|
+
db.close();
|
|
441
|
+
migrated.push("mail.db");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Migrate metrics.db
|
|
445
|
+
const metricsDbPath = join(legioPath, "metrics.db");
|
|
446
|
+
if (await fileExists(metricsDbPath)) {
|
|
447
|
+
const db = new Database(metricsDbPath);
|
|
448
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
449
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
450
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
451
|
+
db.exec(`
|
|
452
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
453
|
+
agent_name TEXT NOT NULL,
|
|
454
|
+
bead_id TEXT NOT NULL,
|
|
455
|
+
capability TEXT NOT NULL,
|
|
456
|
+
started_at TEXT NOT NULL,
|
|
457
|
+
completed_at TEXT,
|
|
458
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
459
|
+
exit_code INTEGER,
|
|
460
|
+
merge_result TEXT,
|
|
461
|
+
parent_agent TEXT,
|
|
462
|
+
PRIMARY KEY (agent_name, bead_id)
|
|
463
|
+
)`);
|
|
464
|
+
db.close();
|
|
465
|
+
migrated.push("metrics.db");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return migrated;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Content for .legio/.gitignore — runtime state that should not be tracked.
|
|
473
|
+
* Uses wildcard+whitelist pattern: ignore everything, whitelist tracked files.
|
|
474
|
+
* Auto-healed by legio prime on each session start.
|
|
475
|
+
* Config files (config.yaml, agent-manifest.json, hooks.json) remain tracked.
|
|
476
|
+
*/
|
|
477
|
+
export const LEGIO_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
|
|
478
|
+
# Auto-healed by legio prime on each session start
|
|
479
|
+
*
|
|
480
|
+
!.gitignore
|
|
481
|
+
!config.yaml
|
|
482
|
+
!agent-manifest.json
|
|
483
|
+
!hooks.json
|
|
484
|
+
!groups.json
|
|
485
|
+
!agent-defs/
|
|
486
|
+
`;
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Write .legio/.gitignore for runtime state files.
|
|
490
|
+
* Always overwrites to support --force reinit and auto-healing via prime.
|
|
491
|
+
*/
|
|
492
|
+
export async function writeLegioGitignore(legioPath: string): Promise<void> {
|
|
493
|
+
const gitignorePath = join(legioPath, ".gitignore");
|
|
494
|
+
await writeFile(gitignorePath, LEGIO_GITIGNORE);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Print a success status line.
|
|
499
|
+
*/
|
|
500
|
+
function printCreated(relativePath: string): void {
|
|
501
|
+
process.stdout.write(` \u2713 Created ${relativePath}\n`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Entry point for `legio init [--force]`.
|
|
506
|
+
*
|
|
507
|
+
* Scaffolds the .legio/ directory structure in the current working directory.
|
|
508
|
+
*
|
|
509
|
+
* @param args - CLI arguments after "init" subcommand
|
|
510
|
+
*/
|
|
511
|
+
const INIT_HELP = `legio init — Initialize .legio/ in current project
|
|
512
|
+
|
|
513
|
+
Usage: legio init [--force]
|
|
514
|
+
|
|
515
|
+
Options:
|
|
516
|
+
--force Reinitialize even if .legio/ already exists
|
|
517
|
+
--help, -h Show this help`;
|
|
518
|
+
|
|
519
|
+
export async function initCommand(args: string[]): Promise<void> {
|
|
520
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
521
|
+
process.stdout.write(`${INIT_HELP}\n`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const force = args.includes("--force");
|
|
526
|
+
const projectRoot = process.cwd();
|
|
527
|
+
const legioPath = join(projectRoot, LEGIO_DIR);
|
|
528
|
+
|
|
529
|
+
// 0. Verify we're inside a git repository
|
|
530
|
+
const { exitCode: gitCheckExit } = await runCommand(
|
|
531
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
532
|
+
{ cwd: projectRoot },
|
|
533
|
+
);
|
|
534
|
+
if (gitCheckExit !== 0) {
|
|
535
|
+
throw new ValidationError("legio requires a git repository. Run 'git init' first.", {
|
|
536
|
+
field: "git",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 1. Check if .legio/ already exists
|
|
541
|
+
if (await fileExists(join(legioPath, "config.yaml"))) {
|
|
542
|
+
if (!force) {
|
|
543
|
+
process.stdout.write(
|
|
544
|
+
"Warning: .legio/ already initialized in this project.\n" +
|
|
545
|
+
"Use --force to reinitialize.\n",
|
|
546
|
+
);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
process.stdout.write("Reinitializing .legio/ (--force)\n\n");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 2. Detect project info
|
|
553
|
+
const projectName = await detectProjectName(projectRoot);
|
|
554
|
+
const canonicalBranch = await detectCanonicalBranch(projectRoot);
|
|
555
|
+
|
|
556
|
+
process.stdout.write(`Initializing legio for "${projectName}"...\n\n`);
|
|
557
|
+
|
|
558
|
+
// 3. Create directory structure
|
|
559
|
+
const dirs = [
|
|
560
|
+
LEGIO_DIR,
|
|
561
|
+
join(LEGIO_DIR, "agents"),
|
|
562
|
+
join(LEGIO_DIR, "agent-defs"),
|
|
563
|
+
join(LEGIO_DIR, "worktrees"),
|
|
564
|
+
join(LEGIO_DIR, "specs"),
|
|
565
|
+
join(LEGIO_DIR, "logs"),
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
for (const dir of dirs) {
|
|
569
|
+
await mkdir(join(projectRoot, dir), { recursive: true });
|
|
570
|
+
printCreated(`${dir}/`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 3b. Deploy agent definition .md files from legio install directory
|
|
574
|
+
const legioAgentsDir = join(import.meta.dirname, "..", "..", "agents");
|
|
575
|
+
const agentDefsTarget = join(legioPath, "agent-defs");
|
|
576
|
+
const agentDefFiles = await readdir(legioAgentsDir);
|
|
577
|
+
for (const fileName of agentDefFiles) {
|
|
578
|
+
if (!fileName.endsWith(".md")) continue;
|
|
579
|
+
const content = await readFile(join(legioAgentsDir, fileName), "utf-8");
|
|
580
|
+
await writeFile(join(agentDefsTarget, fileName), content);
|
|
581
|
+
printCreated(`${LEGIO_DIR}/agent-defs/${fileName}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 4. Write config.yaml
|
|
585
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
586
|
+
config.project.name = projectName;
|
|
587
|
+
config.project.root = projectRoot;
|
|
588
|
+
config.project.canonicalBranch = canonicalBranch;
|
|
589
|
+
|
|
590
|
+
const configYaml = serializeConfigToYaml(config);
|
|
591
|
+
const configPath = join(legioPath, "config.yaml");
|
|
592
|
+
await writeFile(configPath, configYaml);
|
|
593
|
+
printCreated(`${LEGIO_DIR}/config.yaml`);
|
|
594
|
+
|
|
595
|
+
// 5. Write agent-manifest.json
|
|
596
|
+
const manifest = buildAgentManifest();
|
|
597
|
+
const manifestPath = join(legioPath, "agent-manifest.json");
|
|
598
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`);
|
|
599
|
+
printCreated(`${LEGIO_DIR}/agent-manifest.json`);
|
|
600
|
+
|
|
601
|
+
// 6. Write hooks.json
|
|
602
|
+
const hooksContent = buildHooksJson();
|
|
603
|
+
const hooksPath = join(legioPath, "hooks.json");
|
|
604
|
+
await writeFile(hooksPath, hooksContent);
|
|
605
|
+
printCreated(`${LEGIO_DIR}/hooks.json`);
|
|
606
|
+
|
|
607
|
+
// 7. Write .legio/.gitignore for runtime state
|
|
608
|
+
await writeLegioGitignore(legioPath);
|
|
609
|
+
printCreated(`${LEGIO_DIR}/.gitignore`);
|
|
610
|
+
|
|
611
|
+
// 8. Migrate existing SQLite databases on --force reinit
|
|
612
|
+
if (force) {
|
|
613
|
+
const migrated = await migrateExistingDatabases(legioPath);
|
|
614
|
+
for (const dbName of migrated) {
|
|
615
|
+
process.stdout.write(` \u2713 Migrated ${LEGIO_DIR}/${dbName} (schema validated)\n`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
process.stdout.write("\nDone.\n");
|
|
620
|
+
process.stdout.write(" Next: run `legio hooks install` to enable Claude Code hooks.\n");
|
|
621
|
+
process.stdout.write(" Then: run `legio status` to see the current state.\n");
|
|
622
|
+
}
|