swarm-code 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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/bin/swarm.mjs +45 -0
  4. package/dist/agents/aider.d.ts +12 -0
  5. package/dist/agents/aider.js +182 -0
  6. package/dist/agents/claude-code.d.ts +9 -0
  7. package/dist/agents/claude-code.js +216 -0
  8. package/dist/agents/codex.d.ts +14 -0
  9. package/dist/agents/codex.js +193 -0
  10. package/dist/agents/direct-llm.d.ts +9 -0
  11. package/dist/agents/direct-llm.js +78 -0
  12. package/dist/agents/mock.d.ts +9 -0
  13. package/dist/agents/mock.js +77 -0
  14. package/dist/agents/opencode.d.ts +23 -0
  15. package/dist/agents/opencode.js +571 -0
  16. package/dist/agents/provider.d.ts +11 -0
  17. package/dist/agents/provider.js +31 -0
  18. package/dist/cli.d.ts +15 -0
  19. package/dist/cli.js +285 -0
  20. package/dist/compression/compressor.d.ts +28 -0
  21. package/dist/compression/compressor.js +265 -0
  22. package/dist/config.d.ts +42 -0
  23. package/dist/config.js +170 -0
  24. package/dist/core/repl.d.ts +69 -0
  25. package/dist/core/repl.js +336 -0
  26. package/dist/core/rlm.d.ts +63 -0
  27. package/dist/core/rlm.js +409 -0
  28. package/dist/core/runtime.py +335 -0
  29. package/dist/core/types.d.ts +131 -0
  30. package/dist/core/types.js +19 -0
  31. package/dist/env.d.ts +10 -0
  32. package/dist/env.js +75 -0
  33. package/dist/interactive-swarm.d.ts +20 -0
  34. package/dist/interactive-swarm.js +1041 -0
  35. package/dist/interactive.d.ts +10 -0
  36. package/dist/interactive.js +1765 -0
  37. package/dist/main.d.ts +15 -0
  38. package/dist/main.js +242 -0
  39. package/dist/mcp/server.d.ts +15 -0
  40. package/dist/mcp/server.js +72 -0
  41. package/dist/mcp/session.d.ts +73 -0
  42. package/dist/mcp/session.js +184 -0
  43. package/dist/mcp/tools.d.ts +15 -0
  44. package/dist/mcp/tools.js +377 -0
  45. package/dist/memory/episodic.d.ts +132 -0
  46. package/dist/memory/episodic.js +390 -0
  47. package/dist/prompts/orchestrator.d.ts +5 -0
  48. package/dist/prompts/orchestrator.js +191 -0
  49. package/dist/routing/model-router.d.ts +130 -0
  50. package/dist/routing/model-router.js +515 -0
  51. package/dist/swarm.d.ts +14 -0
  52. package/dist/swarm.js +557 -0
  53. package/dist/threads/cache.d.ts +58 -0
  54. package/dist/threads/cache.js +198 -0
  55. package/dist/threads/manager.d.ts +85 -0
  56. package/dist/threads/manager.js +659 -0
  57. package/dist/ui/banner.d.ts +14 -0
  58. package/dist/ui/banner.js +42 -0
  59. package/dist/ui/dashboard.d.ts +33 -0
  60. package/dist/ui/dashboard.js +151 -0
  61. package/dist/ui/index.d.ts +10 -0
  62. package/dist/ui/index.js +11 -0
  63. package/dist/ui/log.d.ts +39 -0
  64. package/dist/ui/log.js +126 -0
  65. package/dist/ui/onboarding.d.ts +14 -0
  66. package/dist/ui/onboarding.js +518 -0
  67. package/dist/ui/spinner.d.ts +25 -0
  68. package/dist/ui/spinner.js +113 -0
  69. package/dist/ui/summary.d.ts +18 -0
  70. package/dist/ui/summary.js +113 -0
  71. package/dist/ui/theme.d.ts +63 -0
  72. package/dist/ui/theme.js +97 -0
  73. package/dist/viewer.d.ts +12 -0
  74. package/dist/viewer.js +1284 -0
  75. package/dist/worktree/manager.d.ts +45 -0
  76. package/dist/worktree/manager.js +266 -0
  77. package/dist/worktree/merge.d.ts +28 -0
  78. package/dist/worktree/merge.js +138 -0
  79. package/package.json +69 -0
