@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,467 @@
|
|
|
1
|
+
import { access, mkdir, mkdtemp, readFile, rm, stat, 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 { openSessionStore } from "../sessions/compat.ts";
|
|
6
|
+
import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
|
|
7
|
+
import { primeCommand } from "./prime.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tests for `legio prime` command.
|
|
11
|
+
*
|
|
12
|
+
* Uses real filesystem (temp directories) and process.stdout spy to test
|
|
13
|
+
* the prime command end-to-end.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe("primeCommand", () => {
|
|
17
|
+
let chunks: string[];
|
|
18
|
+
let originalWrite: typeof process.stdout.write;
|
|
19
|
+
let originalStderrWrite: typeof process.stderr.write;
|
|
20
|
+
let stderrChunks: string[];
|
|
21
|
+
let tempDir: string;
|
|
22
|
+
let originalCwd: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
// Spy on stdout
|
|
26
|
+
chunks = [];
|
|
27
|
+
originalWrite = process.stdout.write;
|
|
28
|
+
process.stdout.write = ((chunk: string) => {
|
|
29
|
+
chunks.push(chunk);
|
|
30
|
+
return true;
|
|
31
|
+
}) as typeof process.stdout.write;
|
|
32
|
+
|
|
33
|
+
// Spy on stderr
|
|
34
|
+
stderrChunks = [];
|
|
35
|
+
originalStderrWrite = process.stderr.write;
|
|
36
|
+
process.stderr.write = ((chunk: string) => {
|
|
37
|
+
stderrChunks.push(chunk);
|
|
38
|
+
return true;
|
|
39
|
+
}) as typeof process.stderr.write;
|
|
40
|
+
|
|
41
|
+
// Create temp dir with .legio/config.yaml structure
|
|
42
|
+
tempDir = await mkdtemp(join(tmpdir(), "prime-test-"));
|
|
43
|
+
const legioDir = join(tempDir, ".legio");
|
|
44
|
+
await mkdir(legioDir, { recursive: true });
|
|
45
|
+
await writeFile(
|
|
46
|
+
join(legioDir, "config.yaml"),
|
|
47
|
+
`project:\n name: test-project\n root: ${tempDir}\n canonicalBranch: main\nmulch:\n enabled: false\n`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Change to temp dir so loadConfig() works
|
|
51
|
+
originalCwd = process.cwd();
|
|
52
|
+
process.chdir(tempDir);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
process.stdout.write = originalWrite;
|
|
57
|
+
process.stderr.write = originalStderrWrite;
|
|
58
|
+
process.chdir(originalCwd);
|
|
59
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function output(): string {
|
|
63
|
+
return chunks.join("");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stderr(): string {
|
|
67
|
+
return stderrChunks.join("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("Help", () => {
|
|
71
|
+
test("--help shows help text", async () => {
|
|
72
|
+
await primeCommand(["--help"]);
|
|
73
|
+
const out = output();
|
|
74
|
+
|
|
75
|
+
expect(out).toContain("legio prime");
|
|
76
|
+
expect(out).toContain("--agent");
|
|
77
|
+
expect(out).toContain("--compact");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("-h shows help text", async () => {
|
|
81
|
+
await primeCommand(["-h"]);
|
|
82
|
+
const out = output();
|
|
83
|
+
|
|
84
|
+
expect(out).toContain("legio prime");
|
|
85
|
+
expect(out).toContain("--agent");
|
|
86
|
+
expect(out).toContain("--compact");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("parseArgs validation", () => {
|
|
91
|
+
test("--agent without a name throws AgentError", async () => {
|
|
92
|
+
await expect(primeCommand(["--agent"])).rejects.toThrow("--agent requires a name argument");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("--agent followed by another flag throws AgentError", async () => {
|
|
96
|
+
await expect(primeCommand(["--agent", "--compact"])).rejects.toThrow(
|
|
97
|
+
"--agent requires a name argument",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Orchestrator priming (no --agent flag)", () => {
|
|
103
|
+
test("default prime outputs project context", async () => {
|
|
104
|
+
await primeCommand([]);
|
|
105
|
+
const out = output();
|
|
106
|
+
|
|
107
|
+
expect(out).toContain("# Legio Context");
|
|
108
|
+
expect(out).toContain("## Project: test-project");
|
|
109
|
+
expect(out).toContain("Canonical branch: main");
|
|
110
|
+
expect(out).toContain("Max concurrent agents:");
|
|
111
|
+
expect(out).toContain("Max depth:");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("includes agent manifest section", async () => {
|
|
115
|
+
await primeCommand([]);
|
|
116
|
+
const out = output();
|
|
117
|
+
|
|
118
|
+
expect(out).toContain("## Agent Manifest");
|
|
119
|
+
// Without manifest file, should show fallback message
|
|
120
|
+
expect(out).toContain("No agent manifest found.");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("without metrics.db shows no recent sessions message", async () => {
|
|
124
|
+
await primeCommand([]);
|
|
125
|
+
const out = output();
|
|
126
|
+
|
|
127
|
+
expect(out).toContain("## Recent Activity");
|
|
128
|
+
expect(out).toContain("No recent sessions.");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("--compact skips Recent Activity and Expertise sections", async () => {
|
|
132
|
+
await primeCommand(["--compact"]);
|
|
133
|
+
const out = output();
|
|
134
|
+
|
|
135
|
+
// Should still have project basics
|
|
136
|
+
expect(out).toContain("# Legio Context");
|
|
137
|
+
expect(out).toContain("## Project: test-project");
|
|
138
|
+
|
|
139
|
+
// Should NOT have these sections
|
|
140
|
+
expect(out).not.toContain("## Recent Activity");
|
|
141
|
+
expect(out).not.toContain("## Expertise");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("Agent priming (--agent <name>)", () => {
|
|
146
|
+
test("unknown agent outputs basic context and warns", async () => {
|
|
147
|
+
await primeCommand(["--agent", "unknown-agent"]);
|
|
148
|
+
const out = output();
|
|
149
|
+
const err = stderr();
|
|
150
|
+
|
|
151
|
+
expect(out).toContain("# Agent Context: unknown-agent");
|
|
152
|
+
expect(out).toContain("## Identity");
|
|
153
|
+
expect(out).toContain("New agent - no prior sessions");
|
|
154
|
+
expect(err).toContain('Warning: agent "unknown-agent" not found');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("agent with identity.yaml shows identity details", async () => {
|
|
158
|
+
// Write identity.yaml
|
|
159
|
+
const agentDir = join(tempDir, ".legio", "agents", "my-builder");
|
|
160
|
+
await mkdir(agentDir, { recursive: true });
|
|
161
|
+
await writeFile(
|
|
162
|
+
join(agentDir, "identity.yaml"),
|
|
163
|
+
`name: my-builder
|
|
164
|
+
capability: builder
|
|
165
|
+
created: "2026-01-01T00:00:00Z"
|
|
166
|
+
sessionsCompleted: 3
|
|
167
|
+
expertiseDomains:
|
|
168
|
+
- typescript
|
|
169
|
+
- testing
|
|
170
|
+
recentTasks:
|
|
171
|
+
- beadId: task-001
|
|
172
|
+
summary: "Implemented feature X"
|
|
173
|
+
completedAt: "2026-01-10T12:00:00Z"
|
|
174
|
+
`,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
await primeCommand(["--agent", "my-builder"]);
|
|
178
|
+
const out = output();
|
|
179
|
+
|
|
180
|
+
expect(out).toContain("# Agent Context: my-builder");
|
|
181
|
+
expect(out).toContain("Name: my-builder");
|
|
182
|
+
expect(out).toContain("Capability: builder");
|
|
183
|
+
expect(out).toContain("Sessions completed: 3");
|
|
184
|
+
expect(out).toContain("Expertise: typescript, testing");
|
|
185
|
+
expect(out).toContain("Recent tasks:");
|
|
186
|
+
expect(out).toContain("task-001: Implemented feature X");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("agent with active session shows Activation section", async () => {
|
|
190
|
+
// Seed sessions.db with active session via SessionStore
|
|
191
|
+
const { store } = openSessionStore(join(tempDir, ".legio"));
|
|
192
|
+
store.upsert({
|
|
193
|
+
id: "session-001",
|
|
194
|
+
agentName: "active-builder",
|
|
195
|
+
capability: "builder",
|
|
196
|
+
worktreePath: join(tempDir, ".legio", "worktrees", "active-builder"),
|
|
197
|
+
branchName: "legio/active-builder/task-001",
|
|
198
|
+
beadId: "task-001",
|
|
199
|
+
tmuxSession: "legio-active-builder",
|
|
200
|
+
state: "working",
|
|
201
|
+
pid: 12345,
|
|
202
|
+
parentAgent: null,
|
|
203
|
+
depth: 0,
|
|
204
|
+
runId: null,
|
|
205
|
+
startedAt: new Date().toISOString(),
|
|
206
|
+
lastActivity: new Date().toISOString(),
|
|
207
|
+
escalationLevel: 0,
|
|
208
|
+
stalledSince: null,
|
|
209
|
+
});
|
|
210
|
+
store.close();
|
|
211
|
+
|
|
212
|
+
await primeCommand(["--agent", "active-builder"]);
|
|
213
|
+
const out = output();
|
|
214
|
+
|
|
215
|
+
expect(out).toContain("# Agent Context: active-builder");
|
|
216
|
+
expect(out).toContain("## Activation");
|
|
217
|
+
expect(out).toContain("You have a bound task: **task-001**");
|
|
218
|
+
expect(out).toContain("begin working immediately");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("agent with completed session does NOT show Activation", async () => {
|
|
222
|
+
// Seed sessions.db with completed session via SessionStore
|
|
223
|
+
const { store } = openSessionStore(join(tempDir, ".legio"));
|
|
224
|
+
store.upsert({
|
|
225
|
+
id: "session-002",
|
|
226
|
+
agentName: "completed-builder",
|
|
227
|
+
capability: "builder",
|
|
228
|
+
worktreePath: join(tempDir, ".legio", "worktrees", "completed-builder"),
|
|
229
|
+
branchName: "legio/completed-builder/task-002",
|
|
230
|
+
beadId: "task-002",
|
|
231
|
+
tmuxSession: "legio-completed-builder",
|
|
232
|
+
state: "completed",
|
|
233
|
+
pid: null,
|
|
234
|
+
parentAgent: null,
|
|
235
|
+
depth: 0,
|
|
236
|
+
runId: null,
|
|
237
|
+
startedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
238
|
+
lastActivity: new Date().toISOString(),
|
|
239
|
+
escalationLevel: 0,
|
|
240
|
+
stalledSince: null,
|
|
241
|
+
});
|
|
242
|
+
store.close();
|
|
243
|
+
|
|
244
|
+
await primeCommand(["--agent", "completed-builder"]);
|
|
245
|
+
const out = output();
|
|
246
|
+
|
|
247
|
+
expect(out).toContain("# Agent Context: completed-builder");
|
|
248
|
+
expect(out).not.toContain("## Activation");
|
|
249
|
+
expect(out).not.toContain("bound task");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("--compact with checkpoint.json shows Session Recovery", async () => {
|
|
253
|
+
// Write checkpoint.json
|
|
254
|
+
const agentDir = join(tempDir, ".legio", "agents", "recovery-agent");
|
|
255
|
+
await mkdir(agentDir, { recursive: true });
|
|
256
|
+
await writeFile(
|
|
257
|
+
join(agentDir, "checkpoint.json"),
|
|
258
|
+
`${JSON.stringify(
|
|
259
|
+
{
|
|
260
|
+
agentName: "recovery-agent",
|
|
261
|
+
beadId: "task-003",
|
|
262
|
+
sessionId: "session-003",
|
|
263
|
+
timestamp: new Date().toISOString(),
|
|
264
|
+
progressSummary: "Implemented initial tests for prime command",
|
|
265
|
+
filesModified: ["src/commands/prime.test.ts"],
|
|
266
|
+
currentBranch: "legio/recovery-agent/task-003",
|
|
267
|
+
pendingWork: "Add tests for edge cases",
|
|
268
|
+
mulchDomains: ["typescript", "testing"],
|
|
269
|
+
},
|
|
270
|
+
null,
|
|
271
|
+
2,
|
|
272
|
+
)}\n`,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Also need identity to avoid warning
|
|
276
|
+
await writeFile(
|
|
277
|
+
join(agentDir, "identity.yaml"),
|
|
278
|
+
`name: recovery-agent
|
|
279
|
+
capability: builder
|
|
280
|
+
created: "2026-01-01T00:00:00Z"
|
|
281
|
+
sessionsCompleted: 0
|
|
282
|
+
expertiseDomains: []
|
|
283
|
+
recentTasks: []
|
|
284
|
+
`,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await primeCommand(["--agent", "recovery-agent", "--compact"]);
|
|
288
|
+
const out = output();
|
|
289
|
+
|
|
290
|
+
expect(out).toContain("# Agent Context: recovery-agent");
|
|
291
|
+
expect(out).toContain("## Session Recovery");
|
|
292
|
+
expect(out).toContain("Progress so far:** Implemented initial tests for prime command");
|
|
293
|
+
expect(out).toContain("Files modified:** src/commands/prime.test.ts");
|
|
294
|
+
expect(out).toContain("Pending work:** Add tests for edge cases");
|
|
295
|
+
expect(out).toContain("Branch:** legio/recovery-agent/task-003");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("--compact skips Expertise section", async () => {
|
|
299
|
+
// Write identity with expertise
|
|
300
|
+
const agentDir = join(tempDir, ".legio", "agents", "compact-agent");
|
|
301
|
+
await mkdir(agentDir, { recursive: true });
|
|
302
|
+
await writeFile(
|
|
303
|
+
join(agentDir, "identity.yaml"),
|
|
304
|
+
`name: compact-agent
|
|
305
|
+
capability: builder
|
|
306
|
+
created: "2026-01-01T00:00:00Z"
|
|
307
|
+
sessionsCompleted: 1
|
|
308
|
+
expertiseDomains:
|
|
309
|
+
- typescript
|
|
310
|
+
recentTasks: []
|
|
311
|
+
`,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await primeCommand(["--agent", "compact-agent", "--compact"]);
|
|
315
|
+
const out = output();
|
|
316
|
+
|
|
317
|
+
expect(out).toContain("# Agent Context: compact-agent");
|
|
318
|
+
expect(out).not.toContain("## Expertise");
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("Session branch capture", () => {
|
|
323
|
+
test("orchestrator prime writes session-branch.txt with current git branch", async () => {
|
|
324
|
+
// Need a real git repo for branch detection
|
|
325
|
+
const gitRepoDir = await createTempGitRepo();
|
|
326
|
+
try {
|
|
327
|
+
const legioDir = join(gitRepoDir, ".legio");
|
|
328
|
+
await mkdir(legioDir, { recursive: true });
|
|
329
|
+
await writeFile(
|
|
330
|
+
join(legioDir, "config.yaml"),
|
|
331
|
+
`project:\n name: branch-test\n root: ${gitRepoDir}\n canonicalBranch: main\nmulch:\n enabled: false\n`,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Save and change cwd to the git repo
|
|
335
|
+
process.chdir(gitRepoDir);
|
|
336
|
+
|
|
337
|
+
await primeCommand([]);
|
|
338
|
+
const out = output();
|
|
339
|
+
|
|
340
|
+
expect(out).toContain("# Legio Context");
|
|
341
|
+
|
|
342
|
+
// Verify session-branch.txt was written
|
|
343
|
+
const sessionBranchPath = join(legioDir, "session-branch.txt");
|
|
344
|
+
const content = await readFile(sessionBranchPath, "utf-8");
|
|
345
|
+
expect(content.trim()).toBe("main");
|
|
346
|
+
} finally {
|
|
347
|
+
process.chdir(originalCwd);
|
|
348
|
+
await cleanupTempDir(gitRepoDir);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("shows session branch in context when different from canonical", async () => {
|
|
353
|
+
const gitRepoDir = await createTempGitRepo();
|
|
354
|
+
try {
|
|
355
|
+
// Create and switch to a feature branch
|
|
356
|
+
// Use spawnSync to checkout the feature branch
|
|
357
|
+
const { spawnSync } = await import("node:child_process");
|
|
358
|
+
spawnSync("git", ["checkout", "-b", "feature/my-work"], { cwd: gitRepoDir });
|
|
359
|
+
|
|
360
|
+
const legioDir = join(gitRepoDir, ".legio");
|
|
361
|
+
await mkdir(legioDir, { recursive: true });
|
|
362
|
+
await writeFile(
|
|
363
|
+
join(legioDir, "config.yaml"),
|
|
364
|
+
`project:\n name: branch-test\n root: ${gitRepoDir}\n canonicalBranch: main\nmulch:\n enabled: false\n`,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
process.chdir(gitRepoDir);
|
|
368
|
+
|
|
369
|
+
await primeCommand([]);
|
|
370
|
+
const out = output();
|
|
371
|
+
|
|
372
|
+
expect(out).toContain("Session branch: feature/my-work (merge target)");
|
|
373
|
+
|
|
374
|
+
// Verify session-branch.txt was written with the feature branch
|
|
375
|
+
const sessionBranchPath = join(legioDir, "session-branch.txt");
|
|
376
|
+
const content = await readFile(sessionBranchPath, "utf-8");
|
|
377
|
+
expect(content.trim()).toBe("feature/my-work");
|
|
378
|
+
} finally {
|
|
379
|
+
process.chdir(originalCwd);
|
|
380
|
+
await cleanupTempDir(gitRepoDir);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe("Gitignore auto-heal", () => {
|
|
386
|
+
const expectedGitignore = `# Wildcard+whitelist: ignore everything, whitelist tracked files
|
|
387
|
+
# Auto-healed by legio prime on each session start
|
|
388
|
+
*
|
|
389
|
+
!.gitignore
|
|
390
|
+
!config.yaml
|
|
391
|
+
!agent-manifest.json
|
|
392
|
+
!hooks.json
|
|
393
|
+
!groups.json
|
|
394
|
+
!agent-defs/
|
|
395
|
+
`;
|
|
396
|
+
|
|
397
|
+
test("creates .legio/.gitignore if missing", async () => {
|
|
398
|
+
// The beforeEach creates .legio/config.yaml but not .gitignore
|
|
399
|
+
const gitignorePath = join(tempDir, ".legio", ".gitignore");
|
|
400
|
+
|
|
401
|
+
// Verify it doesn't exist
|
|
402
|
+
let existsBefore = false;
|
|
403
|
+
try {
|
|
404
|
+
await access(gitignorePath);
|
|
405
|
+
existsBefore = true;
|
|
406
|
+
} catch {
|
|
407
|
+
existsBefore = false;
|
|
408
|
+
}
|
|
409
|
+
expect(existsBefore).toBe(false);
|
|
410
|
+
|
|
411
|
+
// Run primeCommand
|
|
412
|
+
await primeCommand([]);
|
|
413
|
+
|
|
414
|
+
// Verify .gitignore was created with correct content
|
|
415
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
416
|
+
expect(content).toBe(expectedGitignore);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("overwrites stale .legio/.gitignore with current template", async () => {
|
|
420
|
+
// Write an old-style deny-list gitignore
|
|
421
|
+
const gitignorePath = join(tempDir, ".legio", ".gitignore");
|
|
422
|
+
const staleContent = `# Old deny-list format
|
|
423
|
+
worktrees/
|
|
424
|
+
logs/
|
|
425
|
+
mail.db
|
|
426
|
+
sessions.db
|
|
427
|
+
`;
|
|
428
|
+
await writeFile(gitignorePath, staleContent);
|
|
429
|
+
|
|
430
|
+
// Verify stale content is present
|
|
431
|
+
const contentBefore = await readFile(gitignorePath, "utf-8");
|
|
432
|
+
expect(contentBefore).toBe(staleContent);
|
|
433
|
+
|
|
434
|
+
// Run primeCommand
|
|
435
|
+
await primeCommand([]);
|
|
436
|
+
|
|
437
|
+
// Verify .gitignore now has the wildcard+whitelist content
|
|
438
|
+
const contentAfter = await readFile(gitignorePath, "utf-8");
|
|
439
|
+
expect(contentAfter).toBe(expectedGitignore);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("does not overwrite .legio/.gitignore if already correct", async () => {
|
|
443
|
+
// Write the correct LEGIO_GITIGNORE content
|
|
444
|
+
const gitignorePath = join(tempDir, ".legio", ".gitignore");
|
|
445
|
+
await writeFile(gitignorePath, expectedGitignore);
|
|
446
|
+
|
|
447
|
+
// Get file stat before
|
|
448
|
+
const statBefore = await stat(gitignorePath);
|
|
449
|
+
const mtimeBefore = statBefore.mtime;
|
|
450
|
+
|
|
451
|
+
// Wait a tiny bit to ensure mtime would change if file is rewritten
|
|
452
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
453
|
+
|
|
454
|
+
// Run primeCommand
|
|
455
|
+
await primeCommand([]);
|
|
456
|
+
|
|
457
|
+
// Verify content is still correct
|
|
458
|
+
const contentAfter = await readFile(gitignorePath, "utf-8");
|
|
459
|
+
expect(contentAfter).toBe(expectedGitignore);
|
|
460
|
+
|
|
461
|
+
// Verify mtime is unchanged (file was not rewritten)
|
|
462
|
+
const statAfter = await stat(gitignorePath);
|
|
463
|
+
const mtimeAfter = statAfter.mtime;
|
|
464
|
+
expect(mtimeAfter).toEqual(mtimeBefore);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
});
|