@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,374 @@
1
+ /**
2
+ * CLI command: legio merge
3
+ *
4
+ * Merges agent branches back to the canonical branch using
5
+ * the merge queue and tiered conflict resolver.
6
+ *
7
+ * Usage:
8
+ * legio merge --branch <name> Merge a specific branch
9
+ * legio merge --all Merge all pending branches
10
+ * legio merge --dry-run Check for conflicts without merging
11
+ * legio merge --json Output results as JSON
12
+ */
13
+
14
+ import { spawn } from "node:child_process";
15
+ import { readFile } from "node:fs/promises";
16
+ import { join } from "node:path";
17
+ import { loadConfig } from "../config.ts";
18
+ import { MergeError, ValidationError } from "../errors.ts";
19
+ import { createMergeQueue } from "../merge/queue.ts";
20
+ import { createMergeResolver } from "../merge/resolver.ts";
21
+ import { createMulchClient } from "../mulch/client.ts";
22
+ import type { MergeEntry, MergeResult } from "../types.ts";
23
+
24
+ /**
25
+ * Parse a named flag value from an args array.
26
+ * Returns the value after the flag, or undefined if not present.
27
+ */
28
+ function getFlag(args: string[], flag: string): string | undefined {
29
+ const idx = args.indexOf(flag);
30
+ if (idx === -1 || idx + 1 >= args.length) {
31
+ return undefined;
32
+ }
33
+ return args[idx + 1];
34
+ }
35
+
36
+ /** Check if a boolean flag is present in the args. */
37
+ function hasFlag(args: string[], flag: string): boolean {
38
+ return args.includes(flag);
39
+ }
40
+
41
+ /**
42
+ * Extract agent name from a branch following the legio naming convention.
43
+ * Pattern: legio/{agentName}/{beadId}
44
+ * Falls back to "unknown" if the pattern does not match.
45
+ */
46
+ function parseAgentName(branchName: string): string {
47
+ const parts = branchName.split("/");
48
+ if (parts[0] === "legio" && parts[1] !== undefined) {
49
+ return parts[1];
50
+ }
51
+ return "unknown";
52
+ }
53
+
54
+ /**
55
+ * Extract bead ID from a branch following the legio naming convention.
56
+ * Pattern: legio/{agentName}/{beadId}
57
+ * Falls back to "unknown" if the pattern does not match.
58
+ */
59
+ function parseBeadId(branchName: string): string {
60
+ const parts = branchName.split("/");
61
+ if (parts[0] === "legio" && parts[2] !== undefined) {
62
+ return parts[2];
63
+ }
64
+ return "unknown";
65
+ }
66
+
67
+ /**
68
+ * Detect modified files between a branch and the canonical branch using git diff.
69
+ * Returns an array of file paths that differ.
70
+ */
71
+ async function detectModifiedFiles(
72
+ repoRoot: string,
73
+ canonicalBranch: string,
74
+ branchName: string,
75
+ ): Promise<string[]> {
76
+ const { exitCode, stdout, stderr } = await new Promise<{
77
+ exitCode: number;
78
+ stdout: string;
79
+ stderr: string;
80
+ }>((resolve) => {
81
+ const proc = spawn("git", ["diff", "--name-only", `${canonicalBranch}...${branchName}`], {
82
+ cwd: repoRoot,
83
+ stdio: ["ignore", "pipe", "pipe"],
84
+ });
85
+ const stdoutChunks: Buffer[] = [];
86
+ const stderrChunks: Buffer[] = [];
87
+ proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
88
+ proc.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
89
+ proc.on("close", (code: number | null) => {
90
+ resolve({
91
+ exitCode: code ?? 1,
92
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
93
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
94
+ });
95
+ });
96
+ });
97
+
98
+ if (exitCode !== 0) {
99
+ throw new MergeError(
100
+ `Failed to detect modified files for branch "${branchName}": ${stderr.trim()}`,
101
+ { branchName },
102
+ );
103
+ }
104
+
105
+ return stdout
106
+ .trim()
107
+ .split("\n")
108
+ .filter((line) => line.length > 0);
109
+ }
110
+
111
+ /** Format a single merge result for human-readable output. */
112
+ function formatResult(result: MergeResult): string {
113
+ const statusIcon = result.success ? "Merged" : "Failed";
114
+ const lines: string[] = [
115
+ `Merging branch: ${result.entry.branchName}`,
116
+ ` Agent: ${result.entry.agentName} | Task: ${result.entry.beadId}`,
117
+ ` Files: ${result.entry.filesModified.length} modified`,
118
+ ` Result: ${statusIcon} (tier: ${result.tier})`,
119
+ ];
120
+
121
+ if (result.conflictFiles.length > 0) {
122
+ lines.push(` Conflicts: ${result.conflictFiles.join(", ")}`);
123
+ }
124
+
125
+ if (result.errorMessage) {
126
+ lines.push(` Error: ${result.errorMessage}`);
127
+ }
128
+
129
+ return lines.join("\n");
130
+ }
131
+
132
+ /** Format a dry-run report for a merge entry. */
133
+ function formatDryRun(entry: MergeEntry): string {
134
+ const lines: string[] = [
135
+ `[dry-run] Branch: ${entry.branchName}`,
136
+ ` Agent: ${entry.agentName} | Task: ${entry.beadId}`,
137
+ ` Status: ${entry.status}`,
138
+ ` Files: ${entry.filesModified.length} modified`,
139
+ ];
140
+
141
+ if (entry.filesModified.length > 0) {
142
+ for (const f of entry.filesModified) {
143
+ lines.push(` - ${f}`);
144
+ }
145
+ }
146
+
147
+ return lines.join("\n");
148
+ }
149
+
150
+ /**
151
+ * Entry point for `legio merge [flags]`.
152
+ *
153
+ * Flags:
154
+ * --branch <name> Merge a specific branch
155
+ * --all Merge all pending branches in the queue
156
+ * --dry-run Check for conflicts without actually merging
157
+ * --json Output results as JSON
158
+ */
159
+ const MERGE_HELP = `legio merge — Merge agent branches into canonical
160
+
161
+ Usage: legio merge --branch <name> | --all [--into <branch>] [--dry-run] [--json]
162
+
163
+ Options:
164
+ --branch <name> Merge a specific branch
165
+ --all Merge all pending branches in the queue
166
+ --into <branch> Target branch to merge into (default: config canonicalBranch)
167
+ --dry-run Check for conflicts without actually merging
168
+ --json Output results as JSON
169
+ --help, -h Show this help`;
170
+
171
+ export async function mergeCommand(args: string[]): Promise<void> {
172
+ if (args.includes("--help") || args.includes("-h")) {
173
+ process.stdout.write(`${MERGE_HELP}\n`);
174
+ return;
175
+ }
176
+
177
+ const branchName = getFlag(args, "--branch");
178
+ const all = hasFlag(args, "--all");
179
+ const into = getFlag(args, "--into");
180
+ const dryRun = hasFlag(args, "--dry-run");
181
+ const json = hasFlag(args, "--json");
182
+
183
+ if (!branchName && !all) {
184
+ throw new ValidationError("Either --branch <name> or --all is required for legio merge", {
185
+ field: "branch|all",
186
+ });
187
+ }
188
+
189
+ const cwd = process.cwd();
190
+ const config = await loadConfig(cwd);
191
+
192
+ // Resolution chain: --into flag > session-start branch > config canonicalBranch
193
+ let sessionBranch: string | null = null;
194
+ if (into === undefined) {
195
+ const sessionBranchPath = join(config.project.root, ".legio", "session-branch.txt");
196
+ try {
197
+ const content = (await readFile(sessionBranchPath, "utf-8")).trim();
198
+ if (content) {
199
+ sessionBranch = content;
200
+ }
201
+ } catch {
202
+ // session-branch.txt doesn't exist
203
+ }
204
+ }
205
+ const targetBranch = into ?? sessionBranch ?? config.project.canonicalBranch;
206
+ const queuePath = join(config.project.root, ".legio", "merge-queue.db");
207
+ const queue = createMergeQueue(queuePath);
208
+ const mulchClient = createMulchClient(config.project.root);
209
+ const resolver = createMergeResolver({
210
+ aiResolveEnabled: config.merge.aiResolveEnabled,
211
+ reimagineEnabled: config.merge.reimagineEnabled,
212
+ mulchClient,
213
+ });
214
+
215
+ if (branchName) {
216
+ await handleBranch(branchName, queue, resolver, config, targetBranch, dryRun, json);
217
+ } else {
218
+ await handleAll(queue, resolver, config, targetBranch, dryRun, json);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Handle merging a specific branch.
224
+ * If the branch is not in the queue, creates a new entry by detecting
225
+ * agent name, bead ID, and modified files from git.
226
+ */
227
+ async function handleBranch(
228
+ branchName: string,
229
+ queue: ReturnType<typeof createMergeQueue>,
230
+ resolver: ReturnType<typeof createMergeResolver>,
231
+ config: Awaited<ReturnType<typeof loadConfig>>,
232
+ targetBranch: string,
233
+ dryRun: boolean,
234
+ json: boolean,
235
+ ): Promise<void> {
236
+ const canonicalBranch = targetBranch;
237
+ const repoRoot = config.project.root;
238
+
239
+ // Look for existing entry in the queue
240
+ const allEntries = queue.list();
241
+ let entry = allEntries.find((e) => e.branchName === branchName) ?? null;
242
+
243
+ // If not in queue, create one by detecting info from the branch
244
+ if (entry === null) {
245
+ // Validate that the branch exists before attempting any git operations
246
+ const verifyResult = await new Promise<{ exitCode: number }>((resolve) => {
247
+ const proc = spawn("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
248
+ cwd: repoRoot,
249
+ stdio: ["ignore", "pipe", "pipe"],
250
+ });
251
+ proc.on("close", (code: number | null) => {
252
+ resolve({ exitCode: code ?? 1 });
253
+ });
254
+ });
255
+ if (verifyResult.exitCode !== 0) {
256
+ throw new ValidationError(`Branch "${branchName}" not found`, {
257
+ field: "branch",
258
+ value: branchName,
259
+ });
260
+ }
261
+
262
+ const agentName = parseAgentName(branchName);
263
+ const beadId = parseBeadId(branchName);
264
+ const filesModified = await detectModifiedFiles(repoRoot, canonicalBranch, branchName);
265
+
266
+ entry = queue.enqueue({
267
+ branchName,
268
+ beadId,
269
+ agentName,
270
+ filesModified,
271
+ });
272
+ }
273
+
274
+ if (dryRun) {
275
+ if (json) {
276
+ process.stdout.write(`${JSON.stringify(entry)}\n`);
277
+ } else {
278
+ process.stdout.write(`${formatDryRun(entry)}\n`);
279
+ }
280
+ return;
281
+ }
282
+
283
+ // Perform the actual merge
284
+ const result = await resolver.resolve(entry, canonicalBranch, repoRoot);
285
+
286
+ // Update queue status based on result
287
+ queue.updateStatus(branchName, result.success ? "merged" : "conflict", result.tier);
288
+
289
+ if (json) {
290
+ process.stdout.write(`${JSON.stringify(result)}\n`);
291
+ } else {
292
+ process.stdout.write(`${formatResult(result)}\n`);
293
+ }
294
+
295
+ if (!result.success) {
296
+ throw new MergeError(result.errorMessage ?? `Merge failed for branch "${branchName}"`, {
297
+ branchName,
298
+ conflictFiles: result.conflictFiles,
299
+ });
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Handle merging all pending branches in the queue.
305
+ * Processes entries sequentially in FIFO order.
306
+ */
307
+ async function handleAll(
308
+ queue: ReturnType<typeof createMergeQueue>,
309
+ resolver: ReturnType<typeof createMergeResolver>,
310
+ config: Awaited<ReturnType<typeof loadConfig>>,
311
+ targetBranch: string,
312
+ dryRun: boolean,
313
+ json: boolean,
314
+ ): Promise<void> {
315
+ const canonicalBranch = targetBranch;
316
+ const repoRoot = config.project.root;
317
+
318
+ const pendingEntries = queue.list("pending");
319
+
320
+ if (pendingEntries.length === 0) {
321
+ if (json) {
322
+ process.stdout.write(`${JSON.stringify({ results: [], count: 0 })}\n`);
323
+ } else {
324
+ process.stdout.write("No pending branches to merge.\n");
325
+ }
326
+ return;
327
+ }
328
+
329
+ if (dryRun) {
330
+ if (json) {
331
+ process.stdout.write(`${JSON.stringify(pendingEntries)}\n`);
332
+ } else {
333
+ process.stdout.write(
334
+ `${pendingEntries.length} pending branch${pendingEntries.length === 1 ? "" : "es"}:\n\n`,
335
+ );
336
+ for (const entry of pendingEntries) {
337
+ process.stdout.write(`${formatDryRun(entry)}\n\n`);
338
+ }
339
+ }
340
+ return;
341
+ }
342
+
343
+ const results: MergeResult[] = [];
344
+ let successCount = 0;
345
+ let failCount = 0;
346
+
347
+ for (const entry of pendingEntries) {
348
+ const result = await resolver.resolve(entry, canonicalBranch, repoRoot);
349
+
350
+ queue.updateStatus(entry.branchName, result.success ? "merged" : "conflict", result.tier);
351
+
352
+ results.push(result);
353
+
354
+ if (result.success) {
355
+ successCount++;
356
+ } else {
357
+ failCount++;
358
+ }
359
+
360
+ if (!json) {
361
+ process.stdout.write(`${formatResult(result)}\n\n`);
362
+ }
363
+ }
364
+
365
+ if (json) {
366
+ process.stdout.write(
367
+ `${JSON.stringify({ results, count: results.length, successCount, failCount })}\n`,
368
+ );
369
+ } else {
370
+ process.stdout.write(
371
+ `Done: ${successCount} merged, ${failCount} failed out of ${results.length} total.\n`,
372
+ );
373
+ }
374
+ }