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