@@ -0,0 +1,1041 @@
1
+ /**
2
+ * Interactive swarm REPL — a persistent session for follow-up tasks,
3
+ * thread inspection, manual merge/reject, and live DAG visualization.
4
+ *
5
+ * Usage: swarm --dir ./project (no query argument enters interactive mode)
6
+ *
7
+ * Commands:
8
+ * <task> Run a task through the RLM orchestrator
9
+ * /threads (/t) List all threads with status, cost, duration
10
+ * /thread <id> Show detailed info for a specific thread
11
+ * /merge [id...] Merge specific thread branches (or all if no args)
12
+ * /reject <id> Discard a thread's worktree and branch
13
+ * /dag Show thread dependency DAG with status indicators
14
+ * /budget Show budget state
15
+ * /status Overall session status
16
+ * /help Show available commands
17
+ * /quit (/exit) Cleanup and exit
18
+ */
19
+ import "./env.js";
20
+ import * as fs from "node:fs";
21
+ import * as path from "node:path";
22
+ import * as readline from "node:readline";
23
+ // Dynamic imports — ensures env.js has set process.env BEFORE pi-ai loads
24
+ const { getModels, getProviders } = await import("@mariozechner/pi-ai");
25
+ const { PythonRepl } = await import("./core/repl.js");
26
+ const { runRlmLoop } = await import("./core/rlm.js");
27
+ const { loadConfig } = await import("./config.js");
28
+ // Register agent backends
29
+ const opencodeMod = await import("./agents/opencode.js");
30
+ await import("./agents/direct-llm.js");
31
+ await import("./agents/claude-code.js");
32
+ await import("./agents/codex.js");
33
+ await import("./agents/aider.js");
34
+ import { randomBytes } from "node:crypto";
35
+ import { EpisodicMemory } from "./memory/episodic.js";
36
+ import { buildSwarmSystemPrompt } from "./prompts/orchestrator.js";
37
+ import { classifyTaskComplexity, describeAvailableAgents, FailureTracker, routeTask } from "./routing/model-router.js";
38
+ import { ThreadManager } from "./threads/manager.js";
39
+ import { ThreadDashboard } from "./ui/dashboard.js";
40
+ import { logError, logRouter, logSuccess, logVerbose, logWarn, setLogLevel } from "./ui/log.js";
41
+ import { runOnboarding } from "./ui/onboarding.js";
42
+ // UI system
43
+ import { Spinner } from "./ui/spinner.js";
44
+ import { bold, coral, cyan, dim, green, isTTY, red, symbols, termWidth, truncate, yellow } from "./ui/theme.js";
45
+ import { mergeAllThreads, mergeThreadBranch } from "./worktree/merge.js";
46
+ function parseInteractiveArgs(args) {
47
+ let dir = "";
48
+ let orchestratorModel = "";
49
+ let agent = "";
50
+ let maxBudget = null;
51
+ let verbose = false;
52
+ let quiet = false;
53
+ let autoRoute = false;
54
+ for (let i = 0; i < args.length; i++) {
55
+ const arg = args[i];
56
+ if (arg === "--dir" && i + 1 < args.length) {
57
+ dir = args[++i];
58
+ }
59
+ else if (arg === "--orchestrator" && i + 1 < args.length) {
60
+ orchestratorModel = args[++i];
61
+ }
62
+ else if (arg === "--agent" && i + 1 < args.length) {
63
+ agent = args[++i];
64
+ }
65
+ else if (arg === "--max-budget" && i + 1 < args.length) {
66
+ const raw = args[++i];
67
+ const parsed = parseFloat(raw);
68
+ if (Number.isFinite(parsed) && parsed > 0)
69
+ maxBudget = parsed;
70
+ }
71
+ else if (arg === "--verbose") {
72
+ verbose = true;
73
+ }
74
+ else if (arg === "--quiet" || arg === "-q") {
75
+ quiet = true;
76
+ }
77
+ else if (arg === "--auto-route") {
78
+ autoRoute = true;
79
+ }
80
+ // Silently ignore unknown flags and positional args
81
+ }
82
+ if (!dir) {
83
+ logError("--dir <path> is required for interactive swarm mode");
84
+ process.exit(1);
85
+ }
86
+ return {
87
+ dir: path.resolve(dir),
88
+ orchestratorModel: orchestratorModel || process.env.RLM_MODEL || "claude-sonnet-4-6",
89
+ agent: agent || "",
90
+ maxBudget,
91
+ verbose,
92
+ quiet,
93
+ autoRoute,
94
+ };
95
+ }
96
+ // ── Codebase scanning (mirrored from swarm.ts) ──────────────────────────────
97
+ const SKIP_DIRS = new Set([
98
+ "node_modules",
99
+ ".git",
100
+ "dist",
101
+ "build",
102
+ ".next",
103
+ ".venv",
104
+ "venv",
105
+ "__pycache__",
106
+ ".swarm-worktrees",
107
+ "coverage",
108
+ ".turbo",
109
+ ".cache",
110
+ ]);
111
+ const SKIP_EXTENSIONS = new Set([
112
+ ".png",
113
+ ".jpg",
114
+ ".jpeg",
115
+ ".gif",
116
+ ".ico",
117
+ ".svg",
118
+ ".woff",
119
+ ".woff2",
120
+ ".ttf",
121
+ ".eot",
122
+ ".mp3",
123
+ ".mp4",
124
+ ".webm",
125
+ ".zip",
126
+ ".tar",
127
+ ".gz",
128
+ ".lock",
129
+ ".map",
130
+ ]);
131
+ function scanDirectory(dir, maxFiles = 200, maxTotalSize = 2 * 1024 * 1024) {
132
+ const files = [];
133
+ let totalSize = 0;
134
+ function walk(currentDir, depth) {
135
+ if (depth > 15 || files.length >= maxFiles || totalSize >= maxTotalSize)
136
+ return;
137
+ let entries;
138
+ try {
139
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
140
+ }
141
+ catch {
142
+ return;
143
+ }
144
+ entries.sort((a, b) => a.name.localeCompare(b.name));
145
+ for (const entry of entries) {
146
+ if (files.length >= maxFiles || totalSize >= maxTotalSize)
147
+ return;
148
+ if (entry.isDirectory()) {
149
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
150
+ walk(path.join(currentDir, entry.name), depth + 1);
151
+ }
152
+ }
153
+ else if (entry.isFile()) {
154
+ const ext = path.extname(entry.name).toLowerCase();
155
+ if (SKIP_EXTENSIONS.has(ext))
156
+ continue;
157
+ const fullPath = path.join(currentDir, entry.name);
158
+ try {
159
+ const stat = fs.statSync(fullPath);
160
+ if (stat.size > 100 * 1024)
161
+ continue;
162
+ if (stat.size === 0)
163
+ continue;
164
+ const content = fs.readFileSync(fullPath, "utf-8");
165
+ if (content.includes("\0"))
166
+ continue;
167
+ const relPath = path.relative(dir, fullPath);
168
+ files.push({ relPath, content });
169
+ totalSize += content.length;
170
+ }
171
+ catch { }
172
+ }
173
+ }
174
+ }
175
+ walk(dir, 0);
176
+ const parts = [];
177
+ parts.push(`Codebase: ${path.basename(dir)}`);
178
+ parts.push(`Files: ${files.length}`);
179
+ parts.push(`Total size: ${(totalSize / 1024).toFixed(1)}KB`);
180
+ parts.push("---");
181
+ for (const file of files) {
182
+ parts.push(`\n=== ${file.relPath} ===`);
183
+ parts.push(file.content);
184
+ }
185
+ return parts.join("\n");
186
+ }
187
+ // ── Model resolution (mirrored from swarm.ts) ───────────────────────────────
188
+ function resolveModel(modelId) {
189
+ const providerKeys = {
190
+ anthropic: "ANTHROPIC_API_KEY",
191
+ openai: "OPENAI_API_KEY",
192
+ google: "GEMINI_API_KEY",
193
+ };
194
+ const defaultModels = {
195
+ anthropic: "claude-sonnet-4-6",
196
+ openai: "gpt-4o",
197
+ google: "gemini-2.5-flash",
198
+ };
199
+ const knownProviders = new Set(Object.keys(providerKeys));
200
+ let model;
201
+ let resolvedProvider = "";
202
+ for (const provider of getProviders()) {
203
+ if (!knownProviders.has(provider))
204
+ continue;
205
+ const key = providerKeys[provider];
206
+ if (!process.env[key])
207
+ continue;
208
+ for (const m of getModels(provider)) {
209
+ if (m.id === modelId) {
210
+ model = m;
211
+ resolvedProvider = provider;
212
+ break;
213
+ }
214
+ }
215
+ if (model)
216
+ break;
217
+ }
218
+ if (!model) {
219
+ for (const provider of getProviders()) {
220
+ if (knownProviders.has(provider))
221
+ continue;
222
+ for (const m of getModels(provider)) {
223
+ if (m.id === modelId) {
224
+ model = m;
225
+ resolvedProvider = provider;
226
+ break;
227
+ }
228
+ }
229
+ if (model)
230
+ break;
231
+ }
232
+ }
233
+ if (!model) {
234
+ for (const [prov, envKey] of Object.entries(providerKeys)) {
235
+ if (!process.env[envKey])
236
+ continue;
237
+ const fallbackId = defaultModels[prov];
238
+ if (!fallbackId)
239
+ continue;
240
+ for (const p of getProviders()) {
241
+ if (p !== prov)
242
+ continue;
243
+ for (const m of getModels(p)) {
244
+ if (m.id === fallbackId) {
245
+ model = m;
246
+ resolvedProvider = prov;
247
+ logWarn(`Using ${fallbackId} (${prov}) — model "${modelId}" not found`);
248
+ break;
249
+ }
250
+ }
251
+ if (model)
252
+ break;
253
+ }
254
+ if (model)
255
+ break;
256
+ }
257
+ }
258
+ if (!model)
259
+ return null;
260
+ return { model, provider: resolvedProvider };
261
+ }
262
+ // ── Formatting helpers ──────────────────────────────────────────────────────
263
+ function formatDuration(ms) {
264
+ if (ms < 1000)
265
+ return `${ms}ms`;
266
+ const s = ms / 1000;
267
+ if (s < 60)
268
+ return `${s.toFixed(1)}s`;
269
+ const m = Math.floor(s / 60);
270
+ const rem = s % 60;
271
+ return `${m}m ${rem.toFixed(0)}s`;
272
+ }
273
+ function formatCost(usd) {
274
+ return `$${usd.toFixed(4)}`;
275
+ }
276
+ function statusIcon(status) {
277
+ switch (status) {
278
+ case "completed":
279
+ return green(symbols.check);
280
+ case "failed":
281
+ return red(symbols.cross);
282
+ case "cancelled":
283
+ return yellow(symbols.dash);
284
+ case "running":
285
+ return coral(symbols.arrow);
286
+ case "pending":
287
+ return dim(symbols.dot);
288
+ default:
289
+ return dim("?");
290
+ }
291
+ }
292
+ function statusColor(status) {
293
+ switch (status) {
294
+ case "completed":
295
+ return green;
296
+ case "failed":
297
+ return red;
298
+ case "cancelled":
299
+ return yellow;
300
+ case "running":
301
+ return coral;
302
+ default:
303
+ return dim;
304
+ }
305
+ }
306
+ // ── Command handlers ────────────────────────────────────────────────────────
307
+ function cmdHelp() {
308
+ const w = Math.min(termWidth(), 70);
309
+ const out = process.stderr;
310
+ out.write("\n");
311
+ out.write(` ${bold(cyan("Interactive Swarm Commands"))}\n`);
312
+ out.write(` ${dim(symbols.horizontal.repeat(Math.min(w - 4, 40)))}\n`);
313
+ out.write("\n");
314
+ out.write(` ${yellow("<task>")} ${dim("Run a task through the orchestrator")}\n`);
315
+ out.write(` ${cyan("/threads")} ${dim("(/t)")} ${dim("List all threads with status")}\n`);
316
+ out.write(` ${cyan("/thread")} ${yellow("<id>")} ${dim("Show detailed info for a thread")}\n`);
317
+ out.write(` ${cyan("/merge")} ${yellow("[id...]")} ${dim("Merge thread branches (all if no args)")}\n`);
318
+ out.write(` ${cyan("/reject")} ${yellow("<id>")} ${dim("Discard a thread worktree and branch")}\n`);
319
+ out.write(` ${cyan("/dag")} ${dim("Show thread DAG with status indicators")}\n`);
320
+ out.write(` ${cyan("/budget")} ${dim("Show budget state")}\n`);
321
+ out.write(` ${cyan("/status")} ${dim("Overall session status")}\n`);
322
+ out.write(` ${cyan("/help")} ${dim("Show this help")}\n`);
323
+ out.write(` ${cyan("/quit")} ${dim("(/exit)")} ${dim("Cleanup and exit")}\n`);
324
+ out.write("\n");
325
+ }
326
+ function cmdThreads(threadManager) {
327
+ const threads = threadManager.getThreads();
328
+ const out = process.stderr;
329
+ if (threads.length === 0) {
330
+ out.write(`\n ${dim("No threads yet. Type a task to get started.")}\n\n`);
331
+ return;
332
+ }
333
+ out.write("\n");
334
+ out.write(` ${bold(cyan("Threads"))} ${dim(`(${threads.length} total)`)}\n`);
335
+ out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 60)))}\n`);
336
+ for (const t of threads) {
337
+ const icon = statusIcon(t.status);
338
+ const id = dim(t.id.slice(0, 8));
339
+ const status = statusColor(t.status)(t.status);
340
+ const dur = t.completedAt && t.startedAt
341
+ ? dim(formatDuration(t.completedAt - t.startedAt))
342
+ : t.startedAt
343
+ ? dim(formatDuration(Date.now() - t.startedAt))
344
+ : dim("--");
345
+ const cost = t.estimatedCostUsd > 0 ? dim(formatCost(t.estimatedCostUsd)) : "";
346
+ const files = t.result?.filesChanged.length ?? 0;
347
+ const fileStr = files > 0 ? dim(`${files} files`) : "";
348
+ const task = truncate(t.config.task, 45);
349
+ out.write(` ${icon} ${id} ${status} ${dur} ${cost} ${fileStr} ${dim(task)}\n`);
350
+ }
351
+ out.write("\n");
352
+ }
353
+ function cmdThread(threadManager, threadId) {
354
+ const out = process.stderr;
355
+ if (!threadId) {
356
+ logError("Usage: /thread <id>");
357
+ return;
358
+ }
359
+ // Find thread by prefix match
360
+ const threads = threadManager.getThreads();
361
+ const matches = threads.filter((t) => t.id.startsWith(threadId));
362
+ if (matches.length === 0) {
363
+ logError(`No thread found matching "${threadId}"`);
364
+ return;
365
+ }
366
+ if (matches.length > 1) {
367
+ logWarn(`Multiple matches for "${threadId}": ${matches.map((t) => t.id.slice(0, 8)).join(", ")}`);
368
+ return;
369
+ }
370
+ const t = matches[0];
371
+ out.write("\n");
372
+ out.write(` ${bold(cyan("Thread"))} ${dim(t.id)}\n`);
373
+ out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 60)))}\n`);
374
+ out.write(` ${dim("Status")} ${statusColor(t.status)(t.status)} ${statusIcon(t.status)}\n`);
375
+ out.write(` ${dim("Phase")} ${t.phase}\n`);
376
+ out.write(` ${dim("Task")} ${t.config.task}\n`);
377
+ out.write(` ${dim("Agent")} ${t.config.agent.backend || "default"}\n`);
378
+ out.write(` ${dim("Model")} ${t.config.agent.model || "default"}\n`);
379
+ out.write(` ${dim("Attempt")} ${t.attempt}/${t.maxAttempts}\n`);
380
+ if (t.startedAt) {
381
+ const started = new Date(t.startedAt).toLocaleTimeString();
382
+ out.write(` ${dim("Started")} ${started}\n`);
383
+ }
384
+ if (t.completedAt && t.startedAt) {
385
+ out.write(` ${dim("Duration")} ${formatDuration(t.completedAt - t.startedAt)}\n`);
386
+ }
387
+ if (t.estimatedCostUsd > 0) {
388
+ out.write(` ${dim("Cost")} ${formatCost(t.estimatedCostUsd)}\n`);
389
+ }
390
+ if (t.branchName) {
391
+ out.write(` ${dim("Branch")} ${cyan(t.branchName)}\n`);
392
+ }
393
+ if (t.worktreePath) {
394
+ out.write(` ${dim("Worktree")} ${t.worktreePath}\n`);
395
+ }
396
+ if (t.error) {
397
+ out.write(` ${dim("Error")} ${red(t.error)}\n`);
398
+ }
399
+ // Show result summary
400
+ if (t.result) {
401
+ out.write("\n");
402
+ out.write(` ${bold("Result")}\n`);
403
+ if (t.result.filesChanged.length > 0) {
404
+ out.write(` ${dim("Files changed:")}\n`);
405
+ for (const f of t.result.filesChanged) {
406
+ out.write(` ${green("+")} ${f}\n`);
407
+ }
408
+ }
409
+ if (t.result.diffStats && t.result.diffStats !== "(no changes)") {
410
+ out.write(`\n ${dim("Diff stats:")}\n`);
411
+ for (const line of t.result.diffStats.split("\n")) {
412
+ out.write(` ${dim(line)}\n`);
413
+ }
414
+ }
415
+ if (t.result.summary) {
416
+ out.write(`\n ${dim("Summary:")}\n`);
417
+ const lines = t.result.summary.split("\n");
418
+ for (const line of lines.slice(0, 20)) {
419
+ out.write(` ${line}\n`);
420
+ }
421
+ if (lines.length > 20) {
422
+ out.write(` ${dim(`... ${lines.length - 20} more lines`)}\n`);
423
+ }
424
+ }
425
+ }
426
+ out.write("\n");
427
+ }
428
+ async function cmdMerge(threadManager, dir, idArgs) {
429
+ const threads = threadManager.getThreads();
430
+ const out = process.stderr;
431
+ if (idArgs.length === 0) {
432
+ // Merge all completed threads
433
+ out.write(`\n ${dim("Merging all completed thread branches...")}\n`);
434
+ const opts = { continueOnConflict: true };
435
+ const results = await mergeAllThreads(dir, threads, opts);
436
+ if (results.length === 0) {
437
+ out.write(` ${dim("No eligible threads to merge.")}\n\n`);
438
+ return;
439
+ }
440
+ for (const r of results) {
441
+ if (r.success) {
442
+ out.write(` ${green(symbols.check)} ${cyan(r.branch)} ${dim(r.message)}\n`);
443
+ }
444
+ else {
445
+ out.write(` ${red(symbols.cross)} ${cyan(r.branch)} ${red(r.message)}\n`);
446
+ if (r.conflicts.length > 0) {
447
+ out.write(` ${dim("Conflicts:")} ${r.conflicts.join(", ")}\n`);
448
+ }
449
+ }
450
+ }
451
+ const merged = results.filter((r) => r.success).length;
452
+ const failed = results.filter((r) => !r.success).length;
453
+ if (merged > 0)
454
+ logSuccess(`Merged ${merged} branches`);
455
+ if (failed > 0)
456
+ logWarn(`${failed} branches had conflicts`);
457
+ out.write("\n");
458
+ }
459
+ else {
460
+ // Merge specific threads by ID prefix
461
+ for (const idArg of idArgs) {
462
+ const match = threads.find((t) => t.id.startsWith(idArg));
463
+ if (!match) {
464
+ logError(`No thread found matching "${idArg}"`);
465
+ continue;
466
+ }
467
+ if (match.status !== "completed" || !match.branchName) {
468
+ logWarn(`Thread ${match.id.slice(0, 8)} is not eligible for merge (status: ${match.status})`);
469
+ continue;
470
+ }
471
+ if (!match.result?.success) {
472
+ logWarn(`Thread ${match.id.slice(0, 8)} did not complete successfully`);
473
+ continue;
474
+ }
475
+ out.write(` ${dim("Merging")} ${cyan(match.branchName)}${dim("...")}\n`);
476
+ const result = await mergeThreadBranch(dir, match.branchName, match.id);
477
+ if (result.success) {
478
+ out.write(` ${green(symbols.check)} ${dim(result.message)}\n`);
479
+ }
480
+ else {
481
+ out.write(` ${red(symbols.cross)} ${red(result.message)}\n`);
482
+ if (result.conflicts.length > 0) {
483
+ out.write(` ${dim("Conflicts:")} ${result.conflicts.join(", ")}\n`);
484
+ }
485
+ }
486
+ }
487
+ out.write("\n");
488
+ }
489
+ }
490
+ async function cmdReject(threadManager, threadId) {
491
+ const out = process.stderr;
492
+ if (!threadId) {
493
+ logError("Usage: /reject <id>");
494
+ return;
495
+ }
496
+ const threads = threadManager.getThreads();
497
+ const match = threads.find((t) => t.id.startsWith(threadId));
498
+ if (!match) {
499
+ logError(`No thread found matching "${threadId}"`);
500
+ return;
501
+ }
502
+ const wtManager = threadManager.getWorktreeManager();
503
+ const wtInfo = wtManager.getWorktreeInfo(match.id);
504
+ if (wtInfo) {
505
+ out.write(` ${dim("Destroying worktree and branch for")} ${dim(match.id.slice(0, 8))}${dim("...")}\n`);
506
+ await wtManager.destroy(match.id, true);
507
+ logSuccess(`Rejected thread ${match.id.slice(0, 8)} — worktree and branch removed`);
508
+ }
509
+ else {
510
+ logWarn(`Thread ${match.id.slice(0, 8)} has no active worktree (may already be cleaned up)`);
511
+ }
512
+ }
513
+ function cmdDag(threadManager) {
514
+ const threads = threadManager.getThreads();
515
+ const out = process.stderr;
516
+ if (threads.length === 0) {
517
+ out.write(`\n ${dim("No threads yet.")}\n\n`);
518
+ return;
519
+ }
520
+ out.write("\n");
521
+ out.write(` ${bold(cyan("Thread DAG"))}\n`);
522
+ out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 60)))}\n`);
523
+ out.write("\n");
524
+ // Group by status for visual clarity
525
+ const running = threads.filter((t) => t.status === "running" || t.status === "pending");
526
+ const completed = threads.filter((t) => t.status === "completed");
527
+ const failed = threads.filter((t) => t.status === "failed");
528
+ const cancelled = threads.filter((t) => t.status === "cancelled");
529
+ // Main branch root
530
+ out.write(` ${cyan(symbols.dot)} ${bold("main")}\n`);
531
+ const renderThread = (t, isLast) => {
532
+ const connector = isLast ? symbols.bottomLeft : `${symbols.vertLine}`;
533
+ const branch = t.branchName || `swarm/${t.id.slice(0, 8)}`;
534
+ const icon = statusIcon(t.status);
535
+ const task = truncate(t.config.task, 35);
536
+ const dur = t.completedAt && t.startedAt ? dim(formatDuration(t.completedAt - t.startedAt)) : "";
537
+ out.write(` ${cyan(symbols.vertLine)}\n`);
538
+ out.write(` ${cyan(connector)}${cyan(symbols.horizontal.repeat(2))} ${icon} ${dim(branch)} ${dim(task)} ${dur}\n`);
539
+ };
540
+ const allThreads = [...running, ...completed, ...failed, ...cancelled];
541
+ for (let i = 0; i < allThreads.length; i++) {
542
+ renderThread(allThreads[i], i === allThreads.length - 1);
543
+ }
544
+ out.write("\n");
545
+ // Legend
546
+ out.write(` ${dim("Legend:")} ${green(symbols.check)} completed ${red(symbols.cross)} failed ${yellow(symbols.dash)} cancelled ${coral(symbols.arrow)} running ${dim(symbols.dot)} pending\n`);
547
+ out.write("\n");
548
+ }
549
+ function cmdBudget(threadManager) {
550
+ const budget = threadManager.getBudgetState();
551
+ const out = process.stderr;
552
+ out.write("\n");
553
+ out.write(` ${bold(cyan("Budget"))}\n`);
554
+ out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 40)))}\n`);
555
+ const pct = budget.sessionLimitUsd > 0 ? ((budget.totalSpentUsd / budget.sessionLimitUsd) * 100).toFixed(1) : "0";
556
+ const budgetColor = budget.totalSpentUsd > budget.sessionLimitUsd * 0.8 ? yellow : green;
557
+ out.write(` ${dim("Spent")} ${budgetColor(formatCost(budget.totalSpentUsd))} / ${formatCost(budget.sessionLimitUsd)} (${pct}%)\n`);
558
+ out.write(` ${dim("Per-thread")} ${formatCost(budget.perThreadLimitUsd)} max\n`);
559
+ if (budget.actualCostThreads > 0 || budget.estimatedCostThreads > 0) {
560
+ out.write(` ${dim("Cost source")} ${budget.actualCostThreads} actual, ${budget.estimatedCostThreads} estimated\n`);
561
+ }
562
+ const tokens = budget.totalTokens;
563
+ if (tokens.input > 0 || tokens.output > 0) {
564
+ const totalK = ((tokens.input + tokens.output) / 1000).toFixed(1);
565
+ out.write(` ${dim("Tokens")} ${tokens.input.toLocaleString()} in + ${tokens.output.toLocaleString()} out (${totalK}K)\n`);
566
+ }
567
+ // Per-thread costs
568
+ if (budget.threadCosts.size > 0) {
569
+ out.write(`\n ${dim("Per-thread costs:")}\n`);
570
+ for (const [id, cost] of budget.threadCosts) {
571
+ out.write(` ${dim(id.slice(0, 8))} ${formatCost(cost)}\n`);
572
+ }
573
+ }
574
+ out.write("\n");
575
+ }
576
+ function cmdStatus(threadManager, sessionStartTime, taskCount) {
577
+ const threads = threadManager.getThreads();
578
+ const budget = threadManager.getBudgetState();
579
+ const cache = threadManager.getCacheStats();
580
+ const concurrency = threadManager.getConcurrencyStats();
581
+ const out = process.stderr;
582
+ out.write("\n");
583
+ out.write(` ${bold(cyan("Session Status"))}\n`);
584
+ out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 40)))}\n`);
585
+ const elapsed = formatDuration(Date.now() - sessionStartTime);
586
+ out.write(` ${dim("Uptime")} ${elapsed}\n`);
587
+ out.write(` ${dim("Tasks run")} ${taskCount}\n`);
588
+ // Thread stats
589
+ const completed = threads.filter((t) => t.status === "completed").length;
590
+ const failed = threads.filter((t) => t.status === "failed").length;
591
+ const running = threads.filter((t) => t.status === "running").length;
592
+ const pending = threads.filter((t) => t.status === "pending").length;
593
+ out.write(` ${dim("Threads")} ${threads.length} total`);
594
+ if (completed > 0)
595
+ out.write(` ${green(`${completed} done`)}`);
596
+ if (failed > 0)
597
+ out.write(` ${red(`${failed} failed`)}`);
598
+ if (running > 0)
599
+ out.write(` ${coral(`${running} running`)}`);
600
+ if (pending > 0)
601
+ out.write(` ${dim(`${pending} pending`)}`);
602
+ out.write("\n");
603
+ out.write(` ${dim("Concurrency")} ${concurrency.active}/${concurrency.max} active, ${concurrency.waiting} waiting\n`);
604
+ // Budget
605
+ const pct = budget.sessionLimitUsd > 0 ? ((budget.totalSpentUsd / budget.sessionLimitUsd) * 100).toFixed(1) : "0";
606
+ out.write(` ${dim("Budget")} ${formatCost(budget.totalSpentUsd)} / ${formatCost(budget.sessionLimitUsd)} (${pct}%)\n`);
607
+ // Cache
608
+ if (cache.hits > 0 || cache.size > 0) {
609
+ const saved = cache.totalSavedMs > 0 ? `, saved ${formatDuration(cache.totalSavedMs)}` : "";
610
+ out.write(` ${dim("Cache")} ${cache.hits} hits, ${cache.misses} misses, ${cache.size} entries${saved}\n`);
611
+ }
612
+ out.write("\n");
613
+ }
614
+ // ── Interactive banner ──────────────────────────────────────────────────────
615
+ function renderInteractiveBanner(config) {
616
+ const w = Math.max(Math.min(termWidth(), 60), 24);
617
+ const out = process.stderr;
618
+ if (isTTY) {
619
+ const title = " swarm ";
620
+ const mode = " interactive ";
621
+ const padLen = Math.max(0, w - title.length - mode.length - 4);
622
+ const leftPad = symbols.horizontal.repeat(Math.floor(padLen / 2));
623
+ const rightPad = symbols.horizontal.repeat(Math.ceil(padLen / 2));
624
+ out.write("\n");
625
+ out.write(` ${cyan(`${symbols.topLeft}${leftPad}`)}${bold(coral(title))}${dim(mode)}${cyan(`${rightPad}${symbols.topRight}`)}\n`);
626
+ out.write(` ${cyan(symbols.vertLine)}${" ".repeat(Math.max(0, w - 2))}${cyan(symbols.vertLine)}\n`);
627
+ }
628
+ else {
629
+ out.write("\nswarm interactive\n");
630
+ }
631
+ const kv = (key, val) => {
632
+ out.write(` ${dim(key.padEnd(12))} ${val}\n`);
633
+ };
634
+ kv("Directory", config.dir);
635
+ kv("Model", `${config.model} ${dim(`(${config.provider})`)}`);
636
+ kv("Agent", config.agent);
637
+ kv("Routing", config.routing);
638
+ if (isTTY) {
639
+ out.write(` ${cyan(symbols.vertLine)}${" ".repeat(Math.max(0, w - 2))}${cyan(symbols.vertLine)}\n`);
640
+ out.write(` ${cyan(symbols.bottomLeft)}${cyan(symbols.horizontal.repeat(Math.max(0, w - 2)))}${cyan(symbols.bottomRight)}\n`);
641
+ }
642
+ out.write(`\n ${dim("Type a task to run, or /help for commands.")}\n\n`);
643
+ }
644
+ // ── Main ────────────────────────────────────────────────────────────────────
645
+ export async function runInteractiveSwarm(rawArgs) {
646
+ const args = parseInteractiveArgs(rawArgs);
647
+ const config = loadConfig();
648
+ // Configure UI
649
+ if (args.quiet)
650
+ setLogLevel("quiet");
651
+ else if (args.verbose)
652
+ setLogLevel("verbose");
653
+ // Verify target directory
654
+ if (!fs.existsSync(args.dir)) {
655
+ logError(`Directory "${args.dir}" does not exist`);
656
+ process.exit(1);
657
+ }
658
+ // First-run onboarding
659
+ await runOnboarding();
660
+ // Override config with CLI args
661
+ if (args.agent)
662
+ config.default_agent = args.agent;
663
+ if (args.maxBudget !== null)
664
+ config.max_session_budget_usd = args.maxBudget;
665
+ if (args.autoRoute)
666
+ config.auto_model_selection = true;
667
+ // Resolve orchestrator model
668
+ const resolved = resolveModel(args.orchestratorModel);
669
+ if (!resolved) {
670
+ logError(`Could not find model "${args.orchestratorModel}"`, "Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY in your .env file");
671
+ process.exit(1);
672
+ }
673
+ // Initialize episodic memory and failure tracker
674
+ let episodicMemory;
675
+ if (config.episodic_memory_enabled) {
676
+ episodicMemory = new EpisodicMemory(config.memory_dir);
677
+ await episodicMemory.init();
678
+ }
679
+ const failureTracker = new FailureTracker();
680
+ // Render banner
681
+ renderInteractiveBanner({
682
+ dir: args.dir,
683
+ model: resolved.model.id,
684
+ provider: resolved.provider,
685
+ agent: config.default_agent,
686
+ routing: config.auto_model_selection ? "auto" : "orchestrator-driven",
687
+ });
688
+ // Scan codebase
689
+ const spinner = new Spinner();
690
+ spinner.start("scanning codebase");
691
+ const context = scanDirectory(args.dir);
692
+ spinner.stop();
693
+ logSuccess(`Scanned codebase — ${(context.length / 1024).toFixed(1)}KB context`);
694
+ // Start REPL and thread infrastructure
695
+ const repl = new PythonRepl();
696
+ const sessionAc = new AbortController();
697
+ const dashboard = new ThreadDashboard();
698
+ const threadProgress = (threadId, phase, detail) => {
699
+ if (phase === "completed" || phase === "failed" || phase === "cancelled") {
700
+ dashboard.complete(threadId, phase, detail);
701
+ }
702
+ else {
703
+ dashboard.update(threadId, phase, detail);
704
+ }
705
+ };
706
+ // Enable OpenCode server mode
707
+ if (config.default_agent === "opencode" && config.opencode_server_mode) {
708
+ opencodeMod.enableServerMode();
709
+ logVerbose("OpenCode server mode enabled");
710
+ }
711
+ // Initialize thread manager
712
+ const threadManager = new ThreadManager(args.dir, config, threadProgress, sessionAc.signal);
713
+ await threadManager.init();
714
+ if (episodicMemory) {
715
+ threadManager.setEpisodicMemory(episodicMemory);
716
+ }
717
+ // Register LLM summarizer if needed
718
+ if (config.compression_strategy === "llm-summary") {
719
+ const { setSummarizer } = await import("./compression/compressor.js");
720
+ const { completeSimple } = await import("@mariozechner/pi-ai");
721
+ setSummarizer(async (text, instruction) => {
722
+ const response = await completeSimple(resolved.model, {
723
+ systemPrompt: instruction,
724
+ messages: [
725
+ {
726
+ role: "user",
727
+ content: text,
728
+ timestamp: Date.now(),
729
+ },
730
+ ],
731
+ });
732
+ return response.content
733
+ .filter((b) => b.type === "text")
734
+ .map((b) => b.text)
735
+ .join("");
736
+ });
737
+ }
738
+ // Build system prompt
739
+ const agentDesc = await describeAvailableAgents();
740
+ let systemPrompt = buildSwarmSystemPrompt(config, agentDesc);
741
+ // Add episodic memory hints for general context
742
+ if (episodicMemory && episodicMemory.size > 0) {
743
+ const hints = episodicMemory.getStrategyHints("general coding tasks");
744
+ if (hints) {
745
+ systemPrompt += `\n\n## Episodic Memory\n${hints}\nConsider these strategies when decomposing your task.`;
746
+ }
747
+ }
748
+ // Session state
749
+ const sessionStartTime = Date.now();
750
+ let taskCount = 0;
751
+ // Start Python REPL
752
+ await repl.start(sessionAc.signal);
753
+ // Create readline interface
754
+ const rl = readline.createInterface({
755
+ input: process.stdin,
756
+ output: process.stderr,
757
+ prompt: isTTY ? ` ${coral("swarm")}${dim(">")} ` : "swarm> ",
758
+ terminal: isTTY,
759
+ });
760
+ // SIGINT handling — first press cancels current task, second exits
761
+ let currentTaskAc = null;
762
+ let sigintCount = 0;
763
+ let cleanupCalled = false;
764
+ // Forward declarations for mutual references
765
+ function handleSigint() {
766
+ sigintCount++;
767
+ if (sigintCount === 1 && currentTaskAc) {
768
+ // Cancel current task
769
+ process.stderr.write(`\n ${yellow("Cancelling current task...")} ${dim("(press Ctrl+C again to exit)")}\n`);
770
+ currentTaskAc.abort();
771
+ currentTaskAc = null;
772
+ }
773
+ else if (sigintCount >= 2) {
774
+ // Force exit
775
+ process.stderr.write(`\n ${yellow("Exiting...")}\n`);
776
+ cleanup();
777
+ }
778
+ else {
779
+ // No task running, treat as exit warning
780
+ process.stderr.write(`\n ${dim("Press Ctrl+C again to exit, or type /quit")}\n`);
781
+ sigintCount = 1;
782
+ // Reset after 2 seconds
783
+ setTimeout(() => {
784
+ sigintCount = 0;
785
+ }, 2000);
786
+ }
787
+ }
788
+ async function cleanup() {
789
+ if (cleanupCalled)
790
+ return;
791
+ cleanupCalled = true;
792
+ rl.close();
793
+ spinner.stop();
794
+ dashboard.clear();
795
+ process.removeListener("SIGINT", handleSigint);
796
+ sessionAc.abort();
797
+ repl.shutdown();
798
+ await threadManager.cleanup();
799
+ await opencodeMod.disableServerMode();
800
+ process.exit(0);
801
+ }
802
+ process.on("SIGINT", handleSigint);
803
+ // Thread handler (reused across tasks)
804
+ const threadHandler = async (task, threadContext, agentBackend, model, files) => {
805
+ let resolvedAgent = agentBackend || config.default_agent;
806
+ let resolvedModel = model || config.default_model;
807
+ let routeSlot = "";
808
+ let routeComplexity = "";
809
+ if (config.auto_model_selection && !agentBackend && !model) {
810
+ const route = await routeTask(task, config, episodicMemory, failureTracker);
811
+ resolvedAgent = route.agent;
812
+ resolvedModel = route.model;
813
+ routeSlot = route.slot;
814
+ routeComplexity = classifyTaskComplexity(task);
815
+ logRouter(`${route.reason} [slot: ${route.slot}]`);
816
+ }
817
+ const threadId = randomBytes(6).toString("hex");
818
+ dashboard.update(threadId, "queued", undefined, {
819
+ task,
820
+ agent: resolvedAgent,
821
+ model: resolvedModel,
822
+ });
823
+ const result = await threadManager.spawnThread({
824
+ id: threadId,
825
+ task,
826
+ context: threadContext,
827
+ agent: {
828
+ backend: resolvedAgent,
829
+ model: resolvedModel,
830
+ },
831
+ files,
832
+ });
833
+ // Record episode or failure
834
+ if (result.success) {
835
+ if (episodicMemory && routeSlot) {
836
+ episodicMemory
837
+ .record({
838
+ task,
839
+ agent: resolvedAgent,
840
+ model: resolvedModel,
841
+ slot: routeSlot,
842
+ complexity: routeComplexity,
843
+ success: true,
844
+ durationMs: result.durationMs,
845
+ estimatedCostUsd: result.estimatedCostUsd,
846
+ filesChanged: result.filesChanged,
847
+ summary: result.summary,
848
+ })
849
+ .catch(() => { });
850
+ }
851
+ }
852
+ else {
853
+ failureTracker.recordFailure(resolvedAgent, resolvedModel, task, result.summary || "unknown error");
854
+ }
855
+ return {
856
+ result: result.summary,
857
+ success: result.success,
858
+ filesChanged: result.filesChanged,
859
+ durationMs: result.durationMs,
860
+ };
861
+ };
862
+ // Merge handler
863
+ const mergeHandler = async () => {
864
+ spinner.update("merging thread branches");
865
+ const threads = threadManager.getThreads();
866
+ const mergeOpts = { continueOnConflict: true };
867
+ const results = await mergeAllThreads(args.dir, threads, mergeOpts);
868
+ const merged = results.filter((r) => r.success).length;
869
+ const failed = results.filter((r) => !r.success).length;
870
+ if (failed > 0) {
871
+ logWarn(`Merged ${merged} branches, ${failed} failed`);
872
+ }
873
+ else if (merged > 0) {
874
+ logSuccess(`Merged ${merged} branches`);
875
+ }
876
+ const summary = results
877
+ .map((r) => (r.success ? `Merged ${r.branch}: ${r.message}` : `FAILED ${r.branch}: ${r.message}`))
878
+ .join("\n");
879
+ return {
880
+ result: summary || "No threads to merge",
881
+ success: results.every((r) => r.success),
882
+ };
883
+ };
884
+ // Run a task through the RLM loop
885
+ const runTask = async (query) => {
886
+ taskCount++;
887
+ sigintCount = 0;
888
+ currentTaskAc = new AbortController();
889
+ // Link task abort to session abort
890
+ const onSessionAbort = () => currentTaskAc?.abort();
891
+ sessionAc.signal.addEventListener("abort", onSessionAbort, { once: true });
892
+ spinner.start();
893
+ const startTime = Date.now();
894
+ try {
895
+ // Update episodic memory hints per-task
896
+ let taskSystemPrompt = systemPrompt;
897
+ if (episodicMemory && episodicMemory.size > 0) {
898
+ const hints = episodicMemory.getStrategyHints(query);
899
+ if (hints) {
900
+ // Replace existing hints or add new ones
901
+ const memoryIdx = taskSystemPrompt.indexOf("## Episodic Memory");
902
+ if (memoryIdx !== -1) {
903
+ const endIdx = taskSystemPrompt.indexOf("\n## ", memoryIdx + 1);
904
+ const before = taskSystemPrompt.slice(0, memoryIdx);
905
+ const after = endIdx !== -1 ? taskSystemPrompt.slice(endIdx) : "";
906
+ taskSystemPrompt = `${before}## Episodic Memory\n${hints}\nConsider these strategies when decomposing your task.${after}`;
907
+ }
908
+ }
909
+ }
910
+ const result = await runRlmLoop({
911
+ context,
912
+ query,
913
+ model: resolved.model,
914
+ repl,
915
+ signal: currentTaskAc.signal,
916
+ systemPrompt: taskSystemPrompt,
917
+ threadHandler,
918
+ mergeHandler,
919
+ onProgress: (info) => {
920
+ spinner.update(`iteration ${info.iteration}/${info.maxIterations}` +
921
+ (info.subQueries > 0 ? ` · ${info.subQueries} queries` : ""));
922
+ logVerbose(`Iteration ${info.iteration}/${info.maxIterations} | ` +
923
+ `Sub-queries: ${info.subQueries} | Phase: ${info.phase}`);
924
+ },
925
+ });
926
+ spinner.stop();
927
+ dashboard.clear();
928
+ const elapsed = (Date.now() - startTime) / 1000;
929
+ // Show concise result
930
+ process.stderr.write("\n");
931
+ const status = result.completed ? green("completed") : yellow("incomplete");
932
+ process.stderr.write(` ${status} in ${bold(`${elapsed.toFixed(1)}s`)} ${dim(`(${result.iterations} iterations)`)}\n`);
933
+ // Show answer
934
+ if (result.answer) {
935
+ process.stderr.write("\n");
936
+ const lines = result.answer.split("\n");
937
+ for (const line of lines) {
938
+ process.stderr.write(` ${line}\n`);
939
+ }
940
+ }
941
+ process.stderr.write("\n");
942
+ }
943
+ catch (err) {
944
+ spinner.stop();
945
+ dashboard.clear();
946
+ if (currentTaskAc?.signal.aborted) {
947
+ logWarn("Task cancelled");
948
+ }
949
+ else {
950
+ const msg = err instanceof Error ? err.message : String(err);
951
+ logError(`Task failed: ${msg}`);
952
+ }
953
+ }
954
+ finally {
955
+ sessionAc.signal.removeEventListener("abort", onSessionAbort);
956
+ currentTaskAc = null;
957
+ sigintCount = 0;
958
+ }
959
+ };
960
+ // Process a line of input
961
+ const processLine = async (line) => {
962
+ const trimmed = line.trim();
963
+ if (!trimmed)
964
+ return false;
965
+ // Parse commands
966
+ if (trimmed.startsWith("/")) {
967
+ const parts = trimmed.split(/\s+/);
968
+ const cmd = parts[0].toLowerCase();
969
+ const cmdArgs = parts.slice(1);
970
+ switch (cmd) {
971
+ case "/help":
972
+ case "/h":
973
+ cmdHelp();
974
+ break;
975
+ case "/threads":
976
+ case "/t":
977
+ cmdThreads(threadManager);
978
+ break;
979
+ case "/thread":
980
+ cmdThread(threadManager, cmdArgs[0] || "");
981
+ break;
982
+ case "/merge":
983
+ case "/m":
984
+ await cmdMerge(threadManager, args.dir, cmdArgs);
985
+ break;
986
+ case "/reject":
987
+ case "/r":
988
+ await cmdReject(threadManager, cmdArgs[0] || "");
989
+ break;
990
+ case "/dag":
991
+ case "/d":
992
+ cmdDag(threadManager);
993
+ break;
994
+ case "/budget":
995
+ case "/b":
996
+ cmdBudget(threadManager);
997
+ break;
998
+ case "/status":
999
+ case "/s":
1000
+ cmdStatus(threadManager, sessionStartTime, taskCount);
1001
+ break;
1002
+ case "/quit":
1003
+ case "/exit":
1004
+ case "/q":
1005
+ return true; // Signal exit
1006
+ default:
1007
+ logWarn(`Unknown command: ${cmd}. Type /help for available commands.`);
1008
+ break;
1009
+ }
1010
+ return false;
1011
+ }
1012
+ // Not a command — run as a task
1013
+ await runTask(trimmed);
1014
+ return false;
1015
+ };
1016
+ // REPL loop
1017
+ rl.prompt();
1018
+ rl.on("line", async (line) => {
1019
+ // Pause readline during processing so prompt doesn't re-appear
1020
+ rl.pause();
1021
+ try {
1022
+ const shouldExit = await processLine(line);
1023
+ if (shouldExit) {
1024
+ await cleanup();
1025
+ return;
1026
+ }
1027
+ }
1028
+ catch (err) {
1029
+ const msg = err instanceof Error ? err.message : String(err);
1030
+ logError(`Unexpected error: ${msg}`);
1031
+ }
1032
+ // Resume and show prompt again
1033
+ rl.resume();
1034
+ rl.prompt();
1035
+ });
1036
+ rl.on("close", async () => {
1037
+ process.stderr.write("\n");
1038
+ await cleanup();
1039
+ });
1040
+ }
1041
+ //# sourceMappingURL=interactive-swarm.js.map