@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/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 pc7 from "picocolors";
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/ui/spinner.ts
2513
- var RESET2 = "\x1B[0m";
2514
- var isTTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
2515
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2516
- var violet = (s) => isTTY ? `\x1B[38;2;167;139;250m${s}${RESET2}` : s;
2517
- var dim = (s) => isTTY ? `\x1B[2m${s}${RESET2}` : s;
2518
- var Spinner = class {
2519
- timer;
2520
- frame = 0;
2521
- startedAt = 0;
2522
- label = "";
2523
- suffix = "";
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
- /** Start (or, if already running, just update the label). */
2529
- start(label) {
2530
- this.label = label;
2531
- if (!isTTY) return;
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
- /** Erase the spinner line and stop animating. */
2539
- stop() {
2540
- if (this.timer) {
2541
- clearInterval(this.timer);
2542
- this.timer = void 0;
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
- if (isTTY) process.stdout.write("\r\x1B[K");
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/cli/commands/run.ts
2556
- async function run(task, opts) {
2557
- let config = await loadConfig();
2558
- const agentConfig = resolveAgent(config, opts.agent);
2559
- const workspace = process.cwd();
2560
- const session = {
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 ctx = {
2600
- session,
2601
- runTask,
2602
- getConfig: () => config,
2603
- reload: async () => {
2604
- config = await loadConfig();
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
- async function executeTask(task, resolved, workspace, session) {
2610
- const spinner3 = new Spinner();
2611
- const controller = new AbortController();
2612
- const cancel2 = listenForCancel(controller);
2613
- const permissions = new PermissionEngine({
2614
- mode: session.mode,
2615
- policy: { workspace, allow: session.allow, deny: session.deny },
2616
- allowedCommands: session.allowedCommands,
2617
- confirm: async (req) => {
2618
- spinner3.stop();
2619
- cancel2.pause();
2620
- const ok = await confirmAction(req);
2621
- cancel2.resume();
2622
- return ok;
2623
- }
2624
- });
2625
- spinner3.start(t("ui.thinking"));
2626
- let result;
2627
- try {
2628
- result = await runAgent({
2629
- task,
2630
- workspace,
2631
- agent: resolved,
2632
- permissions,
2633
- promptContext: { workspace, mode: session.mode, allow: session.allow },
2634
- history: session.history,
2635
- maxSteps: session.maxSteps,
2636
- signal: controller.signal,
2637
- events: renderEvents(spinner3)
2638
- });
2639
- } finally {
2640
- spinner3.stop();
2641
- cancel2.dispose();
2642
- }
2643
- session.history = result.messages;
2644
- if (result.reason === "finished") {
2645
- console.log(pc7.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
2646
- } else if (result.reason === "cancelled") {
2647
- console.log(pc7.dim("\n" + t("run.cancelled")));
2648
- } else if (result.reason === "stalled" || result.reason === "maxsteps") {
2649
- console.log(pc7.yellow("\n" + t("run.stopped", { steps: result.steps })));
2650
- }
2651
- if (result.usage.promptTokens || result.usage.completionTokens) {
2652
- const total = result.usage.promptTokens + result.usage.completionTokens;
2653
- console.log(
2654
- pc7.dim(
2655
- "\u21B3 " + t("ui.tokens", {
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(pc7.dim(req.preview));
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(pc7.cyan(text2.trim()));
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(pc7.dim(` \u2192 ${call.name}${arg ? ` ${String(arg)}` : ""}`));
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 ? pc7.green(" \u2713 ") : pc7.red(" \u2717 ")) + pc7.dim(head.slice(0, 120)));
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(pc7.yellow(" " + t("run.reprompt", { attempt })));
3272
+ console.log(pc8.yellow(" " + t("run.reprompt", { attempt })));
2727
3273
  },
2728
3274
  onCorrection() {
2729
3275
  spinner3.stop();
2730
- console.log(pc7.yellow(" \u21BB " + t("run.autocorrect")));
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 pc8 from "picocolors";
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 join3 } from "path";
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
- Return at most ${maxSubtasks} subtasks as a JSON array.` }
3177
- ],
3178
- params: { temperature: 0 }
3179
- });
3180
- const parsed = extractJsonArray(res.content);
3181
- if (parsed && parsed.length > 0) {
3182
- return parsed.slice(0, maxSubtasks).map((item, i) => ({
3183
- id: `t${i + 1}`,
3184
- title: String(item.title ?? `subtask ${i + 1}`),
3185
- brief: String(item.brief ?? item.title ?? task)
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
- } catch {
3539
+ await mkdir3(dirname3(abs), { recursive: true });
3540
+ await writeFile4(abs, content, "utf8");
3541
+ created.push(display);
3189
3542
  }
3190
- return [{ id: "t1", title: "task", brief: task }];
3543
+ return { created, skipped };
3191
3544
  }
3192
- function extractJsonArray(text2) {
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
- const parsed = JSON.parse(text2.slice(start, end + 1));
3198
- return Array.isArray(parsed) ? parsed : null;
3547
+ await access(path);
3548
+ return true;
3199
3549
  } catch {
3200
- return null;
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/swarm.ts
3368
- var MIN_SWARM_AGENTS = 3;
3369
- function canSwarm(agentCount) {
3370
- return agentCount >= MIN_SWARM_AGENTS;
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
- console.log(
3387
- pc9.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace: process.cwd() }))
3388
- );
3389
- console.log(pc9.yellow(t("swarm.bypassNote") + "\n"));
3390
- const view = new SwarmView(resolved[0].config.name);
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
- const conflicts = result.merges.filter((m) => !m.ok);
3423
- if (conflicts.length > 0) {
3424
- console.log(pc9.red("\n" + t("swarm.conflictsHeader", { n: conflicts.length })));
3425
- for (const m of conflicts) {
3426
- console.log(pc9.red(` ${m.branch}: ${m.conflicts.join(", ")}`));
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