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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +182 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +216 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +193 -0
- package/dist/agents/direct-llm.d.ts +9 -0
- package/dist/agents/direct-llm.js +78 -0
- package/dist/agents/mock.d.ts +9 -0
- package/dist/agents/mock.js +77 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +571 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +285 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +265 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +170 -0
- package/dist/core/repl.d.ts +69 -0
- package/dist/core/repl.js +336 -0
- package/dist/core/rlm.d.ts +63 -0
- package/dist/core/rlm.js +409 -0
- package/dist/core/runtime.py +335 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +19 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/interactive-swarm.d.ts +20 -0
- package/dist/interactive-swarm.js +1041 -0
- package/dist/interactive.d.ts +10 -0
- package/dist/interactive.js +1765 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +242 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +72 -0
- package/dist/mcp/session.d.ts +73 -0
- package/dist/mcp/session.js +184 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +377 -0
- package/dist/memory/episodic.d.ts +132 -0
- package/dist/memory/episodic.js +390 -0
- package/dist/prompts/orchestrator.d.ts +5 -0
- package/dist/prompts/orchestrator.js +191 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +557 -0
- package/dist/threads/cache.d.ts +58 -0
- package/dist/threads/cache.js +198 -0
- package/dist/threads/manager.d.ts +85 -0
- package/dist/threads/manager.js +659 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +151 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +11 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +518 -0
- package/dist/ui/spinner.d.ts +25 -0
- package/dist/ui/spinner.js +113 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/viewer.d.ts +12 -0
- package/dist/viewer.js +1284 -0
- package/dist/worktree/manager.d.ts +45 -0
- package/dist/worktree/manager.js +266 -0
- package/dist/worktree/merge.d.ts +28 -0
- package/dist/worktree/merge.js +138 -0
- 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
|