@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,676 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
4
+ import { ValidationError } from "../errors.ts";
5
+ import { createMergeQueue } from "../merge/queue.ts";
6
+ import {
7
+ cleanupTempDir,
8
+ cloneFixtureRepo,
9
+ commitFile,
10
+ getDefaultBranch,
11
+ runGitInDir,
12
+ } from "../test-helpers.ts";
13
+ import { mergeCommand } from "./merge.ts";
14
+
15
+ describe("mergeCommand", () => {
16
+ let repoDir: string;
17
+ let defaultBranch: string;
18
+ let originalCwd: string;
19
+
20
+ beforeEach(async () => {
21
+ originalCwd = process.cwd();
22
+ repoDir = await cloneFixtureRepo();
23
+ defaultBranch = await getDefaultBranch(repoDir);
24
+ process.chdir(repoDir);
25
+ });
26
+
27
+ afterEach(async () => {
28
+ process.chdir(originalCwd);
29
+ await cleanupTempDir(repoDir);
30
+ });
31
+
32
+ /**
33
+ * Setup helper: Create .legio/ dir and write config.yaml with project canonicalBranch.
34
+ */
35
+ async function setupProject(dir: string, canonicalBranch: string): Promise<void> {
36
+ const legioDir = join(dir, ".legio");
37
+ await mkdir(legioDir);
38
+
39
+ const configYaml = `project:
40
+ canonicalBranch: ${canonicalBranch}
41
+ root: ${dir}
42
+
43
+ merge:
44
+ aiResolveEnabled: false
45
+ reimagineEnabled: false
46
+ `;
47
+ await writeFile(join(legioDir, "config.yaml"), configYaml);
48
+ }
49
+
50
+ /**
51
+ * Setup helper: Create a clean feature branch with a committed file.
52
+ * Commits a base file (if not exists), creates a new branch, commits a feature file, then switches back to defaultBranch.
53
+ */
54
+ async function createCleanFeatureBranch(dir: string, branchName: string): Promise<void> {
55
+ // Only commit base file if it doesn't exist
56
+ const baseFilePath = join(dir, "src/base.ts");
57
+ const baseFileExists = await access(baseFilePath).then(
58
+ () => true,
59
+ () => false,
60
+ );
61
+ if (!baseFileExists) {
62
+ await commitFile(dir, "src/base.ts", "base content");
63
+ }
64
+ await runGitInDir(dir, ["checkout", "-b", branchName]);
65
+ await commitFile(dir, `src/${branchName}.ts`, "feature content");
66
+ await runGitInDir(dir, ["checkout", defaultBranch]);
67
+ }
68
+
69
+ describe("help and validation", () => {
70
+ test("--help prints help containing 'legio merge', '--branch', '--all', '--dry-run', '--into'", async () => {
71
+ let output = "";
72
+ const originalWrite = process.stdout.write.bind(process.stdout);
73
+ process.stdout.write = (chunk: unknown): boolean => {
74
+ output += String(chunk);
75
+ return true;
76
+ };
77
+
78
+ try {
79
+ await mergeCommand(["--help"]);
80
+ } finally {
81
+ process.stdout.write = originalWrite;
82
+ }
83
+
84
+ expect(output).toContain("legio merge");
85
+ expect(output).toContain("--branch");
86
+ expect(output).toContain("--all");
87
+ expect(output).toContain("--dry-run");
88
+ expect(output).toContain("--into");
89
+ });
90
+
91
+ test("-h prints help", async () => {
92
+ let output = "";
93
+ const originalWrite = process.stdout.write.bind(process.stdout);
94
+ process.stdout.write = (chunk: unknown): boolean => {
95
+ output += String(chunk);
96
+ return true;
97
+ };
98
+
99
+ try {
100
+ await mergeCommand(["-h"]);
101
+ } finally {
102
+ process.stdout.write = originalWrite;
103
+ }
104
+
105
+ expect(output).toContain("legio merge");
106
+ });
107
+
108
+ test("no flags throws ValidationError mentioning '--branch' and '--all'", async () => {
109
+ await setupProject(repoDir, defaultBranch);
110
+
111
+ try {
112
+ await mergeCommand([]);
113
+ expect(true).toBe(false); // Should not reach here
114
+ } catch (err: unknown) {
115
+ expect(err).toBeInstanceOf(ValidationError);
116
+ const validationErr = err as ValidationError;
117
+ expect(validationErr.message).toContain("--branch");
118
+ expect(validationErr.message).toContain("--all");
119
+ }
120
+ });
121
+ });
122
+
123
+ describe("--branch with real git repo", () => {
124
+ test("nonexistent branch throws ValidationError", async () => {
125
+ await setupProject(repoDir, defaultBranch);
126
+
127
+ try {
128
+ await mergeCommand(["--branch", "nonexistent-branch"]);
129
+ expect(true).toBe(false); // Should not reach here
130
+ } catch (err: unknown) {
131
+ expect(err).toBeInstanceOf(ValidationError);
132
+ const validationErr = err as ValidationError;
133
+ expect(validationErr.message).toContain("nonexistent-branch");
134
+ }
135
+ });
136
+
137
+ test("--dry-run shows branch info without merging (verify still on defaultBranch after)", async () => {
138
+ await setupProject(repoDir, defaultBranch);
139
+ const branchName = "legio/test-agent/bead-123";
140
+ await createCleanFeatureBranch(repoDir, branchName);
141
+
142
+ let output = "";
143
+ const originalWrite = process.stdout.write.bind(process.stdout);
144
+ process.stdout.write = (chunk: unknown): boolean => {
145
+ output += String(chunk);
146
+ return true;
147
+ };
148
+
149
+ try {
150
+ await mergeCommand(["--branch", branchName, "--dry-run"]);
151
+ } finally {
152
+ process.stdout.write = originalWrite;
153
+ }
154
+
155
+ expect(output).toContain(branchName);
156
+ expect(output).toContain("pending");
157
+
158
+ // Verify still on defaultBranch
159
+ const currentBranch = await getDefaultBranch(repoDir);
160
+ expect(currentBranch).toBe(defaultBranch);
161
+ });
162
+
163
+ test("--dry-run --json outputs JSON with branchName and status:pending", async () => {
164
+ await setupProject(repoDir, defaultBranch);
165
+ const branchName = "legio/test-agent/bead-456";
166
+ await createCleanFeatureBranch(repoDir, branchName);
167
+
168
+ let output = "";
169
+ const originalWrite = process.stdout.write.bind(process.stdout);
170
+ process.stdout.write = (chunk: unknown): boolean => {
171
+ output += String(chunk);
172
+ return true;
173
+ };
174
+
175
+ try {
176
+ await mergeCommand(["--branch", branchName, "--dry-run", "--json"]);
177
+ } finally {
178
+ process.stdout.write = originalWrite;
179
+ }
180
+
181
+ const parsed = JSON.parse(output);
182
+ expect(parsed.branchName).toBe(branchName);
183
+ expect(parsed.status).toBe("pending");
184
+ });
185
+
186
+ test("merges a clean branch successfully (verify feature file exists after)", async () => {
187
+ await setupProject(repoDir, defaultBranch);
188
+ const branchName = "legio/builder/bead-789";
189
+ await createCleanFeatureBranch(repoDir, branchName);
190
+
191
+ const originalWrite = process.stdout.write.bind(process.stdout);
192
+ process.stdout.write = (): boolean => {
193
+ return true;
194
+ };
195
+
196
+ try {
197
+ await mergeCommand(["--branch", branchName]);
198
+ } finally {
199
+ process.stdout.write = originalWrite;
200
+ }
201
+
202
+ // Verify feature file exists after merge
203
+ const featureFilePath = join(repoDir, `src/${branchName}.ts`);
204
+ const featureFile = await readFile(featureFilePath, "utf-8");
205
+ expect(featureFile).toBe("feature content");
206
+ });
207
+
208
+ test("--json outputs JSON with success:true and tier:clean-merge", async () => {
209
+ await setupProject(repoDir, defaultBranch);
210
+ const branchName = "legio/builder/bead-abc";
211
+ await createCleanFeatureBranch(repoDir, branchName);
212
+
213
+ let output = "";
214
+ const originalWrite = process.stdout.write.bind(process.stdout);
215
+ process.stdout.write = (chunk: unknown): boolean => {
216
+ output += String(chunk);
217
+ return true;
218
+ };
219
+
220
+ try {
221
+ await mergeCommand(["--branch", branchName, "--json"]);
222
+ } finally {
223
+ process.stdout.write = originalWrite;
224
+ }
225
+
226
+ const parsed = JSON.parse(output);
227
+ expect(parsed.success).toBe(true);
228
+ expect(parsed.tier).toBe("clean-merge");
229
+ });
230
+
231
+ test("parses agent name from legio/my-builder/bead-abc convention (use --dry-run)", async () => {
232
+ await setupProject(repoDir, defaultBranch);
233
+ const branchName = "legio/my-builder/bead-xyz";
234
+ await createCleanFeatureBranch(repoDir, branchName);
235
+
236
+ let output = "";
237
+ const originalWrite = process.stdout.write.bind(process.stdout);
238
+ process.stdout.write = (chunk: unknown): boolean => {
239
+ output += String(chunk);
240
+ return true;
241
+ };
242
+
243
+ try {
244
+ await mergeCommand(["--branch", branchName, "--dry-run", "--json"]);
245
+ } finally {
246
+ process.stdout.write = originalWrite;
247
+ }
248
+
249
+ const parsed = JSON.parse(output);
250
+ expect(parsed.agentName).toBe("my-builder");
251
+ expect(parsed.beadId).toBe("bead-xyz");
252
+ });
253
+ });
254
+
255
+ describe("--all with real git repo", () => {
256
+ test("prints 'No pending' when queue empty", async () => {
257
+ await setupProject(repoDir, defaultBranch);
258
+
259
+ let output = "";
260
+ const originalWrite = process.stdout.write.bind(process.stdout);
261
+ process.stdout.write = (chunk: unknown): boolean => {
262
+ output += String(chunk);
263
+ return true;
264
+ };
265
+
266
+ try {
267
+ await mergeCommand(["--all"]);
268
+ } finally {
269
+ process.stdout.write = originalWrite;
270
+ }
271
+
272
+ expect(output).toContain("No pending");
273
+ });
274
+
275
+ test("--json shows empty results", async () => {
276
+ await setupProject(repoDir, defaultBranch);
277
+
278
+ let output = "";
279
+ const originalWrite = process.stdout.write.bind(process.stdout);
280
+ process.stdout.write = (chunk: unknown): boolean => {
281
+ output += String(chunk);
282
+ return true;
283
+ };
284
+
285
+ try {
286
+ await mergeCommand(["--all", "--json"]);
287
+ } finally {
288
+ process.stdout.write = originalWrite;
289
+ }
290
+
291
+ const parsed = JSON.parse(output);
292
+ expect(parsed.results).toEqual([]);
293
+ expect(parsed.count).toBe(0);
294
+ });
295
+
296
+ test("--all --dry-run lists pending entries from merge-queue.json", async () => {
297
+ await setupProject(repoDir, defaultBranch);
298
+ const branch1 = "legio/agent1/bead-001";
299
+ const branch2 = "legio/agent2/bead-002";
300
+ await createCleanFeatureBranch(repoDir, branch1);
301
+ await createCleanFeatureBranch(repoDir, branch2);
302
+
303
+ // Enqueue entries via createMergeQueue
304
+ const queuePath = join(repoDir, ".legio", "merge-queue.db");
305
+ const queue = createMergeQueue(queuePath);
306
+ queue.enqueue({
307
+ branchName: branch1,
308
+ beadId: "bead-001",
309
+ agentName: "agent1",
310
+ filesModified: [`src/${branch1}.ts`],
311
+ });
312
+ queue.enqueue({
313
+ branchName: branch2,
314
+ beadId: "bead-002",
315
+ agentName: "agent2",
316
+ filesModified: [`src/${branch2}.ts`],
317
+ });
318
+ queue.close();
319
+
320
+ let output = "";
321
+ const originalWrite = process.stdout.write.bind(process.stdout);
322
+ process.stdout.write = (chunk: unknown): boolean => {
323
+ output += String(chunk);
324
+ return true;
325
+ };
326
+
327
+ try {
328
+ await mergeCommand(["--all", "--dry-run"]);
329
+ } finally {
330
+ process.stdout.write = originalWrite;
331
+ }
332
+
333
+ expect(output).toContain("2 pending");
334
+ expect(output).toContain(branch1);
335
+ expect(output).toContain(branch2);
336
+ });
337
+
338
+ test("--all merges multiple pending entries (write merge-queue.json with entries, verify counts)", async () => {
339
+ await setupProject(repoDir, defaultBranch);
340
+ const branch1 = "legio/builder1/bead-100";
341
+ const branch2 = "legio/builder2/bead-200";
342
+ await createCleanFeatureBranch(repoDir, branch1);
343
+ await createCleanFeatureBranch(repoDir, branch2);
344
+
345
+ // Enqueue entries via createMergeQueue
346
+ const queuePath = join(repoDir, ".legio", "merge-queue.db");
347
+ const queue = createMergeQueue(queuePath);
348
+ queue.enqueue({
349
+ branchName: branch1,
350
+ beadId: "bead-100",
351
+ agentName: "builder1",
352
+ filesModified: [`src/${branch1}.ts`],
353
+ });
354
+ queue.enqueue({
355
+ branchName: branch2,
356
+ beadId: "bead-200",
357
+ agentName: "builder2",
358
+ filesModified: [`src/${branch2}.ts`],
359
+ });
360
+ queue.close();
361
+
362
+ let output = "";
363
+ const originalWrite = process.stdout.write.bind(process.stdout);
364
+ process.stdout.write = (chunk: unknown): boolean => {
365
+ output += String(chunk);
366
+ return true;
367
+ };
368
+
369
+ try {
370
+ await mergeCommand(["--all"]);
371
+ } finally {
372
+ process.stdout.write = originalWrite;
373
+ }
374
+
375
+ expect(output).toContain("Done");
376
+ expect(output).toContain("2 merged");
377
+
378
+ // Verify both feature files exist after merge
379
+ const file1 = await readFile(join(repoDir, `src/${branch1}.ts`), "utf-8");
380
+ const file2 = await readFile(join(repoDir, `src/${branch2}.ts`), "utf-8");
381
+ expect(file1).toBe("feature content");
382
+ expect(file2).toBe("feature content");
383
+ });
384
+
385
+ test("--all --json reports successCount and failCount", async () => {
386
+ await setupProject(repoDir, defaultBranch);
387
+ const branch1 = "legio/builder3/bead-300";
388
+ await createCleanFeatureBranch(repoDir, branch1);
389
+
390
+ // Enqueue entry via createMergeQueue
391
+ const queuePath = join(repoDir, ".legio", "merge-queue.db");
392
+ const queue = createMergeQueue(queuePath);
393
+ queue.enqueue({
394
+ branchName: branch1,
395
+ beadId: "bead-300",
396
+ agentName: "builder3",
397
+ filesModified: [`src/${branch1}.ts`],
398
+ });
399
+ queue.close();
400
+
401
+ let output = "";
402
+ const originalWrite = process.stdout.write.bind(process.stdout);
403
+ process.stdout.write = (chunk: unknown): boolean => {
404
+ output += String(chunk);
405
+ return true;
406
+ };
407
+
408
+ try {
409
+ await mergeCommand(["--all", "--json"]);
410
+ } finally {
411
+ process.stdout.write = originalWrite;
412
+ }
413
+
414
+ const parsed = JSON.parse(output);
415
+ expect(parsed.successCount).toBe(1);
416
+ expect(parsed.failCount).toBe(0);
417
+ expect(parsed.count).toBe(1);
418
+ });
419
+ });
420
+
421
+ describe("--into flag", { timeout: 15_000 }, () => {
422
+ test("merges into a non-default target branch", async () => {
423
+ await setupProject(repoDir, defaultBranch);
424
+
425
+ // Create a target branch (not the default/canonical branch)
426
+ await commitFile(repoDir, "src/base.ts", "base content");
427
+ await runGitInDir(repoDir, ["checkout", "-b", "develop"]);
428
+ await commitFile(repoDir, "src/develop-marker.ts", "develop marker");
429
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
430
+
431
+ // Create a feature branch off defaultBranch
432
+ const branchName = "legio/builder/bead-into-test";
433
+ await runGitInDir(repoDir, ["checkout", "-b", branchName]);
434
+ await commitFile(repoDir, `src/${branchName}.ts`, "feature for develop");
435
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
436
+
437
+ let output = "";
438
+ const originalWrite = process.stdout.write.bind(process.stdout);
439
+ process.stdout.write = (chunk: unknown): boolean => {
440
+ output += String(chunk);
441
+ return true;
442
+ };
443
+
444
+ try {
445
+ await mergeCommand(["--branch", branchName, "--into", "develop", "--json"]);
446
+ } finally {
447
+ process.stdout.write = originalWrite;
448
+ }
449
+
450
+ const parsed = JSON.parse(output);
451
+ expect(parsed.success).toBe(true);
452
+ expect(parsed.tier).toBe("clean-merge");
453
+
454
+ // Verify we ended up on the develop branch after merge
455
+ const currentBranch = await runGitInDir(repoDir, ["symbolic-ref", "--short", "HEAD"]);
456
+ expect(currentBranch.trim()).toBe("develop");
457
+
458
+ // Verify feature file exists on develop
459
+ const featureFile = await readFile(join(repoDir, `src/${branchName}.ts`), "utf-8");
460
+ expect(featureFile).toBe("feature for develop");
461
+
462
+ // Verify defaultBranch was NOT modified (switch back and check)
463
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
464
+ const featureOnDefault = await access(join(repoDir, `src/${branchName}.ts`)).then(
465
+ () => true,
466
+ () => false,
467
+ );
468
+ expect(featureOnDefault).toBe(false);
469
+ });
470
+
471
+ test("--into with --all merges all pending into target branch", async () => {
472
+ await setupProject(repoDir, defaultBranch);
473
+
474
+ // Create a target branch
475
+ await commitFile(repoDir, "src/base.ts", "base content");
476
+ await runGitInDir(repoDir, ["checkout", "-b", "staging"]);
477
+ await commitFile(repoDir, "src/staging-marker.ts", "staging marker");
478
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
479
+
480
+ // Create feature branches
481
+ const branch1 = "legio/agent1/bead-into-all-1";
482
+ await runGitInDir(repoDir, ["checkout", "-b", branch1]);
483
+ await commitFile(repoDir, `src/${branch1}.ts`, "feature 1");
484
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
485
+
486
+ const branch2 = "legio/agent2/bead-into-all-2";
487
+ await runGitInDir(repoDir, ["checkout", "-b", branch2]);
488
+ await commitFile(repoDir, `src/${branch2}.ts`, "feature 2");
489
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
490
+
491
+ // Enqueue entries
492
+ const queuePath = join(repoDir, ".legio", "merge-queue.db");
493
+ const queue = createMergeQueue(queuePath);
494
+ queue.enqueue({
495
+ branchName: branch1,
496
+ beadId: "bead-into-all-1",
497
+ agentName: "agent1",
498
+ filesModified: [`src/${branch1}.ts`],
499
+ });
500
+ queue.enqueue({
501
+ branchName: branch2,
502
+ beadId: "bead-into-all-2",
503
+ agentName: "agent2",
504
+ filesModified: [`src/${branch2}.ts`],
505
+ });
506
+ queue.close();
507
+
508
+ let output = "";
509
+ const originalWrite = process.stdout.write.bind(process.stdout);
510
+ process.stdout.write = (chunk: unknown): boolean => {
511
+ output += String(chunk);
512
+ return true;
513
+ };
514
+
515
+ try {
516
+ await mergeCommand(["--all", "--into", "staging", "--json"]);
517
+ } finally {
518
+ process.stdout.write = originalWrite;
519
+ }
520
+
521
+ const parsed = JSON.parse(output);
522
+ expect(parsed.successCount).toBe(2);
523
+ expect(parsed.failCount).toBe(0);
524
+
525
+ // Verify we're on staging, not defaultBranch
526
+ const currentBranch = await runGitInDir(repoDir, ["symbolic-ref", "--short", "HEAD"]);
527
+ expect(currentBranch.trim()).toBe("staging");
528
+ });
529
+
530
+ test("defaults to canonicalBranch when --into and session-branch.txt are absent", async () => {
531
+ await setupProject(repoDir, defaultBranch);
532
+ const branchName = "legio/builder/bead-default-target";
533
+ await createCleanFeatureBranch(repoDir, branchName);
534
+
535
+ let output = "";
536
+ const originalWrite = process.stdout.write.bind(process.stdout);
537
+ process.stdout.write = (chunk: unknown): boolean => {
538
+ output += String(chunk);
539
+ return true;
540
+ };
541
+
542
+ try {
543
+ await mergeCommand(["--branch", branchName, "--json"]);
544
+ } finally {
545
+ process.stdout.write = originalWrite;
546
+ }
547
+
548
+ const parsed = JSON.parse(output);
549
+ expect(parsed.success).toBe(true);
550
+
551
+ // Verify we ended up on the default branch (the canonical branch)
552
+ const currentBranch = await runGitInDir(repoDir, ["symbolic-ref", "--short", "HEAD"]);
553
+ expect(currentBranch.trim()).toBe(defaultBranch);
554
+ });
555
+
556
+ test("reads session-branch.txt as default when --into is not specified", async () => {
557
+ await setupProject(repoDir, defaultBranch);
558
+
559
+ // Create a target branch
560
+ await commitFile(repoDir, "src/base.ts", "base content");
561
+ await runGitInDir(repoDir, ["checkout", "-b", "feature/session-work"]);
562
+ await commitFile(repoDir, "src/session-marker.ts", "session marker");
563
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
564
+
565
+ // Write session-branch.txt pointing to the feature branch
566
+ await writeFile(join(repoDir, ".legio", "session-branch.txt"), "feature/session-work\n");
567
+
568
+ // Create a feature branch to merge
569
+ const branchName = "legio/builder/bead-session-branch";
570
+ await runGitInDir(repoDir, ["checkout", "-b", branchName]);
571
+ await commitFile(repoDir, `src/${branchName}.ts`, "feature for session branch");
572
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
573
+
574
+ let output = "";
575
+ const originalWrite = process.stdout.write.bind(process.stdout);
576
+ process.stdout.write = (chunk: unknown): boolean => {
577
+ output += String(chunk);
578
+ return true;
579
+ };
580
+
581
+ try {
582
+ // No --into flag — should read session-branch.txt
583
+ await mergeCommand(["--branch", branchName, "--json"]);
584
+ } finally {
585
+ process.stdout.write = originalWrite;
586
+ }
587
+
588
+ const parsed = JSON.parse(output);
589
+ expect(parsed.success).toBe(true);
590
+
591
+ // Verify merge went to session branch, not defaultBranch
592
+ const currentBranch = await runGitInDir(repoDir, ["symbolic-ref", "--short", "HEAD"]);
593
+ expect(currentBranch.trim()).toBe("feature/session-work");
594
+
595
+ // Verify feature file exists on the session branch
596
+ const featureFile = await readFile(join(repoDir, `src/${branchName}.ts`), "utf-8");
597
+ expect(featureFile).toBe("feature for session branch");
598
+ });
599
+
600
+ test("--into flag overrides session-branch.txt", async () => {
601
+ await setupProject(repoDir, defaultBranch);
602
+
603
+ // Create two target branches
604
+ await commitFile(repoDir, "src/base.ts", "base content");
605
+ await runGitInDir(repoDir, ["checkout", "-b", "session-branch-target"]);
606
+ await commitFile(repoDir, "src/session-marker.ts", "session marker");
607
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
608
+ await runGitInDir(repoDir, ["checkout", "-b", "explicit-target"]);
609
+ await commitFile(repoDir, "src/explicit-marker.ts", "explicit marker");
610
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
611
+
612
+ // Write session-branch.txt pointing to session-branch-target
613
+ await writeFile(join(repoDir, ".legio", "session-branch.txt"), "session-branch-target\n");
614
+
615
+ // Create a feature branch to merge
616
+ const branchName = "legio/builder/bead-override-test";
617
+ await runGitInDir(repoDir, ["checkout", "-b", branchName]);
618
+ await commitFile(repoDir, `src/${branchName}.ts`, "feature content");
619
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
620
+
621
+ let output = "";
622
+ const originalWrite = process.stdout.write.bind(process.stdout);
623
+ process.stdout.write = (chunk: unknown): boolean => {
624
+ output += String(chunk);
625
+ return true;
626
+ };
627
+
628
+ try {
629
+ // --into overrides session-branch.txt
630
+ await mergeCommand(["--branch", branchName, "--into", "explicit-target", "--json"]);
631
+ } finally {
632
+ process.stdout.write = originalWrite;
633
+ }
634
+
635
+ const parsed = JSON.parse(output);
636
+ expect(parsed.success).toBe(true);
637
+
638
+ // Verify merge went to explicit-target, not session-branch-target
639
+ const currentBranch = await runGitInDir(repoDir, ["symbolic-ref", "--short", "HEAD"]);
640
+ expect(currentBranch.trim()).toBe("explicit-target");
641
+ });
642
+ });
643
+
644
+ describe("conflict handling", () => {
645
+ test("content conflict auto-resolves: same file modified on both branches, verify incoming content wins", async () => {
646
+ await setupProject(repoDir, defaultBranch);
647
+
648
+ // Create a conflict: modify same file on both branches
649
+ await commitFile(repoDir, "src/shared.ts", "base content");
650
+
651
+ // Modify on default branch
652
+ await commitFile(repoDir, "src/shared.ts", "default branch content");
653
+
654
+ // Create feature branch and modify the same file
655
+ const branchName = "legio/builder-conflict/bead-999";
656
+ await runGitInDir(repoDir, ["checkout", "-b", branchName]);
657
+ await commitFile(repoDir, "src/shared.ts", "feature branch content");
658
+ await runGitInDir(repoDir, ["checkout", defaultBranch]);
659
+
660
+ const originalWrite = process.stdout.write.bind(process.stdout);
661
+ process.stdout.write = (): boolean => {
662
+ return true;
663
+ };
664
+
665
+ try {
666
+ await mergeCommand(["--branch", branchName]);
667
+ } finally {
668
+ process.stdout.write = originalWrite;
669
+ }
670
+
671
+ // Verify incoming (feature branch) content wins
672
+ const sharedFile = await readFile(join(repoDir, "src/shared.ts"), "utf-8");
673
+ expect(sharedFile).toBe("feature branch content");
674
+ });
675
+ });
676
+ });