@gaberrb/polypus 0.4.0 → 0.4.1
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/README.md +5 -0
- package/dist/index.js +739 -597
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -143,6 +143,7 @@ var en = {
|
|
|
143
143
|
"repl.allowShow": "mode={mode} allow=[{allow}]",
|
|
144
144
|
"repl.historyCleared": "history cleared",
|
|
145
145
|
"repl.unknown": "Unknown command /{cmd}. Type /help.",
|
|
146
|
+
"repl.pasted": "[Pasted text #{id} +{lines} lines]",
|
|
146
147
|
"repl.agentSwitched": "active agent \u2192 {name}",
|
|
147
148
|
"repl.switchedTo": "active agent is now {name}",
|
|
148
149
|
"repl.noAgentsLeft": "No agents left. Use /add to create one.",
|
|
@@ -156,6 +157,7 @@ var en = {
|
|
|
156
157
|
" /plan switch to plan mode (read-only)",
|
|
157
158
|
" /review switch to review mode (confirm each action)",
|
|
158
159
|
" /bypass switch to bypass mode (auto-approve)",
|
|
160
|
+
" /swarm <task> run a task as a parallel swarm (needs 3+ agents)",
|
|
159
161
|
" /allow <glob> add a path glob to the allow-list",
|
|
160
162
|
" /allow show the current allow-list and mode",
|
|
161
163
|
" /reset clear the conversation history",
|
|
@@ -359,6 +361,7 @@ var ptBR = {
|
|
|
359
361
|
"repl.allowShow": "modo={mode} allow=[{allow}]",
|
|
360
362
|
"repl.historyCleared": "hist\xF3rico limpo",
|
|
361
363
|
"repl.unknown": "Comando desconhecido /{cmd}. Digite /help.",
|
|
364
|
+
"repl.pasted": "[Texto colado #{id} +{lines} linhas]",
|
|
362
365
|
"repl.agentSwitched": "agente ativo \u2192 {name}",
|
|
363
366
|
"repl.switchedTo": "agente ativo agora \xE9 {name}",
|
|
364
367
|
"repl.noAgentsLeft": "Nenhum agente restante. Use /add para criar um.",
|
|
@@ -372,6 +375,7 @@ var ptBR = {
|
|
|
372
375
|
" /plan muda para o modo plan (somente leitura)",
|
|
373
376
|
" /review muda para o modo review (confirma cada a\xE7\xE3o)",
|
|
374
377
|
" /bypass muda para o modo bypass (aprova automaticamente)",
|
|
378
|
+
" /swarm <task> roda a tarefa como swarm paralelo (requer 3+ agentes)",
|
|
375
379
|
" /allow <glob> adiciona um glob de caminho \xE0 allow-list",
|
|
376
380
|
" /allow mostra a allow-list e o modo atuais",
|
|
377
381
|
" /reset limpa o hist\xF3rico da conversa",
|
|
@@ -688,7 +692,7 @@ async function listAgents() {
|
|
|
688
692
|
}
|
|
689
693
|
|
|
690
694
|
// src/cli/commands/run.ts
|
|
691
|
-
import
|
|
695
|
+
import pc8 from "picocolors";
|
|
692
696
|
import * as p2 from "@clack/prompts";
|
|
693
697
|
|
|
694
698
|
// src/core/providers/anthropic.ts
|
|
@@ -1860,8 +1864,6 @@ ${guidance}`;
|
|
|
1860
1864
|
}
|
|
1861
1865
|
|
|
1862
1866
|
// src/ui/repl.ts
|
|
1863
|
-
import * as readline from "readline/promises";
|
|
1864
|
-
import { stdin, stdout } from "process";
|
|
1865
1867
|
import pc6 from "picocolors";
|
|
1866
1868
|
|
|
1867
1869
|
// src/ui/wizard.ts
|
|
@@ -2383,6 +2385,136 @@ function promptLabel(mode) {
|
|
|
2383
2385
|
return c2("\u{1F419} polypus") + pc5.dim(`(${mode})`) + c3(" \u203A ");
|
|
2384
2386
|
}
|
|
2385
2387
|
|
|
2388
|
+
// src/ui/line-reader.ts
|
|
2389
|
+
import * as readline from "readline/promises";
|
|
2390
|
+
import { PassThrough } from "stream";
|
|
2391
|
+
import { stdin, stdout } from "process";
|
|
2392
|
+
|
|
2393
|
+
// src/ui/paste.ts
|
|
2394
|
+
var PASTE_START = "\x1B[200~";
|
|
2395
|
+
var PASTE_END = "\x1B[201~";
|
|
2396
|
+
var PasteStore = class {
|
|
2397
|
+
/** `format(id, lines)` builds the placeholder (localized by the caller). */
|
|
2398
|
+
constructor(format) {
|
|
2399
|
+
this.format = format;
|
|
2400
|
+
}
|
|
2401
|
+
format;
|
|
2402
|
+
seq = 0;
|
|
2403
|
+
map = /* @__PURE__ */ new Map();
|
|
2404
|
+
/** Register pasted text, returning the placeholder to display in its place. */
|
|
2405
|
+
add(text2) {
|
|
2406
|
+
const lines = text2.split(/\r\n|\r|\n/).length;
|
|
2407
|
+
const placeholder = this.format(++this.seq, lines);
|
|
2408
|
+
this.map.set(placeholder, text2);
|
|
2409
|
+
return placeholder;
|
|
2410
|
+
}
|
|
2411
|
+
/** Replace any known placeholders in `line` with their full pasted text. */
|
|
2412
|
+
expand(line) {
|
|
2413
|
+
let out = line;
|
|
2414
|
+
for (const [placeholder, full] of this.map) {
|
|
2415
|
+
if (out.includes(placeholder)) out = out.split(placeholder).join(full);
|
|
2416
|
+
}
|
|
2417
|
+
return out;
|
|
2418
|
+
}
|
|
2419
|
+
get size() {
|
|
2420
|
+
return this.map.size;
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
var PasteFilter = class {
|
|
2424
|
+
constructor(store) {
|
|
2425
|
+
this.store = store;
|
|
2426
|
+
}
|
|
2427
|
+
store;
|
|
2428
|
+
buf = "";
|
|
2429
|
+
inPaste = false;
|
|
2430
|
+
pasteBuf = "";
|
|
2431
|
+
push(chunk) {
|
|
2432
|
+
this.buf += chunk;
|
|
2433
|
+
let out = "";
|
|
2434
|
+
for (; ; ) {
|
|
2435
|
+
if (!this.inPaste) {
|
|
2436
|
+
const i = this.buf.indexOf(PASTE_START);
|
|
2437
|
+
if (i === -1) {
|
|
2438
|
+
const keep = partialSuffix(this.buf, PASTE_START);
|
|
2439
|
+
out += this.buf.slice(0, this.buf.length - keep);
|
|
2440
|
+
this.buf = this.buf.slice(this.buf.length - keep);
|
|
2441
|
+
return out;
|
|
2442
|
+
}
|
|
2443
|
+
out += this.buf.slice(0, i);
|
|
2444
|
+
this.buf = this.buf.slice(i + PASTE_START.length);
|
|
2445
|
+
this.inPaste = true;
|
|
2446
|
+
} else {
|
|
2447
|
+
const j = this.buf.indexOf(PASTE_END);
|
|
2448
|
+
if (j === -1) {
|
|
2449
|
+
const keep = partialSuffix(this.buf, PASTE_END);
|
|
2450
|
+
this.pasteBuf += this.buf.slice(0, this.buf.length - keep);
|
|
2451
|
+
this.buf = this.buf.slice(this.buf.length - keep);
|
|
2452
|
+
return out;
|
|
2453
|
+
}
|
|
2454
|
+
this.pasteBuf += this.buf.slice(0, j);
|
|
2455
|
+
this.buf = this.buf.slice(j + PASTE_END.length);
|
|
2456
|
+
this.inPaste = false;
|
|
2457
|
+
out += this.emit(this.pasteBuf);
|
|
2458
|
+
this.pasteBuf = "";
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
/** Multi-line pastes become a placeholder; single-line pastes pass through. */
|
|
2463
|
+
emit(text2) {
|
|
2464
|
+
return /\r|\n/.test(text2) ? this.store.add(text2) : text2;
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
function partialSuffix(s, marker) {
|
|
2468
|
+
const max = Math.min(s.length, marker.length - 1);
|
|
2469
|
+
for (let n = max; n > 0; n--) {
|
|
2470
|
+
if (s.slice(s.length - n) === marker.slice(0, n)) return n;
|
|
2471
|
+
}
|
|
2472
|
+
return 0;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// src/ui/line-reader.ts
|
|
2476
|
+
var ENABLE_BRACKETED_PASTE = "\x1B[?2004h";
|
|
2477
|
+
var DISABLE_BRACKETED_PASTE = "\x1B[?2004l";
|
|
2478
|
+
async function readLine(prompt) {
|
|
2479
|
+
if (!stdin.isTTY) {
|
|
2480
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
2481
|
+
try {
|
|
2482
|
+
return await rl.question(prompt);
|
|
2483
|
+
} catch {
|
|
2484
|
+
return null;
|
|
2485
|
+
} finally {
|
|
2486
|
+
rl.close();
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
return readLineTTY(prompt);
|
|
2490
|
+
}
|
|
2491
|
+
async function readLineTTY(prompt) {
|
|
2492
|
+
const store = new PasteStore((id, lines) => t("repl.pasted", { id, lines }));
|
|
2493
|
+
const filter = new PasteFilter(store);
|
|
2494
|
+
const proxy = new PassThrough();
|
|
2495
|
+
const rl = readline.createInterface({ input: proxy, output: stdout, terminal: true });
|
|
2496
|
+
const onData = (buf) => {
|
|
2497
|
+
proxy.write(filter.push(buf.toString("utf8")));
|
|
2498
|
+
};
|
|
2499
|
+
stdout.write(ENABLE_BRACKETED_PASTE);
|
|
2500
|
+
stdin.setRawMode(true);
|
|
2501
|
+
stdin.resume();
|
|
2502
|
+
stdin.on("data", onData);
|
|
2503
|
+
try {
|
|
2504
|
+
const line = await new Promise((resolve8) => {
|
|
2505
|
+
rl.question(prompt).then(resolve8, () => resolve8(null));
|
|
2506
|
+
rl.on("SIGINT", () => resolve8(null));
|
|
2507
|
+
rl.on("close", () => resolve8(null));
|
|
2508
|
+
});
|
|
2509
|
+
return line === null ? null : store.expand(line);
|
|
2510
|
+
} finally {
|
|
2511
|
+
stdin.off("data", onData);
|
|
2512
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
2513
|
+
stdout.write(DISABLE_BRACKETED_PASTE);
|
|
2514
|
+
rl.close();
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2386
2518
|
// src/ui/repl.ts
|
|
2387
2519
|
async function startRepl(ctx) {
|
|
2388
2520
|
for (; ; ) {
|
|
@@ -2412,16 +2544,6 @@ async function startRepl(ctx) {
|
|
|
2412
2544
|
await handleCommand(cmd, arg, ctx);
|
|
2413
2545
|
}
|
|
2414
2546
|
}
|
|
2415
|
-
async function readLine(prompt) {
|
|
2416
|
-
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
2417
|
-
try {
|
|
2418
|
-
return await rl.question(prompt);
|
|
2419
|
-
} catch {
|
|
2420
|
-
return null;
|
|
2421
|
-
} finally {
|
|
2422
|
-
rl.close();
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
2547
|
async function handleCommand(cmd, arg, ctx) {
|
|
2426
2548
|
const { session } = ctx;
|
|
2427
2549
|
switch (cmd) {
|
|
@@ -2446,6 +2568,18 @@ async function handleCommand(cmd, arg, ctx) {
|
|
|
2446
2568
|
session.history = [];
|
|
2447
2569
|
console.log(pc6.dim(t("repl.historyCleared")));
|
|
2448
2570
|
return;
|
|
2571
|
+
case "swarm": {
|
|
2572
|
+
if (!arg) {
|
|
2573
|
+
console.log(pc6.yellow(t("repl.needName", { usage: "/swarm <task>" })));
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
try {
|
|
2577
|
+
await ctx.runSwarm(arg);
|
|
2578
|
+
} catch (e) {
|
|
2579
|
+
console.log(pc6.red(`\u2717 ${e.message}`));
|
|
2580
|
+
}
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2449
2583
|
case "agents":
|
|
2450
2584
|
printAgents(ctx.getConfig(), session.agentName);
|
|
2451
2585
|
return;
|
|
@@ -2509,150 +2643,562 @@ async function removeAgent2(name, ctx) {
|
|
|
2509
2643
|
}
|
|
2510
2644
|
}
|
|
2511
2645
|
|
|
2512
|
-
// src/
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
/** Extra dim text appended after the elapsed time (e.g. token count). */
|
|
2525
|
-
setSuffix(suffix) {
|
|
2526
|
-
this.suffix = suffix;
|
|
2646
|
+
// src/cli/commands/swarm.ts
|
|
2647
|
+
import pc7 from "picocolors";
|
|
2648
|
+
|
|
2649
|
+
// src/core/git/worktree.ts
|
|
2650
|
+
import { mkdtemp } from "fs/promises";
|
|
2651
|
+
import { tmpdir } from "os";
|
|
2652
|
+
import { join as join3 } from "path";
|
|
2653
|
+
import { simpleGit } from "simple-git";
|
|
2654
|
+
async function ensureRepo(workspace) {
|
|
2655
|
+
const git = simpleGit(workspace);
|
|
2656
|
+
if (!await git.checkIsRepo()) {
|
|
2657
|
+
await git.init();
|
|
2527
2658
|
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
if (this.timer) return;
|
|
2533
|
-
this.startedAt = Date.now();
|
|
2534
|
-
this.render();
|
|
2535
|
-
this.timer = setInterval(() => this.render(), 90);
|
|
2536
|
-
this.timer.unref?.();
|
|
2659
|
+
const identity = await identityArgs(git);
|
|
2660
|
+
const hasHead = await git.raw(["rev-parse", "--verify", "HEAD"]).then(() => true).catch(() => false);
|
|
2661
|
+
if (!hasHead) {
|
|
2662
|
+
await git.raw([...identity, "commit", "--allow-empty", "-m", "polypus: initial commit"]);
|
|
2537
2663
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2664
|
+
return git;
|
|
2665
|
+
}
|
|
2666
|
+
async function identityArgs(git) {
|
|
2667
|
+
const email = await git.raw(["config", "user.email"]).catch(() => "");
|
|
2668
|
+
if (email.trim()) return [];
|
|
2669
|
+
return ["-c", "user.email=polypus@local", "-c", "user.name=Polypus"];
|
|
2670
|
+
}
|
|
2671
|
+
async function createWorktree(git, label) {
|
|
2672
|
+
const branch = `polypus/${label}-${Date.now().toString(36)}`;
|
|
2673
|
+
const path = await mkdtemp(join3(tmpdir(), "polypus-wt-"));
|
|
2674
|
+
await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
|
|
2675
|
+
return { path, branch };
|
|
2676
|
+
}
|
|
2677
|
+
async function commitWorktree(wt, message) {
|
|
2678
|
+
const wtGit = simpleGit(wt.path);
|
|
2679
|
+
await wtGit.add(["-A"]);
|
|
2680
|
+
const status = await wtGit.status();
|
|
2681
|
+
if (status.staged.length === 0 && status.files.length === 0) return false;
|
|
2682
|
+
const identity = await identityArgs(wtGit);
|
|
2683
|
+
await wtGit.raw([...identity, "commit", "-m", message]);
|
|
2684
|
+
return true;
|
|
2685
|
+
}
|
|
2686
|
+
async function mergeWorktreeBranch(git, branch) {
|
|
2687
|
+
try {
|
|
2688
|
+
const identity = await identityArgs(git);
|
|
2689
|
+
await git.raw([...identity, "merge", "--no-edit", branch]);
|
|
2690
|
+
return { branch, ok: true, conflicts: [] };
|
|
2691
|
+
} catch (err) {
|
|
2692
|
+
const status = await git.status().catch(() => void 0);
|
|
2693
|
+
const conflicts = status?.conflicted ?? [];
|
|
2694
|
+
await git.raw(["merge", "--abort"]).catch(() => void 0);
|
|
2695
|
+
if (conflicts.length === 0) {
|
|
2696
|
+
throw err;
|
|
2543
2697
|
}
|
|
2544
|
-
|
|
2545
|
-
}
|
|
2546
|
-
render() {
|
|
2547
|
-
const f = violet(FRAMES[this.frame = (this.frame + 1) % FRAMES.length]);
|
|
2548
|
-
const secs = Math.floor((Date.now() - this.startedAt) / 1e3);
|
|
2549
|
-
const time = secs > 0 ? dim(` (${secs}s)`) : "";
|
|
2550
|
-
const suffix = this.suffix ? dim(` \xB7 ${this.suffix}`) : "";
|
|
2551
|
-
process.stdout.write(`\r\x1B[K${f} \u{1F419} ${dim(this.label + "\u2026")}${time}${suffix}`);
|
|
2698
|
+
return { branch, ok: false, conflicts };
|
|
2552
2699
|
}
|
|
2553
|
-
}
|
|
2700
|
+
}
|
|
2701
|
+
async function removeWorktree(git, wt) {
|
|
2702
|
+
await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
|
|
2703
|
+
await git.raw(["branch", "-D", wt.branch]).catch(() => void 0);
|
|
2704
|
+
}
|
|
2554
2705
|
|
|
2555
|
-
// src/
|
|
2556
|
-
async function
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
agentName: agentConfig.name,
|
|
2562
|
-
mode: opts.mode ?? config.permissions.mode,
|
|
2563
|
-
allow: config.permissions.allow,
|
|
2564
|
-
deny: config.permissions.deny,
|
|
2565
|
-
allowedCommands: config.permissions.allowedCommands,
|
|
2566
|
-
maxSteps: opts.maxSteps ? Number(opts.maxSteps) : void 0,
|
|
2567
|
-
history: []
|
|
2568
|
-
};
|
|
2569
|
-
const runTask = async (taskText) => {
|
|
2570
|
-
const active = resolveAgent(config, session.agentName);
|
|
2571
|
-
const resolved2 = createProvider(active);
|
|
2572
|
-
await executeTask(taskText, resolved2, workspace, session);
|
|
2573
|
-
};
|
|
2574
|
-
if (task) {
|
|
2575
|
-
const resolved2 = createProvider(agentConfig);
|
|
2576
|
-
console.log(
|
|
2577
|
-
pc7.dim(
|
|
2578
|
-
t("run.status", {
|
|
2579
|
-
name: resolved2.config.name,
|
|
2580
|
-
provider: resolved2.config.provider,
|
|
2581
|
-
model: resolved2.config.model,
|
|
2582
|
-
toolMode: resolved2.toolMode,
|
|
2583
|
-
mode: session.mode
|
|
2584
|
-
})
|
|
2585
|
-
)
|
|
2586
|
-
);
|
|
2587
|
-
await executeTask(task, resolved2, workspace, session);
|
|
2588
|
-
return;
|
|
2589
|
-
}
|
|
2590
|
-
const resolved = createProvider(agentConfig);
|
|
2591
|
-
await printWelcome({
|
|
2592
|
-
agentName: resolved.config.name,
|
|
2593
|
-
provider: resolved.config.provider,
|
|
2594
|
-
model: resolved.config.model,
|
|
2595
|
-
toolMode: resolved.toolMode,
|
|
2596
|
-
mode: session.mode,
|
|
2597
|
-
workspace
|
|
2706
|
+
// src/core/agent/worker.ts
|
|
2707
|
+
async function runWorker(subtask, agent, wt, allow, deny, events) {
|
|
2708
|
+
const permissions = new PermissionEngine({
|
|
2709
|
+
mode: "bypass",
|
|
2710
|
+
policy: { workspace: wt.path, allow, deny },
|
|
2711
|
+
allowedCommands: []
|
|
2598
2712
|
});
|
|
2599
|
-
const
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2713
|
+
const result = await runAgent({
|
|
2714
|
+
task: subtask.brief,
|
|
2715
|
+
workspace: wt.path,
|
|
2716
|
+
agent,
|
|
2717
|
+
permissions,
|
|
2718
|
+
promptContext: { workspace: wt.path, mode: "bypass", allow, briefing: subtask.brief },
|
|
2719
|
+
events
|
|
2720
|
+
});
|
|
2721
|
+
const committed = await commitWorktree(wt, `polypus(${subtask.id}): ${subtask.title}`);
|
|
2722
|
+
return {
|
|
2723
|
+
subtask,
|
|
2724
|
+
agentName: agent.config.name,
|
|
2725
|
+
branch: wt.branch,
|
|
2726
|
+
finished: result.finished,
|
|
2727
|
+
summary: result.summary,
|
|
2728
|
+
committed,
|
|
2729
|
+
steps: result.steps
|
|
2606
2730
|
};
|
|
2607
|
-
await startRepl(ctx);
|
|
2608
2731
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
const
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2732
|
+
|
|
2733
|
+
// src/core/agent/orchestrator.ts
|
|
2734
|
+
async function runSwarm(opts) {
|
|
2735
|
+
const lead = opts.agents[0];
|
|
2736
|
+
if (!lead) throw new Error("Swarm requires at least one agent.");
|
|
2737
|
+
const maxSubtasks = opts.maxSubtasks ?? Math.max(opts.agents.length, 2);
|
|
2738
|
+
const git = await ensureRepo(opts.workspace);
|
|
2739
|
+
const subtasks = await decompose(lead, opts.task, maxSubtasks);
|
|
2740
|
+
opts.events?.onDecomposed?.(subtasks);
|
|
2741
|
+
const worktrees = [];
|
|
2742
|
+
for (const subtask of subtasks) {
|
|
2743
|
+
worktrees.push(await createWorktree(git, subtask.id));
|
|
2744
|
+
}
|
|
2745
|
+
const outcomes = await Promise.all(
|
|
2746
|
+
subtasks.map(async (subtask, i) => {
|
|
2747
|
+
const agent = opts.agents[i % opts.agents.length];
|
|
2748
|
+
const wt = worktrees[i];
|
|
2749
|
+
opts.events?.onWorkerStart?.(subtask, agent.config.name);
|
|
2750
|
+
const outcome = await runWorker(
|
|
2751
|
+
subtask,
|
|
2752
|
+
agent,
|
|
2753
|
+
wt,
|
|
2754
|
+
opts.allow,
|
|
2755
|
+
opts.deny,
|
|
2756
|
+
opts.events?.workerEvents?.(subtask)
|
|
2757
|
+
);
|
|
2758
|
+
opts.events?.onWorkerDone?.(outcome);
|
|
2759
|
+
return outcome;
|
|
2760
|
+
})
|
|
2761
|
+
);
|
|
2762
|
+
const merges = [];
|
|
2763
|
+
for (const outcome of outcomes) {
|
|
2764
|
+
if (!outcome.committed) continue;
|
|
2765
|
+
const merge = await mergeWorktreeBranch(git, outcome.branch);
|
|
2766
|
+
merges.push(merge);
|
|
2767
|
+
opts.events?.onMerge?.(merge);
|
|
2768
|
+
}
|
|
2769
|
+
const conflicted = new Set(merges.filter((m) => !m.ok).map((m) => m.branch));
|
|
2770
|
+
for (const wt of worktrees) {
|
|
2771
|
+
if (conflicted.has(wt.branch)) {
|
|
2772
|
+
await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
|
|
2773
|
+
} else {
|
|
2774
|
+
await removeWorktree(git, wt);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
return { subtasks, outcomes, merges };
|
|
2778
|
+
}
|
|
2779
|
+
var DECOMPOSE_SYSTEM = [
|
|
2780
|
+
"You are a tech lead splitting a coding task into independent subtasks that can be done in parallel.",
|
|
2781
|
+
'Return ONLY a JSON array. Each item: {"title": string, "brief": string}.',
|
|
2782
|
+
"Make subtasks touch DIFFERENT files/areas to minimize merge conflicts.",
|
|
2783
|
+
"Keep the list small (prefer 2-4 items). Each brief must be self-contained and actionable."
|
|
2784
|
+
].join("\n");
|
|
2785
|
+
async function decompose(lead, task, maxSubtasks) {
|
|
2786
|
+
try {
|
|
2787
|
+
const res = await lead.provider.chat({
|
|
2788
|
+
messages: [
|
|
2789
|
+
{ role: "system", content: DECOMPOSE_SYSTEM },
|
|
2790
|
+
{ role: "user", content: `Task:
|
|
2791
|
+
${task}
|
|
2792
|
+
|
|
2793
|
+
Return at most ${maxSubtasks} subtasks as a JSON array.` }
|
|
2794
|
+
],
|
|
2795
|
+
params: { temperature: 0 }
|
|
2796
|
+
});
|
|
2797
|
+
const parsed = extractJsonArray(res.content);
|
|
2798
|
+
if (parsed && parsed.length > 0) {
|
|
2799
|
+
return parsed.slice(0, maxSubtasks).map((item, i) => ({
|
|
2800
|
+
id: `t${i + 1}`,
|
|
2801
|
+
title: String(item.title ?? `subtask ${i + 1}`),
|
|
2802
|
+
brief: String(item.brief ?? item.title ?? task)
|
|
2803
|
+
}));
|
|
2804
|
+
}
|
|
2805
|
+
} catch {
|
|
2806
|
+
}
|
|
2807
|
+
return [{ id: "t1", title: "task", brief: task }];
|
|
2808
|
+
}
|
|
2809
|
+
function extractJsonArray(text2) {
|
|
2810
|
+
const start = text2.indexOf("[");
|
|
2811
|
+
const end = text2.lastIndexOf("]");
|
|
2812
|
+
if (start === -1 || end <= start) return null;
|
|
2813
|
+
try {
|
|
2814
|
+
const parsed = JSON.parse(text2.slice(start, end + 1));
|
|
2815
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
2816
|
+
} catch {
|
|
2817
|
+
return null;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/ui/swarm-view.ts
|
|
2822
|
+
var RESET2 = "\x1B[0m";
|
|
2823
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2824
|
+
function describeToolCall(call) {
|
|
2825
|
+
const raw = call.name === "run_command" ? call.arguments.command : call.arguments.path;
|
|
2826
|
+
const arg = typeof raw === "string" ? raw : "";
|
|
2827
|
+
const short = arg.length > 40 ? arg.slice(0, 39) + "\u2026" : arg;
|
|
2828
|
+
return short ? `${call.name} ${short}` : call.name;
|
|
2829
|
+
}
|
|
2830
|
+
var SwarmView = class {
|
|
2831
|
+
constructor(leadName, opts = {}) {
|
|
2832
|
+
this.leadName = leadName;
|
|
2833
|
+
this.tty = opts.tty ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
|
|
2834
|
+
this.color = opts.color ?? this.tty;
|
|
2835
|
+
this.write = opts.sink ?? ((s) => process.stdout.write(s));
|
|
2836
|
+
}
|
|
2837
|
+
leadName;
|
|
2838
|
+
tty;
|
|
2839
|
+
color;
|
|
2840
|
+
write;
|
|
2841
|
+
workers = /* @__PURE__ */ new Map();
|
|
2842
|
+
order = [];
|
|
2843
|
+
phase = "decomposing";
|
|
2844
|
+
frame = 0;
|
|
2845
|
+
lastLines = 0;
|
|
2846
|
+
timer;
|
|
2847
|
+
start() {
|
|
2848
|
+
if (!this.tty) {
|
|
2849
|
+
this.write(`\u{1F419} ${t("swarm.view.header", { lead: this.leadName })} \u2014 ${t("swarm.view.decomposing")}
|
|
2850
|
+
`);
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
this.flush();
|
|
2854
|
+
this.timer = setInterval(() => {
|
|
2855
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
2856
|
+
this.flush();
|
|
2857
|
+
}, 110);
|
|
2858
|
+
this.timer.unref?.();
|
|
2859
|
+
}
|
|
2860
|
+
setSubtasks(subtasks) {
|
|
2861
|
+
this.phase = "running";
|
|
2862
|
+
for (const s of subtasks) {
|
|
2863
|
+
this.workers.set(s.id, { id: s.id, title: s.title, agent: "", status: "pending", action: "", steps: 0 });
|
|
2864
|
+
this.order.push(s.id);
|
|
2865
|
+
}
|
|
2866
|
+
if (!this.tty) {
|
|
2867
|
+
this.write(` ${t("swarm.decomposed", { n: subtasks.length })}
|
|
2868
|
+
`);
|
|
2869
|
+
for (const s of subtasks) this.write(` ${s.id}: ${s.title}
|
|
2870
|
+
`);
|
|
2871
|
+
}
|
|
2872
|
+
this.flush();
|
|
2873
|
+
}
|
|
2874
|
+
workerStart(id, agent) {
|
|
2875
|
+
const w = this.workers.get(id);
|
|
2876
|
+
if (!w) return;
|
|
2877
|
+
w.agent = agent;
|
|
2878
|
+
w.status = "running";
|
|
2879
|
+
if (!this.tty) this.write(` \u25B6 ${id} [${agent}] ${w.title}
|
|
2880
|
+
`);
|
|
2881
|
+
this.flush();
|
|
2882
|
+
}
|
|
2883
|
+
workerAction(id, action) {
|
|
2884
|
+
const w = this.workers.get(id);
|
|
2885
|
+
if (!w) return;
|
|
2886
|
+
w.action = action;
|
|
2887
|
+
this.flush();
|
|
2888
|
+
}
|
|
2889
|
+
workerStep(id, n) {
|
|
2890
|
+
const w = this.workers.get(id);
|
|
2891
|
+
if (!w) return;
|
|
2892
|
+
w.steps = n;
|
|
2893
|
+
this.flush();
|
|
2894
|
+
}
|
|
2895
|
+
workerDone(o) {
|
|
2896
|
+
const w = this.workers.get(o.subtask.id);
|
|
2897
|
+
if (!w) return;
|
|
2898
|
+
w.status = o.finished ? "done" : "stopped";
|
|
2899
|
+
w.steps = o.steps;
|
|
2900
|
+
w.branch = o.branch;
|
|
2901
|
+
w.action = "";
|
|
2902
|
+
if (!this.tty) {
|
|
2903
|
+
const tag = o.finished ? "\u2713" : "\u25A0";
|
|
2904
|
+
const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
|
|
2905
|
+
this.write(` ${tag} ${o.subtask.id} (${t("swarm.view.steps", { n: o.steps })}, ${changes})
|
|
2906
|
+
`);
|
|
2907
|
+
}
|
|
2908
|
+
this.flush();
|
|
2909
|
+
}
|
|
2910
|
+
merge(r) {
|
|
2911
|
+
for (const w of this.workers.values()) {
|
|
2912
|
+
if (w.branch === r.branch) w.merge = r.ok ? "ok" : "conflict";
|
|
2913
|
+
}
|
|
2914
|
+
if (!this.tty) {
|
|
2915
|
+
this.write(r.ok ? ` \u2935 ${t("swarm.merged", { branch: r.branch })}
|
|
2916
|
+
` : ` \u2717 ${t("swarm.mergeConflict", { branch: r.branch })}
|
|
2917
|
+
`);
|
|
2918
|
+
}
|
|
2919
|
+
this.flush();
|
|
2920
|
+
}
|
|
2921
|
+
stop() {
|
|
2922
|
+
this.phase = "done";
|
|
2923
|
+
if (this.timer) {
|
|
2924
|
+
clearInterval(this.timer);
|
|
2925
|
+
this.timer = void 0;
|
|
2926
|
+
}
|
|
2927
|
+
this.flush();
|
|
2928
|
+
}
|
|
2929
|
+
/** Content lines of the dashboard (no cursor control). Exposed for tests. */
|
|
2930
|
+
frameLines() {
|
|
2931
|
+
const spin = this.dim(FRAMES[this.frame]);
|
|
2932
|
+
const lead = `\u{1F419} ${t("swarm.view.header", { lead: this.leadName })}`;
|
|
2933
|
+
const lines = [];
|
|
2934
|
+
if (this.phase === "decomposing") {
|
|
2935
|
+
lines.push(`${spin} ${lead}`);
|
|
2936
|
+
lines.push(" " + this.dim(t("swarm.view.decomposing")));
|
|
2937
|
+
return lines;
|
|
2938
|
+
}
|
|
2939
|
+
lines.push(`${this.phase === "running" ? spin : " "} ${lead}`);
|
|
2940
|
+
lines.push("");
|
|
2941
|
+
for (const id of this.order) {
|
|
2942
|
+
const w = this.workers.get(id);
|
|
2943
|
+
lines.push(this.row(w, spin));
|
|
2944
|
+
}
|
|
2945
|
+
return lines;
|
|
2946
|
+
}
|
|
2947
|
+
// -------------------------------------------------------------------------
|
|
2948
|
+
row(w, spin) {
|
|
2949
|
+
const icon = w.status === "running" ? spin : w.status === "done" ? this.c("\u2713", "32") : w.status === "stopped" ? this.c("\u25A0", "33") : this.dim("\xB7");
|
|
2950
|
+
const status = this.statusLabel(w);
|
|
2951
|
+
const meta = w.steps > 0 ? this.dim(" \xB7 " + (w.status === "running" ? t("swarm.view.step", { n: w.steps }) : t("swarm.view.steps", { n: w.steps }))) : "";
|
|
2952
|
+
const action = w.action ? w.action : this.dim("\u2014");
|
|
2953
|
+
return ` ${icon} ${pad(w.id, 4)} ${pad(status, 12)} ${pad(`[${w.agent}]`, 14)} ${action}${meta}`;
|
|
2954
|
+
}
|
|
2955
|
+
statusLabel(w) {
|
|
2956
|
+
if (w.merge === "conflict") return this.c(t("swarm.view.conflict"), "31");
|
|
2957
|
+
if (w.status === "running") return this.c(t("swarm.view.running"), "36");
|
|
2958
|
+
if (w.status === "done") return this.c(t("swarm.view.done"), "32");
|
|
2959
|
+
if (w.status === "stopped") return this.c(t("swarm.view.stopped"), "33");
|
|
2960
|
+
return this.dim(t("swarm.view.pending"));
|
|
2961
|
+
}
|
|
2962
|
+
/** Redraw the block in place (TTY) by clearing the previous frame first. */
|
|
2963
|
+
flush() {
|
|
2964
|
+
if (!this.tty) return;
|
|
2965
|
+
const lines = this.frameLines();
|
|
2966
|
+
let s = "";
|
|
2967
|
+
if (this.lastLines > 0) s += `\x1B[${this.lastLines}A`;
|
|
2968
|
+
s += "\x1B[0J";
|
|
2969
|
+
s += lines.join("\n") + "\n";
|
|
2970
|
+
this.write(s);
|
|
2971
|
+
this.lastLines = lines.length;
|
|
2972
|
+
}
|
|
2973
|
+
c(s, code) {
|
|
2974
|
+
return this.color ? `\x1B[${code}m${s}${RESET2}` : s;
|
|
2975
|
+
}
|
|
2976
|
+
dim(s) {
|
|
2977
|
+
return this.color ? `\x1B[2m${s}${RESET2}` : s;
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
function pad(s, n) {
|
|
2981
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// src/cli/commands/swarm.ts
|
|
2985
|
+
var MIN_SWARM_AGENTS = 3;
|
|
2986
|
+
function canSwarm(agentCount) {
|
|
2987
|
+
return agentCount >= MIN_SWARM_AGENTS;
|
|
2988
|
+
}
|
|
2989
|
+
async function runSwarmSession(task, config, opts = {}) {
|
|
2990
|
+
if (!canSwarm(config.agents.length)) {
|
|
2991
|
+
throw new Error(t("swarm.needsAgents", { min: MIN_SWARM_AGENTS, have: config.agents.length }));
|
|
2992
|
+
}
|
|
2993
|
+
const workspace = opts.workspace ?? process.cwd();
|
|
2994
|
+
const selected = opts.agents?.length ? opts.agents : config.agents.map((a) => a.name);
|
|
2995
|
+
if (selected.length === 0) {
|
|
2996
|
+
throw new Error(t("swarm.noAgents"));
|
|
2997
|
+
}
|
|
2998
|
+
const resolved = selected.map((name) => {
|
|
2999
|
+
const a = config.agents.find((x) => x.name === name);
|
|
3000
|
+
if (!a) throw new Error(t("agent.notFound", { name }));
|
|
3001
|
+
return createProvider(a);
|
|
3002
|
+
});
|
|
3003
|
+
console.log(
|
|
3004
|
+
pc7.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace }))
|
|
3005
|
+
);
|
|
3006
|
+
console.log(pc7.yellow(t("swarm.bypassNote") + "\n"));
|
|
3007
|
+
const view = new SwarmView(resolved[0].config.name);
|
|
3008
|
+
view.start();
|
|
3009
|
+
let result;
|
|
3010
|
+
try {
|
|
3011
|
+
result = await runSwarm({
|
|
3012
|
+
task,
|
|
3013
|
+
workspace,
|
|
3014
|
+
agents: resolved,
|
|
3015
|
+
allow: config.permissions.allow,
|
|
3016
|
+
deny: config.permissions.deny,
|
|
3017
|
+
maxSubtasks: opts.maxSubtasks,
|
|
3018
|
+
events: {
|
|
3019
|
+
onDecomposed: (subtasks) => view.setSubtasks(subtasks),
|
|
3020
|
+
onWorkerStart: (subtask, agentName) => view.workerStart(subtask.id, agentName),
|
|
3021
|
+
onWorkerDone: (outcome) => view.workerDone(outcome),
|
|
3022
|
+
onMerge: (merge) => view.merge(merge),
|
|
3023
|
+
workerEvents: (subtask) => ({
|
|
3024
|
+
onToolCall: (call) => view.workerAction(subtask.id, describeToolCall(call)),
|
|
3025
|
+
onStep: (step) => view.workerStep(subtask.id, step)
|
|
3026
|
+
})
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
} finally {
|
|
3030
|
+
view.stop();
|
|
3031
|
+
}
|
|
3032
|
+
console.log("");
|
|
3033
|
+
console.log(pc7.bold("\n" + t("swarm.summary")));
|
|
3034
|
+
for (const o of result.outcomes) {
|
|
3035
|
+
const status = o.finished ? pc7.green(t("swarm.statusDone")) : pc7.yellow(t("swarm.statusIncomplete"));
|
|
3036
|
+
const committed = o.committed ? "" : pc7.dim(` (${t("swarm.noChanges")})`);
|
|
3037
|
+
console.log(` ${pc7.bold(o.subtask.id)} [${o.agentName}] ${status}${committed} \u2014 ${o.subtask.title}`);
|
|
3038
|
+
}
|
|
3039
|
+
const conflicts = result.merges.filter((m) => !m.ok);
|
|
3040
|
+
if (conflicts.length > 0) {
|
|
3041
|
+
console.log(pc7.red("\n" + t("swarm.conflictsHeader", { n: conflicts.length })));
|
|
3042
|
+
for (const m of conflicts) {
|
|
3043
|
+
console.log(pc7.red(` ${m.branch}: ${m.conflicts.join(", ")}`));
|
|
3044
|
+
}
|
|
3045
|
+
} else {
|
|
3046
|
+
console.log(pc7.green("\n" + t("swarm.allMerged")));
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
async function swarm(task, opts) {
|
|
3050
|
+
const config = await loadConfig();
|
|
3051
|
+
await runSwarmSession(task, config, {
|
|
3052
|
+
agents: opts.agents ? opts.agents.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
3053
|
+
maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/ui/spinner.ts
|
|
3058
|
+
var RESET3 = "\x1B[0m";
|
|
3059
|
+
var isTTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
3060
|
+
var FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3061
|
+
var violet = (s) => isTTY ? `\x1B[38;2;167;139;250m${s}${RESET3}` : s;
|
|
3062
|
+
var dim = (s) => isTTY ? `\x1B[2m${s}${RESET3}` : s;
|
|
3063
|
+
var Spinner = class {
|
|
3064
|
+
timer;
|
|
3065
|
+
frame = 0;
|
|
3066
|
+
startedAt = 0;
|
|
3067
|
+
label = "";
|
|
3068
|
+
suffix = "";
|
|
3069
|
+
/** Extra dim text appended after the elapsed time (e.g. token count). */
|
|
3070
|
+
setSuffix(suffix) {
|
|
3071
|
+
this.suffix = suffix;
|
|
3072
|
+
}
|
|
3073
|
+
/** Start (or, if already running, just update the label). */
|
|
3074
|
+
start(label) {
|
|
3075
|
+
this.label = label;
|
|
3076
|
+
if (!isTTY) return;
|
|
3077
|
+
if (this.timer) return;
|
|
3078
|
+
this.startedAt = Date.now();
|
|
3079
|
+
this.render();
|
|
3080
|
+
this.timer = setInterval(() => this.render(), 90);
|
|
3081
|
+
this.timer.unref?.();
|
|
3082
|
+
}
|
|
3083
|
+
/** Erase the spinner line and stop animating. */
|
|
3084
|
+
stop() {
|
|
3085
|
+
if (this.timer) {
|
|
3086
|
+
clearInterval(this.timer);
|
|
3087
|
+
this.timer = void 0;
|
|
3088
|
+
}
|
|
3089
|
+
if (isTTY) process.stdout.write("\r\x1B[K");
|
|
3090
|
+
}
|
|
3091
|
+
render() {
|
|
3092
|
+
const f = violet(FRAMES2[this.frame = (this.frame + 1) % FRAMES2.length]);
|
|
3093
|
+
const secs = Math.floor((Date.now() - this.startedAt) / 1e3);
|
|
3094
|
+
const time = secs > 0 ? dim(` (${secs}s)`) : "";
|
|
3095
|
+
const suffix = this.suffix ? dim(` \xB7 ${this.suffix}`) : "";
|
|
3096
|
+
process.stdout.write(`\r\x1B[K${f} \u{1F419} ${dim(this.label + "\u2026")}${time}${suffix}`);
|
|
3097
|
+
}
|
|
3098
|
+
};
|
|
3099
|
+
|
|
3100
|
+
// src/cli/commands/run.ts
|
|
3101
|
+
async function run(task, opts) {
|
|
3102
|
+
let config = await loadConfig();
|
|
3103
|
+
const agentConfig = resolveAgent(config, opts.agent);
|
|
3104
|
+
const workspace = process.cwd();
|
|
3105
|
+
const session = {
|
|
3106
|
+
agentName: agentConfig.name,
|
|
3107
|
+
mode: opts.mode ?? config.permissions.mode,
|
|
3108
|
+
allow: config.permissions.allow,
|
|
3109
|
+
deny: config.permissions.deny,
|
|
3110
|
+
allowedCommands: config.permissions.allowedCommands,
|
|
3111
|
+
maxSteps: opts.maxSteps ? Number(opts.maxSteps) : void 0,
|
|
3112
|
+
history: []
|
|
3113
|
+
};
|
|
3114
|
+
const runTask = async (taskText) => {
|
|
3115
|
+
const active = resolveAgent(config, session.agentName);
|
|
3116
|
+
const resolved2 = createProvider(active);
|
|
3117
|
+
await executeTask(taskText, resolved2, workspace, session);
|
|
3118
|
+
};
|
|
3119
|
+
if (task) {
|
|
3120
|
+
const resolved2 = createProvider(agentConfig);
|
|
3121
|
+
console.log(
|
|
3122
|
+
pc8.dim(
|
|
3123
|
+
t("run.status", {
|
|
3124
|
+
name: resolved2.config.name,
|
|
3125
|
+
provider: resolved2.config.provider,
|
|
3126
|
+
model: resolved2.config.model,
|
|
3127
|
+
toolMode: resolved2.toolMode,
|
|
3128
|
+
mode: session.mode
|
|
3129
|
+
})
|
|
3130
|
+
)
|
|
3131
|
+
);
|
|
3132
|
+
await executeTask(task, resolved2, workspace, session);
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
const resolved = createProvider(agentConfig);
|
|
3136
|
+
await printWelcome({
|
|
3137
|
+
agentName: resolved.config.name,
|
|
3138
|
+
provider: resolved.config.provider,
|
|
3139
|
+
model: resolved.config.model,
|
|
3140
|
+
toolMode: resolved.toolMode,
|
|
3141
|
+
mode: session.mode,
|
|
3142
|
+
workspace
|
|
3143
|
+
});
|
|
3144
|
+
const ctx = {
|
|
3145
|
+
session,
|
|
3146
|
+
runTask,
|
|
3147
|
+
runSwarm: (taskText) => runSwarmSession(taskText, config, { workspace }),
|
|
3148
|
+
getConfig: () => config,
|
|
3149
|
+
reload: async () => {
|
|
3150
|
+
config = await loadConfig();
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
await startRepl(ctx);
|
|
3154
|
+
}
|
|
3155
|
+
async function executeTask(task, resolved, workspace, session) {
|
|
3156
|
+
const spinner3 = new Spinner();
|
|
3157
|
+
const controller = new AbortController();
|
|
3158
|
+
const cancel2 = listenForCancel(controller);
|
|
3159
|
+
const permissions = new PermissionEngine({
|
|
3160
|
+
mode: session.mode,
|
|
3161
|
+
policy: { workspace, allow: session.allow, deny: session.deny },
|
|
3162
|
+
allowedCommands: session.allowedCommands,
|
|
3163
|
+
confirm: async (req) => {
|
|
3164
|
+
spinner3.stop();
|
|
3165
|
+
cancel2.pause();
|
|
3166
|
+
const ok = await confirmAction(req);
|
|
3167
|
+
cancel2.resume();
|
|
3168
|
+
return ok;
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
spinner3.start(t("ui.thinking"));
|
|
3172
|
+
let result;
|
|
3173
|
+
try {
|
|
3174
|
+
result = await runAgent({
|
|
3175
|
+
task,
|
|
3176
|
+
workspace,
|
|
3177
|
+
agent: resolved,
|
|
3178
|
+
permissions,
|
|
3179
|
+
promptContext: { workspace, mode: session.mode, allow: session.allow },
|
|
3180
|
+
history: session.history,
|
|
3181
|
+
maxSteps: session.maxSteps,
|
|
3182
|
+
signal: controller.signal,
|
|
3183
|
+
events: renderEvents(spinner3)
|
|
3184
|
+
});
|
|
3185
|
+
} finally {
|
|
3186
|
+
spinner3.stop();
|
|
3187
|
+
cancel2.dispose();
|
|
3188
|
+
}
|
|
3189
|
+
session.history = result.messages;
|
|
3190
|
+
if (result.reason === "finished") {
|
|
3191
|
+
console.log(pc8.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
|
|
3192
|
+
} else if (result.reason === "cancelled") {
|
|
3193
|
+
console.log(pc8.dim("\n" + t("run.cancelled")));
|
|
3194
|
+
} else if (result.reason === "stalled" || result.reason === "maxsteps") {
|
|
3195
|
+
console.log(pc8.yellow("\n" + t("run.stopped", { steps: result.steps })));
|
|
3196
|
+
}
|
|
3197
|
+
if (result.usage.promptTokens || result.usage.completionTokens) {
|
|
3198
|
+
const total = result.usage.promptTokens + result.usage.completionTokens;
|
|
3199
|
+
console.log(
|
|
3200
|
+
pc8.dim(
|
|
3201
|
+
"\u21B3 " + t("ui.tokens", {
|
|
2656
3202
|
total: fmtTokens(total),
|
|
2657
3203
|
in: fmtTokens(result.usage.promptTokens),
|
|
2658
3204
|
out: fmtTokens(result.usage.completionTokens)
|
|
@@ -2692,7 +3238,7 @@ function listenForCancel(controller) {
|
|
|
2692
3238
|
return { pause: detach, resume: attach, dispose: detach };
|
|
2693
3239
|
}
|
|
2694
3240
|
async function confirmAction(req) {
|
|
2695
|
-
if (req.preview) console.log(
|
|
3241
|
+
if (req.preview) console.log(pc8.dim(req.preview));
|
|
2696
3242
|
const answer = await p2.confirm({ message: t("run.confirm", { summary: req.summary }) });
|
|
2697
3243
|
if (p2.isCancel(answer)) return false;
|
|
2698
3244
|
return answer === true;
|
|
@@ -2708,26 +3254,26 @@ function renderEvents(spinner3) {
|
|
|
2708
3254
|
},
|
|
2709
3255
|
onAssistantText(text2) {
|
|
2710
3256
|
spinner3.stop();
|
|
2711
|
-
if (text2.trim()) console.log(
|
|
3257
|
+
if (text2.trim()) console.log(pc8.cyan(text2.trim()));
|
|
2712
3258
|
},
|
|
2713
3259
|
onToolCall(call) {
|
|
2714
3260
|
spinner3.stop();
|
|
2715
3261
|
const arg = call.name === "run_command" ? call.arguments.command : call.arguments.path;
|
|
2716
|
-
console.log(
|
|
3262
|
+
console.log(pc8.dim(` \u2192 ${call.name}${arg ? ` ${String(arg)}` : ""}`));
|
|
2717
3263
|
spinner3.start(t("ui.running", { tool: call.name }));
|
|
2718
3264
|
},
|
|
2719
3265
|
onToolResult(_call, result) {
|
|
2720
3266
|
spinner3.stop();
|
|
2721
3267
|
const head = result.output.split("\n")[0] ?? "";
|
|
2722
|
-
console.log((result.ok ?
|
|
3268
|
+
console.log((result.ok ? pc8.green(" \u2713 ") : pc8.red(" \u2717 ")) + pc8.dim(head.slice(0, 120)));
|
|
2723
3269
|
},
|
|
2724
3270
|
onReprompt(attempt) {
|
|
2725
3271
|
spinner3.stop();
|
|
2726
|
-
console.log(
|
|
3272
|
+
console.log(pc8.yellow(" " + t("run.reprompt", { attempt })));
|
|
2727
3273
|
},
|
|
2728
3274
|
onCorrection() {
|
|
2729
3275
|
spinner3.stop();
|
|
2730
|
-
console.log(
|
|
3276
|
+
console.log(pc8.yellow(" \u21BB " + t("run.autocorrect")));
|
|
2731
3277
|
}
|
|
2732
3278
|
};
|
|
2733
3279
|
}
|
|
@@ -2738,11 +3284,11 @@ async function setup() {
|
|
|
2738
3284
|
}
|
|
2739
3285
|
|
|
2740
3286
|
// src/cli/commands/init.ts
|
|
2741
|
-
import
|
|
3287
|
+
import pc9 from "picocolors";
|
|
2742
3288
|
|
|
2743
3289
|
// src/core/scaffold/init.ts
|
|
2744
3290
|
import { mkdir as mkdir3, writeFile as writeFile4, access } from "fs/promises";
|
|
2745
|
-
import { dirname as dirname3, join as
|
|
3291
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
2746
3292
|
|
|
2747
3293
|
// src/core/scaffold/templates.ts
|
|
2748
3294
|
function polyTemplates(locale) {
|
|
@@ -2976,458 +3522,54 @@ A mudan\xE7a em termos simples \u2014 o comportamento que o usu\xE1rio vai realm
|
|
|
2976
3522
|
|
|
2977
3523
|
- \u2026
|
|
2978
3524
|
`
|
|
2979
|
-
};
|
|
2980
|
-
|
|
2981
|
-
// src/core/scaffold/init.ts
|
|
2982
|
-
async function scaffoldPoly(workspace, opts) {
|
|
2983
|
-
const templates = polyTemplates(opts.locale);
|
|
2984
|
-
const created = [];
|
|
2985
|
-
const skipped = [];
|
|
2986
|
-
for (const [rel, content] of Object.entries(templates)) {
|
|
2987
|
-
const display = `.poly/${rel}`;
|
|
2988
|
-
const abs = join3(workspace, ".poly", ...rel.split("/"));
|
|
2989
|
-
if (!opts.force && await exists(abs)) {
|
|
2990
|
-
skipped.push(display);
|
|
2991
|
-
continue;
|
|
2992
|
-
}
|
|
2993
|
-
await mkdir3(dirname3(abs), { recursive: true });
|
|
2994
|
-
await writeFile4(abs, content, "utf8");
|
|
2995
|
-
created.push(display);
|
|
2996
|
-
}
|
|
2997
|
-
return { created, skipped };
|
|
2998
|
-
}
|
|
2999
|
-
async function exists(path) {
|
|
3000
|
-
try {
|
|
3001
|
-
await access(path);
|
|
3002
|
-
return true;
|
|
3003
|
-
} catch {
|
|
3004
|
-
return false;
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
// src/cli/commands/init.ts
|
|
3009
|
-
async function init(opts) {
|
|
3010
|
-
const { created, skipped } = await scaffoldPoly(process.cwd(), {
|
|
3011
|
-
force: Boolean(opts.force),
|
|
3012
|
-
locale: getLocale()
|
|
3013
|
-
});
|
|
3014
|
-
if (created.length === 0) {
|
|
3015
|
-
console.log(pc8.yellow(t("init.allExist")));
|
|
3016
|
-
for (const f of skipped) console.log(pc8.dim(` ${f}`));
|
|
3017
|
-
console.log(pc8.dim(t("init.forceHint")));
|
|
3018
|
-
return;
|
|
3019
|
-
}
|
|
3020
|
-
console.log(pc8.green(t("init.created")));
|
|
3021
|
-
for (const f of created) console.log(pc8.dim(` ${f}`));
|
|
3022
|
-
if (skipped.length > 0) {
|
|
3023
|
-
console.log(pc8.dim(t("init.skipped")));
|
|
3024
|
-
for (const f of skipped) console.log(pc8.dim(` ${f}`));
|
|
3025
|
-
}
|
|
3026
|
-
console.log("\n" + t("init.tip"));
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
// src/cli/commands/swarm.ts
|
|
3030
|
-
import pc9 from "picocolors";
|
|
3031
|
-
|
|
3032
|
-
// src/core/git/worktree.ts
|
|
3033
|
-
import { mkdtemp } from "fs/promises";
|
|
3034
|
-
import { tmpdir } from "os";
|
|
3035
|
-
import { join as join4 } from "path";
|
|
3036
|
-
import { simpleGit } from "simple-git";
|
|
3037
|
-
async function ensureRepo(workspace) {
|
|
3038
|
-
const git = simpleGit(workspace);
|
|
3039
|
-
if (!await git.checkIsRepo()) {
|
|
3040
|
-
await git.init();
|
|
3041
|
-
}
|
|
3042
|
-
const identity = await identityArgs(git);
|
|
3043
|
-
const hasHead = await git.raw(["rev-parse", "--verify", "HEAD"]).then(() => true).catch(() => false);
|
|
3044
|
-
if (!hasHead) {
|
|
3045
|
-
await git.raw([...identity, "commit", "--allow-empty", "-m", "polypus: initial commit"]);
|
|
3046
|
-
}
|
|
3047
|
-
return git;
|
|
3048
|
-
}
|
|
3049
|
-
async function identityArgs(git) {
|
|
3050
|
-
const email = await git.raw(["config", "user.email"]).catch(() => "");
|
|
3051
|
-
if (email.trim()) return [];
|
|
3052
|
-
return ["-c", "user.email=polypus@local", "-c", "user.name=Polypus"];
|
|
3053
|
-
}
|
|
3054
|
-
async function createWorktree(git, label) {
|
|
3055
|
-
const branch = `polypus/${label}-${Date.now().toString(36)}`;
|
|
3056
|
-
const path = await mkdtemp(join4(tmpdir(), "polypus-wt-"));
|
|
3057
|
-
await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
|
|
3058
|
-
return { path, branch };
|
|
3059
|
-
}
|
|
3060
|
-
async function commitWorktree(wt, message) {
|
|
3061
|
-
const wtGit = simpleGit(wt.path);
|
|
3062
|
-
await wtGit.add(["-A"]);
|
|
3063
|
-
const status = await wtGit.status();
|
|
3064
|
-
if (status.staged.length === 0 && status.files.length === 0) return false;
|
|
3065
|
-
const identity = await identityArgs(wtGit);
|
|
3066
|
-
await wtGit.raw([...identity, "commit", "-m", message]);
|
|
3067
|
-
return true;
|
|
3068
|
-
}
|
|
3069
|
-
async function mergeWorktreeBranch(git, branch) {
|
|
3070
|
-
try {
|
|
3071
|
-
const identity = await identityArgs(git);
|
|
3072
|
-
await git.raw([...identity, "merge", "--no-edit", branch]);
|
|
3073
|
-
return { branch, ok: true, conflicts: [] };
|
|
3074
|
-
} catch (err) {
|
|
3075
|
-
const status = await git.status().catch(() => void 0);
|
|
3076
|
-
const conflicts = status?.conflicted ?? [];
|
|
3077
|
-
await git.raw(["merge", "--abort"]).catch(() => void 0);
|
|
3078
|
-
if (conflicts.length === 0) {
|
|
3079
|
-
throw err;
|
|
3080
|
-
}
|
|
3081
|
-
return { branch, ok: false, conflicts };
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
async function removeWorktree(git, wt) {
|
|
3085
|
-
await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
|
|
3086
|
-
await git.raw(["branch", "-D", wt.branch]).catch(() => void 0);
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
// src/core/agent/worker.ts
|
|
3090
|
-
async function runWorker(subtask, agent, wt, allow, deny, events) {
|
|
3091
|
-
const permissions = new PermissionEngine({
|
|
3092
|
-
mode: "bypass",
|
|
3093
|
-
policy: { workspace: wt.path, allow, deny },
|
|
3094
|
-
allowedCommands: []
|
|
3095
|
-
});
|
|
3096
|
-
const result = await runAgent({
|
|
3097
|
-
task: subtask.brief,
|
|
3098
|
-
workspace: wt.path,
|
|
3099
|
-
agent,
|
|
3100
|
-
permissions,
|
|
3101
|
-
promptContext: { workspace: wt.path, mode: "bypass", allow, briefing: subtask.brief },
|
|
3102
|
-
events
|
|
3103
|
-
});
|
|
3104
|
-
const committed = await commitWorktree(wt, `polypus(${subtask.id}): ${subtask.title}`);
|
|
3105
|
-
return {
|
|
3106
|
-
subtask,
|
|
3107
|
-
agentName: agent.config.name,
|
|
3108
|
-
branch: wt.branch,
|
|
3109
|
-
finished: result.finished,
|
|
3110
|
-
summary: result.summary,
|
|
3111
|
-
committed,
|
|
3112
|
-
steps: result.steps
|
|
3113
|
-
};
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
// src/core/agent/orchestrator.ts
|
|
3117
|
-
async function runSwarm(opts) {
|
|
3118
|
-
const lead = opts.agents[0];
|
|
3119
|
-
if (!lead) throw new Error("Swarm requires at least one agent.");
|
|
3120
|
-
const maxSubtasks = opts.maxSubtasks ?? Math.max(opts.agents.length, 2);
|
|
3121
|
-
const git = await ensureRepo(opts.workspace);
|
|
3122
|
-
const subtasks = await decompose(lead, opts.task, maxSubtasks);
|
|
3123
|
-
opts.events?.onDecomposed?.(subtasks);
|
|
3124
|
-
const worktrees = [];
|
|
3125
|
-
for (const subtask of subtasks) {
|
|
3126
|
-
worktrees.push(await createWorktree(git, subtask.id));
|
|
3127
|
-
}
|
|
3128
|
-
const outcomes = await Promise.all(
|
|
3129
|
-
subtasks.map(async (subtask, i) => {
|
|
3130
|
-
const agent = opts.agents[i % opts.agents.length];
|
|
3131
|
-
const wt = worktrees[i];
|
|
3132
|
-
opts.events?.onWorkerStart?.(subtask, agent.config.name);
|
|
3133
|
-
const outcome = await runWorker(
|
|
3134
|
-
subtask,
|
|
3135
|
-
agent,
|
|
3136
|
-
wt,
|
|
3137
|
-
opts.allow,
|
|
3138
|
-
opts.deny,
|
|
3139
|
-
opts.events?.workerEvents?.(subtask)
|
|
3140
|
-
);
|
|
3141
|
-
opts.events?.onWorkerDone?.(outcome);
|
|
3142
|
-
return outcome;
|
|
3143
|
-
})
|
|
3144
|
-
);
|
|
3145
|
-
const merges = [];
|
|
3146
|
-
for (const outcome of outcomes) {
|
|
3147
|
-
if (!outcome.committed) continue;
|
|
3148
|
-
const merge = await mergeWorktreeBranch(git, outcome.branch);
|
|
3149
|
-
merges.push(merge);
|
|
3150
|
-
opts.events?.onMerge?.(merge);
|
|
3151
|
-
}
|
|
3152
|
-
const conflicted = new Set(merges.filter((m) => !m.ok).map((m) => m.branch));
|
|
3153
|
-
for (const wt of worktrees) {
|
|
3154
|
-
if (conflicted.has(wt.branch)) {
|
|
3155
|
-
await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
|
|
3156
|
-
} else {
|
|
3157
|
-
await removeWorktree(git, wt);
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
return { subtasks, outcomes, merges };
|
|
3161
|
-
}
|
|
3162
|
-
var DECOMPOSE_SYSTEM = [
|
|
3163
|
-
"You are a tech lead splitting a coding task into independent subtasks that can be done in parallel.",
|
|
3164
|
-
'Return ONLY a JSON array. Each item: {"title": string, "brief": string}.',
|
|
3165
|
-
"Make subtasks touch DIFFERENT files/areas to minimize merge conflicts.",
|
|
3166
|
-
"Keep the list small (prefer 2-4 items). Each brief must be self-contained and actionable."
|
|
3167
|
-
].join("\n");
|
|
3168
|
-
async function decompose(lead, task, maxSubtasks) {
|
|
3169
|
-
try {
|
|
3170
|
-
const res = await lead.provider.chat({
|
|
3171
|
-
messages: [
|
|
3172
|
-
{ role: "system", content: DECOMPOSE_SYSTEM },
|
|
3173
|
-
{ role: "user", content: `Task:
|
|
3174
|
-
${task}
|
|
3525
|
+
};
|
|
3175
3526
|
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3527
|
+
// src/core/scaffold/init.ts
|
|
3528
|
+
async function scaffoldPoly(workspace, opts) {
|
|
3529
|
+
const templates = polyTemplates(opts.locale);
|
|
3530
|
+
const created = [];
|
|
3531
|
+
const skipped = [];
|
|
3532
|
+
for (const [rel, content] of Object.entries(templates)) {
|
|
3533
|
+
const display = `.poly/${rel}`;
|
|
3534
|
+
const abs = join4(workspace, ".poly", ...rel.split("/"));
|
|
3535
|
+
if (!opts.force && await exists(abs)) {
|
|
3536
|
+
skipped.push(display);
|
|
3537
|
+
continue;
|
|
3187
3538
|
}
|
|
3188
|
-
|
|
3539
|
+
await mkdir3(dirname3(abs), { recursive: true });
|
|
3540
|
+
await writeFile4(abs, content, "utf8");
|
|
3541
|
+
created.push(display);
|
|
3189
3542
|
}
|
|
3190
|
-
return
|
|
3543
|
+
return { created, skipped };
|
|
3191
3544
|
}
|
|
3192
|
-
function
|
|
3193
|
-
const start = text2.indexOf("[");
|
|
3194
|
-
const end = text2.lastIndexOf("]");
|
|
3195
|
-
if (start === -1 || end <= start) return null;
|
|
3545
|
+
async function exists(path) {
|
|
3196
3546
|
try {
|
|
3197
|
-
|
|
3198
|
-
return
|
|
3547
|
+
await access(path);
|
|
3548
|
+
return true;
|
|
3199
3549
|
} catch {
|
|
3200
|
-
return
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
// src/ui/swarm-view.ts
|
|
3205
|
-
var RESET3 = "\x1B[0m";
|
|
3206
|
-
var FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3207
|
-
function describeToolCall(call) {
|
|
3208
|
-
const raw = call.name === "run_command" ? call.arguments.command : call.arguments.path;
|
|
3209
|
-
const arg = typeof raw === "string" ? raw : "";
|
|
3210
|
-
const short = arg.length > 40 ? arg.slice(0, 39) + "\u2026" : arg;
|
|
3211
|
-
return short ? `${call.name} ${short}` : call.name;
|
|
3212
|
-
}
|
|
3213
|
-
var SwarmView = class {
|
|
3214
|
-
constructor(leadName, opts = {}) {
|
|
3215
|
-
this.leadName = leadName;
|
|
3216
|
-
this.tty = opts.tty ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
|
|
3217
|
-
this.color = opts.color ?? this.tty;
|
|
3218
|
-
this.write = opts.sink ?? ((s) => process.stdout.write(s));
|
|
3219
|
-
}
|
|
3220
|
-
leadName;
|
|
3221
|
-
tty;
|
|
3222
|
-
color;
|
|
3223
|
-
write;
|
|
3224
|
-
workers = /* @__PURE__ */ new Map();
|
|
3225
|
-
order = [];
|
|
3226
|
-
phase = "decomposing";
|
|
3227
|
-
frame = 0;
|
|
3228
|
-
lastLines = 0;
|
|
3229
|
-
timer;
|
|
3230
|
-
start() {
|
|
3231
|
-
if (!this.tty) {
|
|
3232
|
-
this.write(`\u{1F419} ${t("swarm.view.header", { lead: this.leadName })} \u2014 ${t("swarm.view.decomposing")}
|
|
3233
|
-
`);
|
|
3234
|
-
return;
|
|
3235
|
-
}
|
|
3236
|
-
this.flush();
|
|
3237
|
-
this.timer = setInterval(() => {
|
|
3238
|
-
this.frame = (this.frame + 1) % FRAMES2.length;
|
|
3239
|
-
this.flush();
|
|
3240
|
-
}, 110);
|
|
3241
|
-
this.timer.unref?.();
|
|
3242
|
-
}
|
|
3243
|
-
setSubtasks(subtasks) {
|
|
3244
|
-
this.phase = "running";
|
|
3245
|
-
for (const s of subtasks) {
|
|
3246
|
-
this.workers.set(s.id, { id: s.id, title: s.title, agent: "", status: "pending", action: "", steps: 0 });
|
|
3247
|
-
this.order.push(s.id);
|
|
3248
|
-
}
|
|
3249
|
-
if (!this.tty) {
|
|
3250
|
-
this.write(` ${t("swarm.decomposed", { n: subtasks.length })}
|
|
3251
|
-
`);
|
|
3252
|
-
for (const s of subtasks) this.write(` ${s.id}: ${s.title}
|
|
3253
|
-
`);
|
|
3254
|
-
}
|
|
3255
|
-
this.flush();
|
|
3256
|
-
}
|
|
3257
|
-
workerStart(id, agent) {
|
|
3258
|
-
const w = this.workers.get(id);
|
|
3259
|
-
if (!w) return;
|
|
3260
|
-
w.agent = agent;
|
|
3261
|
-
w.status = "running";
|
|
3262
|
-
if (!this.tty) this.write(` \u25B6 ${id} [${agent}] ${w.title}
|
|
3263
|
-
`);
|
|
3264
|
-
this.flush();
|
|
3265
|
-
}
|
|
3266
|
-
workerAction(id, action) {
|
|
3267
|
-
const w = this.workers.get(id);
|
|
3268
|
-
if (!w) return;
|
|
3269
|
-
w.action = action;
|
|
3270
|
-
this.flush();
|
|
3271
|
-
}
|
|
3272
|
-
workerStep(id, n) {
|
|
3273
|
-
const w = this.workers.get(id);
|
|
3274
|
-
if (!w) return;
|
|
3275
|
-
w.steps = n;
|
|
3276
|
-
this.flush();
|
|
3277
|
-
}
|
|
3278
|
-
workerDone(o) {
|
|
3279
|
-
const w = this.workers.get(o.subtask.id);
|
|
3280
|
-
if (!w) return;
|
|
3281
|
-
w.status = o.finished ? "done" : "stopped";
|
|
3282
|
-
w.steps = o.steps;
|
|
3283
|
-
w.branch = o.branch;
|
|
3284
|
-
w.action = "";
|
|
3285
|
-
if (!this.tty) {
|
|
3286
|
-
const tag = o.finished ? "\u2713" : "\u25A0";
|
|
3287
|
-
const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
|
|
3288
|
-
this.write(` ${tag} ${o.subtask.id} (${t("swarm.view.steps", { n: o.steps })}, ${changes})
|
|
3289
|
-
`);
|
|
3290
|
-
}
|
|
3291
|
-
this.flush();
|
|
3292
|
-
}
|
|
3293
|
-
merge(r) {
|
|
3294
|
-
for (const w of this.workers.values()) {
|
|
3295
|
-
if (w.branch === r.branch) w.merge = r.ok ? "ok" : "conflict";
|
|
3296
|
-
}
|
|
3297
|
-
if (!this.tty) {
|
|
3298
|
-
this.write(r.ok ? ` \u2935 ${t("swarm.merged", { branch: r.branch })}
|
|
3299
|
-
` : ` \u2717 ${t("swarm.mergeConflict", { branch: r.branch })}
|
|
3300
|
-
`);
|
|
3301
|
-
}
|
|
3302
|
-
this.flush();
|
|
3303
|
-
}
|
|
3304
|
-
stop() {
|
|
3305
|
-
this.phase = "done";
|
|
3306
|
-
if (this.timer) {
|
|
3307
|
-
clearInterval(this.timer);
|
|
3308
|
-
this.timer = void 0;
|
|
3309
|
-
}
|
|
3310
|
-
this.flush();
|
|
3311
|
-
}
|
|
3312
|
-
/** Content lines of the dashboard (no cursor control). Exposed for tests. */
|
|
3313
|
-
frameLines() {
|
|
3314
|
-
const spin = this.dim(FRAMES2[this.frame]);
|
|
3315
|
-
const lead = `\u{1F419} ${t("swarm.view.header", { lead: this.leadName })}`;
|
|
3316
|
-
const lines = [];
|
|
3317
|
-
if (this.phase === "decomposing") {
|
|
3318
|
-
lines.push(`${spin} ${lead}`);
|
|
3319
|
-
lines.push(" " + this.dim(t("swarm.view.decomposing")));
|
|
3320
|
-
return lines;
|
|
3321
|
-
}
|
|
3322
|
-
lines.push(`${this.phase === "running" ? spin : " "} ${lead}`);
|
|
3323
|
-
lines.push("");
|
|
3324
|
-
for (const id of this.order) {
|
|
3325
|
-
const w = this.workers.get(id);
|
|
3326
|
-
lines.push(this.row(w, spin));
|
|
3327
|
-
}
|
|
3328
|
-
return lines;
|
|
3329
|
-
}
|
|
3330
|
-
// -------------------------------------------------------------------------
|
|
3331
|
-
row(w, spin) {
|
|
3332
|
-
const icon = w.status === "running" ? spin : w.status === "done" ? this.c("\u2713", "32") : w.status === "stopped" ? this.c("\u25A0", "33") : this.dim("\xB7");
|
|
3333
|
-
const status = this.statusLabel(w);
|
|
3334
|
-
const meta = w.steps > 0 ? this.dim(" \xB7 " + (w.status === "running" ? t("swarm.view.step", { n: w.steps }) : t("swarm.view.steps", { n: w.steps }))) : "";
|
|
3335
|
-
const action = w.action ? w.action : this.dim("\u2014");
|
|
3336
|
-
return ` ${icon} ${pad(w.id, 4)} ${pad(status, 12)} ${pad(`[${w.agent}]`, 14)} ${action}${meta}`;
|
|
3337
|
-
}
|
|
3338
|
-
statusLabel(w) {
|
|
3339
|
-
if (w.merge === "conflict") return this.c(t("swarm.view.conflict"), "31");
|
|
3340
|
-
if (w.status === "running") return this.c(t("swarm.view.running"), "36");
|
|
3341
|
-
if (w.status === "done") return this.c(t("swarm.view.done"), "32");
|
|
3342
|
-
if (w.status === "stopped") return this.c(t("swarm.view.stopped"), "33");
|
|
3343
|
-
return this.dim(t("swarm.view.pending"));
|
|
3344
|
-
}
|
|
3345
|
-
/** Redraw the block in place (TTY) by clearing the previous frame first. */
|
|
3346
|
-
flush() {
|
|
3347
|
-
if (!this.tty) return;
|
|
3348
|
-
const lines = this.frameLines();
|
|
3349
|
-
let s = "";
|
|
3350
|
-
if (this.lastLines > 0) s += `\x1B[${this.lastLines}A`;
|
|
3351
|
-
s += "\x1B[0J";
|
|
3352
|
-
s += lines.join("\n") + "\n";
|
|
3353
|
-
this.write(s);
|
|
3354
|
-
this.lastLines = lines.length;
|
|
3355
|
-
}
|
|
3356
|
-
c(s, code) {
|
|
3357
|
-
return this.color ? `\x1B[${code}m${s}${RESET3}` : s;
|
|
3358
|
-
}
|
|
3359
|
-
dim(s) {
|
|
3360
|
-
return this.color ? `\x1B[2m${s}${RESET3}` : s;
|
|
3550
|
+
return false;
|
|
3361
3551
|
}
|
|
3362
|
-
};
|
|
3363
|
-
function pad(s, n) {
|
|
3364
|
-
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
3365
3552
|
}
|
|
3366
3553
|
|
|
3367
|
-
// src/cli/commands/
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
async function swarm(task, opts) {
|
|
3373
|
-
const config = await loadConfig();
|
|
3374
|
-
if (!canSwarm(config.agents.length)) {
|
|
3375
|
-
throw new Error(t("swarm.needsAgents", { min: MIN_SWARM_AGENTS, have: config.agents.length }));
|
|
3376
|
-
}
|
|
3377
|
-
const selected = opts.agents ? opts.agents.split(",").map((s) => s.trim()).filter(Boolean) : config.agents.map((a) => a.name);
|
|
3378
|
-
if (selected.length === 0) {
|
|
3379
|
-
throw new Error(t("swarm.noAgents"));
|
|
3380
|
-
}
|
|
3381
|
-
const resolved = selected.map((name) => {
|
|
3382
|
-
const a = config.agents.find((x) => x.name === name);
|
|
3383
|
-
if (!a) throw new Error(t("agent.notFound", { name }));
|
|
3384
|
-
return createProvider(a);
|
|
3554
|
+
// src/cli/commands/init.ts
|
|
3555
|
+
async function init(opts) {
|
|
3556
|
+
const { created, skipped } = await scaffoldPoly(process.cwd(), {
|
|
3557
|
+
force: Boolean(opts.force),
|
|
3558
|
+
locale: getLocale()
|
|
3385
3559
|
});
|
|
3386
|
-
|
|
3387
|
-
pc9.
|
|
3388
|
-
);
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
view.start();
|
|
3392
|
-
let result;
|
|
3393
|
-
try {
|
|
3394
|
-
result = await runSwarm({
|
|
3395
|
-
task,
|
|
3396
|
-
workspace: process.cwd(),
|
|
3397
|
-
agents: resolved,
|
|
3398
|
-
allow: config.permissions.allow,
|
|
3399
|
-
deny: config.permissions.deny,
|
|
3400
|
-
maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0,
|
|
3401
|
-
events: {
|
|
3402
|
-
onDecomposed: (subtasks) => view.setSubtasks(subtasks),
|
|
3403
|
-
onWorkerStart: (subtask, agentName) => view.workerStart(subtask.id, agentName),
|
|
3404
|
-
onWorkerDone: (outcome) => view.workerDone(outcome),
|
|
3405
|
-
onMerge: (merge) => view.merge(merge),
|
|
3406
|
-
workerEvents: (subtask) => ({
|
|
3407
|
-
onToolCall: (call) => view.workerAction(subtask.id, describeToolCall(call)),
|
|
3408
|
-
onStep: (step) => view.workerStep(subtask.id, step)
|
|
3409
|
-
})
|
|
3410
|
-
}
|
|
3411
|
-
});
|
|
3412
|
-
} finally {
|
|
3413
|
-
view.stop();
|
|
3414
|
-
}
|
|
3415
|
-
console.log("");
|
|
3416
|
-
console.log(pc9.bold("\n" + t("swarm.summary")));
|
|
3417
|
-
for (const o of result.outcomes) {
|
|
3418
|
-
const status = o.finished ? pc9.green(t("swarm.statusDone")) : pc9.yellow(t("swarm.statusIncomplete"));
|
|
3419
|
-
const committed = o.committed ? "" : pc9.dim(` (${t("swarm.noChanges")})`);
|
|
3420
|
-
console.log(` ${pc9.bold(o.subtask.id)} [${o.agentName}] ${status}${committed} \u2014 ${o.subtask.title}`);
|
|
3560
|
+
if (created.length === 0) {
|
|
3561
|
+
console.log(pc9.yellow(t("init.allExist")));
|
|
3562
|
+
for (const f of skipped) console.log(pc9.dim(` ${f}`));
|
|
3563
|
+
console.log(pc9.dim(t("init.forceHint")));
|
|
3564
|
+
return;
|
|
3421
3565
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
}
|
|
3428
|
-
} else {
|
|
3429
|
-
console.log(pc9.green("\n" + t("swarm.allMerged")));
|
|
3566
|
+
console.log(pc9.green(t("init.created")));
|
|
3567
|
+
for (const f of created) console.log(pc9.dim(` ${f}`));
|
|
3568
|
+
if (skipped.length > 0) {
|
|
3569
|
+
console.log(pc9.dim(t("init.skipped")));
|
|
3570
|
+
for (const f of skipped) console.log(pc9.dim(` ${f}`));
|
|
3430
3571
|
}
|
|
3572
|
+
console.log("\n" + t("init.tip"));
|
|
3431
3573
|
}
|
|
3432
3574
|
|
|
3433
3575
|
// src/cli/commands/models.ts
|