@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,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
+ });