@gaberrb/polypus 0.4.0 → 0.4.2

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
@@ -1431,6 +1435,116 @@ function clamp(s) {
1431
1435
  return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + "\n\u2026[truncated]" : s;
1432
1436
  }
1433
1437
 
1438
+ // src/core/tools/search-file.ts
1439
+ import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
1440
+ import { join as join2, resolve as resolve5 } from "path";
1441
+ import { z as z6 } from "zod";
1442
+ var Args5 = z6.object({
1443
+ query: z6.string().min(1),
1444
+ path: z6.string().optional(),
1445
+ glob: z6.string().optional(),
1446
+ max_results: z6.number().int().positive().max(1e3).optional()
1447
+ });
1448
+ var DEFAULT_MAX_RESULTS = 50;
1449
+ var MAX_OUTPUT2 = 2e4;
1450
+ var MAX_FILE_BYTES = 2e6;
1451
+ var SNIPPET_CHARS = 200;
1452
+ var NUL = String.fromCharCode(0);
1453
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "coverage", ".turbo"]);
1454
+ var searchTool = {
1455
+ mutating: false,
1456
+ spec: {
1457
+ name: "search",
1458
+ description: "Search file contents by regular expression across the workspace (like grep/ripgrep). Returns matches as 'path:line: snippet'. Respects the allow/deny-list and skips node_modules/.git. Use this to find where a symbol is defined or used instead of reading files blindly.",
1459
+ parameters: {
1460
+ type: "object",
1461
+ properties: {
1462
+ query: { type: "string", description: "Regular expression to match against each line" },
1463
+ path: { type: "string", description: "Workspace-relative directory to search in (default '.')" },
1464
+ glob: {
1465
+ type: "string",
1466
+ description: "Optional glob to limit files, e.g. 'src/**/*.ts'"
1467
+ },
1468
+ max_results: {
1469
+ type: "number",
1470
+ description: `Maximum number of matches to return (default ${DEFAULT_MAX_RESULTS})`
1471
+ }
1472
+ },
1473
+ required: ["query"]
1474
+ }
1475
+ },
1476
+ async run(rawArgs, ctx) {
1477
+ const parsed = Args5.safeParse(rawArgs);
1478
+ if (!parsed.success) return { ok: false, output: "Invalid args: 'query' is required." };
1479
+ const { query, path = ".", glob, max_results } = parsed.data;
1480
+ let regex;
1481
+ try {
1482
+ regex = new RegExp(query);
1483
+ } catch (err) {
1484
+ return { ok: false, output: `Invalid regular expression: ${err.message}` };
1485
+ }
1486
+ if (path !== ".") {
1487
+ const decision = ctx.permissions.authorizeRead(path);
1488
+ if (!decision.allowed) return { ok: false, output: `Search denied: ${decision.reason}` };
1489
+ }
1490
+ const globRe = glob ? globToRegExp(glob) : void 0;
1491
+ const limit = max_results ?? DEFAULT_MAX_RESULTS;
1492
+ const root = resolve5(ctx.workspace, path);
1493
+ const matches = [];
1494
+ let truncated = false;
1495
+ const walk = async (dir) => {
1496
+ if (matches.length >= limit) return;
1497
+ let entries;
1498
+ try {
1499
+ entries = await readdir2(dir, { withFileTypes: true });
1500
+ } catch {
1501
+ return;
1502
+ }
1503
+ for (const entry of entries) {
1504
+ if (matches.length >= limit) return;
1505
+ const abs = join2(dir, entry.name);
1506
+ const rel = toPosix(abs.slice(ctx.workspace.length + 1));
1507
+ if (entry.isDirectory()) {
1508
+ if (SKIP_DIRS.has(entry.name)) continue;
1509
+ await walk(abs);
1510
+ continue;
1511
+ }
1512
+ if (!entry.isFile()) continue;
1513
+ if (globRe && !globRe.test(rel)) continue;
1514
+ if (!ctx.permissions.authorizeRead(rel).allowed) continue;
1515
+ try {
1516
+ const info = await stat(abs);
1517
+ if (info.size > MAX_FILE_BYTES) continue;
1518
+ const content = await readFile4(abs, "utf8");
1519
+ if (content.includes(NUL)) continue;
1520
+ const lines = content.split("\n");
1521
+ for (let i = 0; i < lines.length; i++) {
1522
+ if (matches.length >= limit) {
1523
+ truncated = true;
1524
+ return;
1525
+ }
1526
+ if (regex.test(lines[i])) {
1527
+ const snippet = lines[i].trim().slice(0, SNIPPET_CHARS);
1528
+ matches.push(`${rel}:${i + 1}: ${snippet}`);
1529
+ }
1530
+ }
1531
+ } catch {
1532
+ }
1533
+ }
1534
+ };
1535
+ await walk(root);
1536
+ if (matches.length === 0) {
1537
+ return { ok: true, output: `No matches for /${query}/${glob ? ` in ${glob}` : ""}.` };
1538
+ }
1539
+ const header = `${matches.length}${truncated ? "+" : ""} match(es) for /${query}/:`;
1540
+ const body = [header, ...matches].join("\n");
1541
+ return {
1542
+ ok: true,
1543
+ output: body.length > MAX_OUTPUT2 ? body.slice(0, MAX_OUTPUT2) + "\n\u2026[truncated]" : body
1544
+ };
1545
+ }
1546
+ };
1547
+
1434
1548
  // src/core/tools/types.ts
1435
1549
  var FINISH_TOOL = {
1436
1550
  name: "finish",
@@ -1446,9 +1560,9 @@ var FINISH_TOOL = {
1446
1560
 
1447
1561
  // src/core/tools/write-file.ts
1448
1562
  import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1449
- import { dirname, resolve as resolve5 } from "path";
1450
- import { z as z6 } from "zod";
1451
- var Args5 = z6.object({ path: z6.string().min(1), content: z6.string() });
1563
+ import { dirname, resolve as resolve6 } from "path";
1564
+ import { z as z7 } from "zod";
1565
+ var Args6 = z7.object({ path: z7.string().min(1), content: z7.string() });
1452
1566
  var writeFileTool = {
1453
1567
  mutating: true,
1454
1568
  spec: {
@@ -1464,7 +1578,7 @@ var writeFileTool = {
1464
1578
  }
1465
1579
  },
1466
1580
  async run(rawArgs, ctx) {
1467
- const args = Args5.safeParse(rawArgs);
1581
+ const args = Args6.safeParse(rawArgs);
1468
1582
  if (!args.success) {
1469
1583
  const got = Object.keys(rawArgs ?? {});
1470
1584
  return {
@@ -1476,7 +1590,7 @@ var writeFileTool = {
1476
1590
  const decision = await ctx.permissions.authorizeWrite(args.data.path, preview);
1477
1591
  if (!decision.allowed) return { ok: false, output: `Write denied: ${decision.reason}` };
1478
1592
  try {
1479
- const abs = resolve5(ctx.workspace, args.data.path);
1593
+ const abs = resolve6(ctx.workspace, args.data.path);
1480
1594
  await mkdir2(dirname(abs), { recursive: true });
1481
1595
  await writeFile3(abs, args.data.content, "utf8");
1482
1596
  const lines = args.data.content.split("\n").length;
@@ -1495,6 +1609,7 @@ function previewContent(content) {
1495
1609
  var TOOLS = {
1496
1610
  [readFileTool.spec.name]: readFileTool,
1497
1611
  [listDirTool.spec.name]: listDirTool,
1612
+ [searchTool.spec.name]: searchTool,
1498
1613
  [writeFileTool.spec.name]: writeFileTool,
1499
1614
  [editFileTool.spec.name]: editFileTool,
1500
1615
  [runCommandTool.spec.name]: runCommandTool
@@ -1507,8 +1622,8 @@ function getTool(name) {
1507
1622
  }
1508
1623
 
1509
1624
  // src/core/agent/correction.ts
1510
- import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
1511
- import { dirname as dirname2, resolve as resolve6 } from "path";
1625
+ import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
1626
+ import { dirname as dirname2, resolve as resolve7 } from "path";
1512
1627
  function truncationGuidance(toolName) {
1513
1628
  const fileHint = toolName === "write_file" || toolName === "edit_file" ? " Write large files in parts: create the file with the first chunk via write_file, then append the rest with edit_file in the next steps." : "";
1514
1629
  return [
@@ -1608,7 +1723,7 @@ ${text2}` : null;
1608
1723
  }
1609
1724
  async function readWorkspaceFile(workspace, path) {
1610
1725
  try {
1611
- return await readFile4(resolve6(workspace, path), "utf8");
1726
+ return await readFile5(resolve7(workspace, path), "utf8");
1612
1727
  } catch {
1613
1728
  return null;
1614
1729
  }
@@ -1666,11 +1781,11 @@ async function occurrenceLines(workspace, path, search) {
1666
1781
  return out;
1667
1782
  }
1668
1783
  async function listNearest(workspace, path) {
1669
- let dir = dirname2(resolve6(workspace, path));
1784
+ let dir = dirname2(resolve7(workspace, path));
1670
1785
  for (let i = 0; i < 8; i++) {
1671
1786
  try {
1672
- const entries = await readdir2(dir, { withFileTypes: true });
1673
- const rel = dir === resolve6(workspace) ? "." : dir;
1787
+ const entries = await readdir3(dir, { withFileTypes: true });
1788
+ const rel = dir === resolve7(workspace) ? "." : dir;
1674
1789
  const names = entries.slice(0, 40).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
1675
1790
  return `${rel}:
1676
1791
  ${names.join(" ") || "(empty)"}`;
@@ -1692,14 +1807,14 @@ function formatSchema(spec) {
1692
1807
  }
1693
1808
 
1694
1809
  // src/core/agent/project-context.ts
1695
- import { readFile as readFile5 } from "fs/promises";
1696
- import { join as join2 } from "path";
1697
- var INSTRUCTION_FILES = [join2(".poly", "agents.md"), "AGENTS.md"];
1810
+ import { readFile as readFile6 } from "fs/promises";
1811
+ import { join as join3 } from "path";
1812
+ var INSTRUCTION_FILES = [join3(".poly", "agents.md"), "AGENTS.md"];
1698
1813
  var MAX_CHARS2 = 8e3;
1699
1814
  async function loadProjectInstructions(workspace) {
1700
1815
  for (const rel of INSTRUCTION_FILES) {
1701
1816
  try {
1702
- const raw = (await readFile5(join2(workspace, rel), "utf8")).trim();
1817
+ const raw = (await readFile6(join3(workspace, rel), "utf8")).trim();
1703
1818
  if (!raw) continue;
1704
1819
  return raw.length > MAX_CHARS2 ? raw.slice(0, MAX_CHARS2) + "\n\u2026(truncated)" : raw;
1705
1820
  } catch {
@@ -1860,8 +1975,6 @@ ${guidance}`;
1860
1975
  }
1861
1976
 
1862
1977
  // src/ui/repl.ts
1863
- import * as readline from "readline/promises";
1864
- import { stdin, stdout } from "process";
1865
1978
  import pc6 from "picocolors";
1866
1979
 
1867
1980
  // src/ui/wizard.ts
@@ -2383,6 +2496,136 @@ function promptLabel(mode) {
2383
2496
  return c2("\u{1F419} polypus") + pc5.dim(`(${mode})`) + c3(" \u203A ");
2384
2497
  }
2385
2498
 
2499
+ // src/ui/line-reader.ts
2500
+ import * as readline from "readline/promises";
2501
+ import { PassThrough } from "stream";
2502
+ import { stdin, stdout } from "process";
2503
+
2504
+ // src/ui/paste.ts
2505
+ var PASTE_START = "\x1B[200~";
2506
+ var PASTE_END = "\x1B[201~";
2507
+ var PasteStore = class {
2508
+ /** `format(id, lines)` builds the placeholder (localized by the caller). */
2509
+ constructor(format) {
2510
+ this.format = format;
2511
+ }
2512
+ format;
2513
+ seq = 0;
2514
+ map = /* @__PURE__ */ new Map();
2515
+ /** Register pasted text, returning the placeholder to display in its place. */
2516
+ add(text2) {
2517
+ const lines = text2.split(/\r\n|\r|\n/).length;
2518
+ const placeholder = this.format(++this.seq, lines);
2519
+ this.map.set(placeholder, text2);
2520
+ return placeholder;
2521
+ }
2522
+ /** Replace any known placeholders in `line` with their full pasted text. */
2523
+ expand(line) {
2524
+ let out = line;
2525
+ for (const [placeholder, full] of this.map) {
2526
+ if (out.includes(placeholder)) out = out.split(placeholder).join(full);
2527
+ }
2528
+ return out;
2529
+ }
2530
+ get size() {
2531
+ return this.map.size;
2532
+ }
2533
+ };
2534
+ var PasteFilter = class {
2535
+ constructor(store) {
2536
+ this.store = store;
2537
+ }
2538
+ store;
2539
+ buf = "";
2540
+ inPaste = false;
2541
+ pasteBuf = "";
2542
+ push(chunk) {
2543
+ this.buf += chunk;
2544
+ let out = "";
2545
+ for (; ; ) {
2546
+ if (!this.inPaste) {
2547
+ const i = this.buf.indexOf(PASTE_START);
2548
+ if (i === -1) {
2549
+ const keep = partialSuffix(this.buf, PASTE_START);
2550
+ out += this.buf.slice(0, this.buf.length - keep);
2551
+ this.buf = this.buf.slice(this.buf.length - keep);
2552
+ return out;
2553
+ }
2554
+ out += this.buf.slice(0, i);
2555
+ this.buf = this.buf.slice(i + PASTE_START.length);
2556
+ this.inPaste = true;
2557
+ } else {
2558
+ const j = this.buf.indexOf(PASTE_END);
2559
+ if (j === -1) {
2560
+ const keep = partialSuffix(this.buf, PASTE_END);
2561
+ this.pasteBuf += this.buf.slice(0, this.buf.length - keep);
2562
+ this.buf = this.buf.slice(this.buf.length - keep);
2563
+ return out;
2564
+ }
2565
+ this.pasteBuf += this.buf.slice(0, j);
2566
+ this.buf = this.buf.slice(j + PASTE_END.length);
2567
+ this.inPaste = false;
2568
+ out += this.emit(this.pasteBuf);
2569
+ this.pasteBuf = "";
2570
+ }
2571
+ }
2572
+ }
2573
+ /** Multi-line pastes become a placeholder; single-line pastes pass through. */
2574
+ emit(text2) {
2575
+ return /\r|\n/.test(text2) ? this.store.add(text2) : text2;
2576
+ }
2577
+ };
2578
+ function partialSuffix(s, marker) {
2579
+ const max = Math.min(s.length, marker.length - 1);
2580
+ for (let n = max; n > 0; n--) {
2581
+ if (s.slice(s.length - n) === marker.slice(0, n)) return n;
2582
+ }
2583
+ return 0;
2584
+ }
2585
+
2586
+ // src/ui/line-reader.ts
2587
+ var ENABLE_BRACKETED_PASTE = "\x1B[?2004h";
2588
+ var DISABLE_BRACKETED_PASTE = "\x1B[?2004l";
2589
+ async function readLine(prompt) {
2590
+ if (!stdin.isTTY) {
2591
+ const rl = readline.createInterface({ input: stdin, output: stdout });
2592
+ try {
2593
+ return await rl.question(prompt);
2594
+ } catch {
2595
+ return null;
2596
+ } finally {
2597
+ rl.close();
2598
+ }
2599
+ }
2600
+ return readLineTTY(prompt);
2601
+ }
2602
+ async function readLineTTY(prompt) {
2603
+ const store = new PasteStore((id, lines) => t("repl.pasted", { id, lines }));
2604
+ const filter = new PasteFilter(store);
2605
+ const proxy = new PassThrough();
2606
+ const rl = readline.createInterface({ input: proxy, output: stdout, terminal: true });
2607
+ const onData = (buf) => {
2608
+ proxy.write(filter.push(buf.toString("utf8")));
2609
+ };
2610
+ stdout.write(ENABLE_BRACKETED_PASTE);
2611
+ stdin.setRawMode(true);
2612
+ stdin.resume();
2613
+ stdin.on("data", onData);
2614
+ try {
2615
+ const line = await new Promise((resolve9) => {
2616
+ rl.question(prompt).then(resolve9, () => resolve9(null));
2617
+ rl.on("SIGINT", () => resolve9(null));
2618
+ rl.on("close", () => resolve9(null));
2619
+ });
2620
+ return line === null ? null : store.expand(line);
2621
+ } finally {
2622
+ stdin.off("data", onData);
2623
+ if (stdin.isTTY) stdin.setRawMode(false);
2624
+ stdout.write(DISABLE_BRACKETED_PASTE);
2625
+ rl.close();
2626
+ }
2627
+ }
2628
+
2386
2629
  // src/ui/repl.ts
2387
2630
  async function startRepl(ctx) {
2388
2631
  for (; ; ) {
@@ -2412,16 +2655,6 @@ async function startRepl(ctx) {
2412
2655
  await handleCommand(cmd, arg, ctx);
2413
2656
  }
2414
2657
  }
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
2658
  async function handleCommand(cmd, arg, ctx) {
2426
2659
  const { session } = ctx;
2427
2660
  switch (cmd) {
@@ -2446,6 +2679,18 @@ async function handleCommand(cmd, arg, ctx) {
2446
2679
  session.history = [];
2447
2680
  console.log(pc6.dim(t("repl.historyCleared")));
2448
2681
  return;
2682
+ case "swarm": {
2683
+ if (!arg) {
2684
+ console.log(pc6.yellow(t("repl.needName", { usage: "/swarm <task>" })));
2685
+ return;
2686
+ }
2687
+ try {
2688
+ await ctx.runSwarm(arg);
2689
+ } catch (e) {
2690
+ console.log(pc6.red(`\u2717 ${e.message}`));
2691
+ }
2692
+ return;
2693
+ }
2449
2694
  case "agents":
2450
2695
  printAgents(ctx.getConfig(), session.agentName);
2451
2696
  return;
@@ -2509,113 +2754,525 @@ async function removeAgent2(name, ctx) {
2509
2754
  }
2510
2755
  }
2511
2756
 
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;
2757
+ // src/cli/commands/swarm.ts
2758
+ import pc7 from "picocolors";
2759
+
2760
+ // src/core/git/worktree.ts
2761
+ import { mkdtemp } from "fs/promises";
2762
+ import { tmpdir } from "os";
2763
+ import { join as join4 } from "path";
2764
+ import { simpleGit } from "simple-git";
2765
+ async function ensureRepo(workspace) {
2766
+ const git = simpleGit(workspace);
2767
+ if (!await git.checkIsRepo()) {
2768
+ await git.init();
2527
2769
  }
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?.();
2770
+ const identity = await identityArgs(git);
2771
+ const hasHead = await git.raw(["rev-parse", "--verify", "HEAD"]).then(() => true).catch(() => false);
2772
+ if (!hasHead) {
2773
+ await git.raw([...identity, "commit", "--allow-empty", "-m", "polypus: initial commit"]);
2537
2774
  }
2538
- /** Erase the spinner line and stop animating. */
2539
- stop() {
2540
- if (this.timer) {
2541
- clearInterval(this.timer);
2542
- this.timer = void 0;
2775
+ return git;
2776
+ }
2777
+ async function identityArgs(git) {
2778
+ const email = await git.raw(["config", "user.email"]).catch(() => "");
2779
+ if (email.trim()) return [];
2780
+ return ["-c", "user.email=polypus@local", "-c", "user.name=Polypus"];
2781
+ }
2782
+ async function createWorktree(git, label) {
2783
+ const branch = `polypus/${label}-${Date.now().toString(36)}`;
2784
+ const path = await mkdtemp(join4(tmpdir(), "polypus-wt-"));
2785
+ await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
2786
+ return { path, branch };
2787
+ }
2788
+ async function commitWorktree(wt, message) {
2789
+ const wtGit = simpleGit(wt.path);
2790
+ await wtGit.add(["-A"]);
2791
+ const status = await wtGit.status();
2792
+ if (status.staged.length === 0 && status.files.length === 0) return false;
2793
+ const identity = await identityArgs(wtGit);
2794
+ await wtGit.raw([...identity, "commit", "-m", message]);
2795
+ return true;
2796
+ }
2797
+ async function mergeWorktreeBranch(git, branch) {
2798
+ try {
2799
+ const identity = await identityArgs(git);
2800
+ await git.raw([...identity, "merge", "--no-edit", branch]);
2801
+ return { branch, ok: true, conflicts: [] };
2802
+ } catch (err) {
2803
+ const status = await git.status().catch(() => void 0);
2804
+ const conflicts = status?.conflicted ?? [];
2805
+ await git.raw(["merge", "--abort"]).catch(() => void 0);
2806
+ if (conflicts.length === 0) {
2807
+ throw err;
2543
2808
  }
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}`);
2809
+ return { branch, ok: false, conflicts };
2552
2810
  }
2553
- };
2811
+ }
2812
+ async function removeWorktree(git, wt) {
2813
+ await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
2814
+ await git.raw(["branch", "-D", wt.branch]).catch(() => void 0);
2815
+ }
2554
2816
 
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
2817
+ // src/core/agent/worker.ts
2818
+ async function runWorker(subtask, agent, wt, allow, deny, events) {
2819
+ const permissions = new PermissionEngine({
2820
+ mode: "bypass",
2821
+ policy: { workspace: wt.path, allow, deny },
2822
+ allowedCommands: []
2598
2823
  });
2599
- const ctx = {
2600
- session,
2601
- runTask,
2602
- getConfig: () => config,
2603
- reload: async () => {
2604
- config = await loadConfig();
2605
- }
2824
+ const result = await runAgent({
2825
+ task: subtask.brief,
2826
+ workspace: wt.path,
2827
+ agent,
2828
+ permissions,
2829
+ promptContext: { workspace: wt.path, mode: "bypass", allow, briefing: subtask.brief },
2830
+ events
2831
+ });
2832
+ const committed = await commitWorktree(wt, `polypus(${subtask.id}): ${subtask.title}`);
2833
+ return {
2834
+ subtask,
2835
+ agentName: agent.config.name,
2836
+ branch: wt.branch,
2837
+ finished: result.finished,
2838
+ summary: result.summary,
2839
+ committed,
2840
+ steps: result.steps
2606
2841
  };
2607
- await startRepl(ctx);
2608
2842
  }
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();
2843
+
2844
+ // src/core/agent/orchestrator.ts
2845
+ async function runSwarm(opts) {
2846
+ const lead = opts.agents[0];
2847
+ if (!lead) throw new Error("Swarm requires at least one agent.");
2848
+ const maxSubtasks = opts.maxSubtasks ?? Math.max(opts.agents.length, 2);
2849
+ const git = await ensureRepo(opts.workspace);
2850
+ const subtasks = await decompose(lead, opts.task, maxSubtasks);
2851
+ opts.events?.onDecomposed?.(subtasks);
2852
+ const worktrees = [];
2853
+ for (const subtask of subtasks) {
2854
+ worktrees.push(await createWorktree(git, subtask.id));
2855
+ }
2856
+ const outcomes = await Promise.all(
2857
+ subtasks.map(async (subtask, i) => {
2858
+ const agent = opts.agents[i % opts.agents.length];
2859
+ const wt = worktrees[i];
2860
+ opts.events?.onWorkerStart?.(subtask, agent.config.name);
2861
+ const outcome = await runWorker(
2862
+ subtask,
2863
+ agent,
2864
+ wt,
2865
+ opts.allow,
2866
+ opts.deny,
2867
+ opts.events?.workerEvents?.(subtask)
2868
+ );
2869
+ opts.events?.onWorkerDone?.(outcome);
2870
+ return outcome;
2871
+ })
2872
+ );
2873
+ const merges = [];
2874
+ for (const outcome of outcomes) {
2875
+ if (!outcome.committed) continue;
2876
+ const merge = await mergeWorktreeBranch(git, outcome.branch);
2877
+ merges.push(merge);
2878
+ opts.events?.onMerge?.(merge);
2879
+ }
2880
+ const conflicted = new Set(merges.filter((m) => !m.ok).map((m) => m.branch));
2881
+ for (const wt of worktrees) {
2882
+ if (conflicted.has(wt.branch)) {
2883
+ await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
2884
+ } else {
2885
+ await removeWorktree(git, wt);
2886
+ }
2887
+ }
2888
+ return { subtasks, outcomes, merges };
2889
+ }
2890
+ var DECOMPOSE_SYSTEM = [
2891
+ "You are a tech lead splitting a coding task into independent subtasks that can be done in parallel.",
2892
+ 'Return ONLY a JSON array. Each item: {"title": string, "brief": string}.',
2893
+ "Make subtasks touch DIFFERENT files/areas to minimize merge conflicts.",
2894
+ "Keep the list small (prefer 2-4 items). Each brief must be self-contained and actionable."
2895
+ ].join("\n");
2896
+ async function decompose(lead, task, maxSubtasks) {
2897
+ try {
2898
+ const res = await lead.provider.chat({
2899
+ messages: [
2900
+ { role: "system", content: DECOMPOSE_SYSTEM },
2901
+ { role: "user", content: `Task:
2902
+ ${task}
2903
+
2904
+ Return at most ${maxSubtasks} subtasks as a JSON array.` }
2905
+ ],
2906
+ params: { temperature: 0 }
2907
+ });
2908
+ const parsed = extractJsonArray(res.content);
2909
+ if (parsed && parsed.length > 0) {
2910
+ return parsed.slice(0, maxSubtasks).map((item, i) => ({
2911
+ id: `t${i + 1}`,
2912
+ title: String(item.title ?? `subtask ${i + 1}`),
2913
+ brief: String(item.brief ?? item.title ?? task)
2914
+ }));
2915
+ }
2916
+ } catch {
2917
+ }
2918
+ return [{ id: "t1", title: "task", brief: task }];
2919
+ }
2920
+ function extractJsonArray(text2) {
2921
+ const start = text2.indexOf("[");
2922
+ const end = text2.lastIndexOf("]");
2923
+ if (start === -1 || end <= start) return null;
2924
+ try {
2925
+ const parsed = JSON.parse(text2.slice(start, end + 1));
2926
+ return Array.isArray(parsed) ? parsed : null;
2927
+ } catch {
2928
+ return null;
2929
+ }
2930
+ }
2931
+
2932
+ // src/ui/swarm-view.ts
2933
+ var RESET2 = "\x1B[0m";
2934
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2935
+ function describeToolCall(call) {
2936
+ const raw = call.name === "run_command" ? call.arguments.command : call.arguments.path;
2937
+ const arg = typeof raw === "string" ? raw : "";
2938
+ const short = arg.length > 40 ? arg.slice(0, 39) + "\u2026" : arg;
2939
+ return short ? `${call.name} ${short}` : call.name;
2940
+ }
2941
+ var SwarmView = class {
2942
+ constructor(leadName, opts = {}) {
2943
+ this.leadName = leadName;
2944
+ this.tty = opts.tty ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
2945
+ this.color = opts.color ?? this.tty;
2946
+ this.write = opts.sink ?? ((s) => process.stdout.write(s));
2947
+ }
2948
+ leadName;
2949
+ tty;
2950
+ color;
2951
+ write;
2952
+ workers = /* @__PURE__ */ new Map();
2953
+ order = [];
2954
+ phase = "decomposing";
2955
+ frame = 0;
2956
+ lastLines = 0;
2957
+ timer;
2958
+ start() {
2959
+ if (!this.tty) {
2960
+ this.write(`\u{1F419} ${t("swarm.view.header", { lead: this.leadName })} \u2014 ${t("swarm.view.decomposing")}
2961
+ `);
2962
+ return;
2963
+ }
2964
+ this.flush();
2965
+ this.timer = setInterval(() => {
2966
+ this.frame = (this.frame + 1) % FRAMES.length;
2967
+ this.flush();
2968
+ }, 110);
2969
+ this.timer.unref?.();
2970
+ }
2971
+ setSubtasks(subtasks) {
2972
+ this.phase = "running";
2973
+ for (const s of subtasks) {
2974
+ this.workers.set(s.id, { id: s.id, title: s.title, agent: "", status: "pending", action: "", steps: 0 });
2975
+ this.order.push(s.id);
2976
+ }
2977
+ if (!this.tty) {
2978
+ this.write(` ${t("swarm.decomposed", { n: subtasks.length })}
2979
+ `);
2980
+ for (const s of subtasks) this.write(` ${s.id}: ${s.title}
2981
+ `);
2982
+ }
2983
+ this.flush();
2984
+ }
2985
+ workerStart(id, agent) {
2986
+ const w = this.workers.get(id);
2987
+ if (!w) return;
2988
+ w.agent = agent;
2989
+ w.status = "running";
2990
+ if (!this.tty) this.write(` \u25B6 ${id} [${agent}] ${w.title}
2991
+ `);
2992
+ this.flush();
2993
+ }
2994
+ workerAction(id, action) {
2995
+ const w = this.workers.get(id);
2996
+ if (!w) return;
2997
+ w.action = action;
2998
+ this.flush();
2999
+ }
3000
+ workerStep(id, n) {
3001
+ const w = this.workers.get(id);
3002
+ if (!w) return;
3003
+ w.steps = n;
3004
+ this.flush();
3005
+ }
3006
+ workerDone(o) {
3007
+ const w = this.workers.get(o.subtask.id);
3008
+ if (!w) return;
3009
+ w.status = o.finished ? "done" : "stopped";
3010
+ w.steps = o.steps;
3011
+ w.branch = o.branch;
3012
+ w.action = "";
3013
+ if (!this.tty) {
3014
+ const tag = o.finished ? "\u2713" : "\u25A0";
3015
+ const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
3016
+ this.write(` ${tag} ${o.subtask.id} (${t("swarm.view.steps", { n: o.steps })}, ${changes})
3017
+ `);
3018
+ }
3019
+ this.flush();
3020
+ }
3021
+ merge(r) {
3022
+ for (const w of this.workers.values()) {
3023
+ if (w.branch === r.branch) w.merge = r.ok ? "ok" : "conflict";
3024
+ }
3025
+ if (!this.tty) {
3026
+ this.write(r.ok ? ` \u2935 ${t("swarm.merged", { branch: r.branch })}
3027
+ ` : ` \u2717 ${t("swarm.mergeConflict", { branch: r.branch })}
3028
+ `);
3029
+ }
3030
+ this.flush();
3031
+ }
3032
+ stop() {
3033
+ this.phase = "done";
3034
+ if (this.timer) {
3035
+ clearInterval(this.timer);
3036
+ this.timer = void 0;
3037
+ }
3038
+ this.flush();
3039
+ }
3040
+ /** Content lines of the dashboard (no cursor control). Exposed for tests. */
3041
+ frameLines() {
3042
+ const spin = this.dim(FRAMES[this.frame]);
3043
+ const lead = `\u{1F419} ${t("swarm.view.header", { lead: this.leadName })}`;
3044
+ const lines = [];
3045
+ if (this.phase === "decomposing") {
3046
+ lines.push(`${spin} ${lead}`);
3047
+ lines.push(" " + this.dim(t("swarm.view.decomposing")));
3048
+ return lines;
3049
+ }
3050
+ lines.push(`${this.phase === "running" ? spin : " "} ${lead}`);
3051
+ lines.push("");
3052
+ for (const id of this.order) {
3053
+ const w = this.workers.get(id);
3054
+ lines.push(this.row(w, spin));
3055
+ }
3056
+ return lines;
3057
+ }
3058
+ // -------------------------------------------------------------------------
3059
+ row(w, spin) {
3060
+ const icon = w.status === "running" ? spin : w.status === "done" ? this.c("\u2713", "32") : w.status === "stopped" ? this.c("\u25A0", "33") : this.dim("\xB7");
3061
+ const status = this.statusLabel(w);
3062
+ 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 }))) : "";
3063
+ const action = w.action ? w.action : this.dim("\u2014");
3064
+ return ` ${icon} ${pad(w.id, 4)} ${pad(status, 12)} ${pad(`[${w.agent}]`, 14)} ${action}${meta}`;
3065
+ }
3066
+ statusLabel(w) {
3067
+ if (w.merge === "conflict") return this.c(t("swarm.view.conflict"), "31");
3068
+ if (w.status === "running") return this.c(t("swarm.view.running"), "36");
3069
+ if (w.status === "done") return this.c(t("swarm.view.done"), "32");
3070
+ if (w.status === "stopped") return this.c(t("swarm.view.stopped"), "33");
3071
+ return this.dim(t("swarm.view.pending"));
3072
+ }
3073
+ /** Redraw the block in place (TTY) by clearing the previous frame first. */
3074
+ flush() {
3075
+ if (!this.tty) return;
3076
+ const lines = this.frameLines();
3077
+ let s = "";
3078
+ if (this.lastLines > 0) s += `\x1B[${this.lastLines}A`;
3079
+ s += "\x1B[0J";
3080
+ s += lines.join("\n") + "\n";
3081
+ this.write(s);
3082
+ this.lastLines = lines.length;
3083
+ }
3084
+ c(s, code) {
3085
+ return this.color ? `\x1B[${code}m${s}${RESET2}` : s;
3086
+ }
3087
+ dim(s) {
3088
+ return this.color ? `\x1B[2m${s}${RESET2}` : s;
3089
+ }
3090
+ };
3091
+ function pad(s, n) {
3092
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
3093
+ }
3094
+
3095
+ // src/cli/commands/swarm.ts
3096
+ var MIN_SWARM_AGENTS = 3;
3097
+ function canSwarm(agentCount) {
3098
+ return agentCount >= MIN_SWARM_AGENTS;
3099
+ }
3100
+ async function runSwarmSession(task, config, opts = {}) {
3101
+ if (!canSwarm(config.agents.length)) {
3102
+ throw new Error(t("swarm.needsAgents", { min: MIN_SWARM_AGENTS, have: config.agents.length }));
3103
+ }
3104
+ const workspace = opts.workspace ?? process.cwd();
3105
+ const selected = opts.agents?.length ? opts.agents : config.agents.map((a) => a.name);
3106
+ if (selected.length === 0) {
3107
+ throw new Error(t("swarm.noAgents"));
3108
+ }
3109
+ const resolved = selected.map((name) => {
3110
+ const a = config.agents.find((x) => x.name === name);
3111
+ if (!a) throw new Error(t("agent.notFound", { name }));
3112
+ return createProvider(a);
3113
+ });
3114
+ console.log(
3115
+ pc7.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace }))
3116
+ );
3117
+ console.log(pc7.yellow(t("swarm.bypassNote") + "\n"));
3118
+ const view = new SwarmView(resolved[0].config.name);
3119
+ view.start();
3120
+ let result;
3121
+ try {
3122
+ result = await runSwarm({
3123
+ task,
3124
+ workspace,
3125
+ agents: resolved,
3126
+ allow: config.permissions.allow,
3127
+ deny: config.permissions.deny,
3128
+ maxSubtasks: opts.maxSubtasks,
3129
+ events: {
3130
+ onDecomposed: (subtasks) => view.setSubtasks(subtasks),
3131
+ onWorkerStart: (subtask, agentName) => view.workerStart(subtask.id, agentName),
3132
+ onWorkerDone: (outcome) => view.workerDone(outcome),
3133
+ onMerge: (merge) => view.merge(merge),
3134
+ workerEvents: (subtask) => ({
3135
+ onToolCall: (call) => view.workerAction(subtask.id, describeToolCall(call)),
3136
+ onStep: (step) => view.workerStep(subtask.id, step)
3137
+ })
3138
+ }
3139
+ });
3140
+ } finally {
3141
+ view.stop();
3142
+ }
3143
+ console.log("");
3144
+ console.log(pc7.bold("\n" + t("swarm.summary")));
3145
+ for (const o of result.outcomes) {
3146
+ const status = o.finished ? pc7.green(t("swarm.statusDone")) : pc7.yellow(t("swarm.statusIncomplete"));
3147
+ const committed = o.committed ? "" : pc7.dim(` (${t("swarm.noChanges")})`);
3148
+ console.log(` ${pc7.bold(o.subtask.id)} [${o.agentName}] ${status}${committed} \u2014 ${o.subtask.title}`);
3149
+ }
3150
+ const conflicts = result.merges.filter((m) => !m.ok);
3151
+ if (conflicts.length > 0) {
3152
+ console.log(pc7.red("\n" + t("swarm.conflictsHeader", { n: conflicts.length })));
3153
+ for (const m of conflicts) {
3154
+ console.log(pc7.red(` ${m.branch}: ${m.conflicts.join(", ")}`));
3155
+ }
3156
+ } else {
3157
+ console.log(pc7.green("\n" + t("swarm.allMerged")));
3158
+ }
3159
+ }
3160
+ async function swarm(task, opts) {
3161
+ const config = await loadConfig();
3162
+ await runSwarmSession(task, config, {
3163
+ agents: opts.agents ? opts.agents.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
3164
+ maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0
3165
+ });
3166
+ }
3167
+
3168
+ // src/ui/spinner.ts
3169
+ var RESET3 = "\x1B[0m";
3170
+ var isTTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
3171
+ var FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3172
+ var violet = (s) => isTTY ? `\x1B[38;2;167;139;250m${s}${RESET3}` : s;
3173
+ var dim = (s) => isTTY ? `\x1B[2m${s}${RESET3}` : s;
3174
+ var Spinner = class {
3175
+ timer;
3176
+ frame = 0;
3177
+ startedAt = 0;
3178
+ label = "";
3179
+ suffix = "";
3180
+ /** Extra dim text appended after the elapsed time (e.g. token count). */
3181
+ setSuffix(suffix) {
3182
+ this.suffix = suffix;
3183
+ }
3184
+ /** Start (or, if already running, just update the label). */
3185
+ start(label) {
3186
+ this.label = label;
3187
+ if (!isTTY) return;
3188
+ if (this.timer) return;
3189
+ this.startedAt = Date.now();
3190
+ this.render();
3191
+ this.timer = setInterval(() => this.render(), 90);
3192
+ this.timer.unref?.();
3193
+ }
3194
+ /** Erase the spinner line and stop animating. */
3195
+ stop() {
3196
+ if (this.timer) {
3197
+ clearInterval(this.timer);
3198
+ this.timer = void 0;
3199
+ }
3200
+ if (isTTY) process.stdout.write("\r\x1B[K");
3201
+ }
3202
+ render() {
3203
+ const f = violet(FRAMES2[this.frame = (this.frame + 1) % FRAMES2.length]);
3204
+ const secs = Math.floor((Date.now() - this.startedAt) / 1e3);
3205
+ const time = secs > 0 ? dim(` (${secs}s)`) : "";
3206
+ const suffix = this.suffix ? dim(` \xB7 ${this.suffix}`) : "";
3207
+ process.stdout.write(`\r\x1B[K${f} \u{1F419} ${dim(this.label + "\u2026")}${time}${suffix}`);
3208
+ }
3209
+ };
3210
+
3211
+ // src/cli/commands/run.ts
3212
+ async function run(task, opts) {
3213
+ let config = await loadConfig();
3214
+ const agentConfig = resolveAgent(config, opts.agent);
3215
+ const workspace = process.cwd();
3216
+ const session = {
3217
+ agentName: agentConfig.name,
3218
+ mode: opts.mode ?? config.permissions.mode,
3219
+ allow: config.permissions.allow,
3220
+ deny: config.permissions.deny,
3221
+ allowedCommands: config.permissions.allowedCommands,
3222
+ maxSteps: opts.maxSteps ? Number(opts.maxSteps) : void 0,
3223
+ history: []
3224
+ };
3225
+ const runTask = async (taskText) => {
3226
+ const active = resolveAgent(config, session.agentName);
3227
+ const resolved2 = createProvider(active);
3228
+ await executeTask(taskText, resolved2, workspace, session);
3229
+ };
3230
+ if (task) {
3231
+ const resolved2 = createProvider(agentConfig);
3232
+ console.log(
3233
+ pc8.dim(
3234
+ t("run.status", {
3235
+ name: resolved2.config.name,
3236
+ provider: resolved2.config.provider,
3237
+ model: resolved2.config.model,
3238
+ toolMode: resolved2.toolMode,
3239
+ mode: session.mode
3240
+ })
3241
+ )
3242
+ );
3243
+ await executeTask(task, resolved2, workspace, session);
3244
+ return;
3245
+ }
3246
+ const resolved = createProvider(agentConfig);
3247
+ await printWelcome({
3248
+ agentName: resolved.config.name,
3249
+ provider: resolved.config.provider,
3250
+ model: resolved.config.model,
3251
+ toolMode: resolved.toolMode,
3252
+ mode: session.mode,
3253
+ workspace
3254
+ });
3255
+ const ctx = {
3256
+ session,
3257
+ runTask,
3258
+ runSwarm: (taskText) => runSwarmSession(taskText, config, { workspace }),
3259
+ getConfig: () => config,
3260
+ reload: async () => {
3261
+ config = await loadConfig();
3262
+ }
3263
+ };
3264
+ await startRepl(ctx);
3265
+ }
3266
+ async function executeTask(task, resolved, workspace, session) {
3267
+ const spinner3 = new Spinner();
3268
+ const controller = new AbortController();
3269
+ const cancel2 = listenForCancel(controller);
3270
+ const permissions = new PermissionEngine({
3271
+ mode: session.mode,
3272
+ policy: { workspace, allow: session.allow, deny: session.deny },
3273
+ allowedCommands: session.allowedCommands,
3274
+ confirm: async (req) => {
3275
+ spinner3.stop();
2619
3276
  cancel2.pause();
2620
3277
  const ok = await confirmAction(req);
2621
3278
  cancel2.resume();
@@ -2642,16 +3299,16 @@ async function executeTask(task, resolved, workspace, session) {
2642
3299
  }
2643
3300
  session.history = result.messages;
2644
3301
  if (result.reason === "finished") {
2645
- console.log(pc7.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
3302
+ console.log(pc8.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
2646
3303
  } else if (result.reason === "cancelled") {
2647
- console.log(pc7.dim("\n" + t("run.cancelled")));
3304
+ console.log(pc8.dim("\n" + t("run.cancelled")));
2648
3305
  } else if (result.reason === "stalled" || result.reason === "maxsteps") {
2649
- console.log(pc7.yellow("\n" + t("run.stopped", { steps: result.steps })));
3306
+ console.log(pc8.yellow("\n" + t("run.stopped", { steps: result.steps })));
2650
3307
  }
2651
3308
  if (result.usage.promptTokens || result.usage.completionTokens) {
2652
3309
  const total = result.usage.promptTokens + result.usage.completionTokens;
2653
3310
  console.log(
2654
- pc7.dim(
3311
+ pc8.dim(
2655
3312
  "\u21B3 " + t("ui.tokens", {
2656
3313
  total: fmtTokens(total),
2657
3314
  in: fmtTokens(result.usage.promptTokens),
@@ -2692,7 +3349,7 @@ function listenForCancel(controller) {
2692
3349
  return { pause: detach, resume: attach, dispose: detach };
2693
3350
  }
2694
3351
  async function confirmAction(req) {
2695
- if (req.preview) console.log(pc7.dim(req.preview));
3352
+ if (req.preview) console.log(pc8.dim(req.preview));
2696
3353
  const answer = await p2.confirm({ message: t("run.confirm", { summary: req.summary }) });
2697
3354
  if (p2.isCancel(answer)) return false;
2698
3355
  return answer === true;
@@ -2708,26 +3365,26 @@ function renderEvents(spinner3) {
2708
3365
  },
2709
3366
  onAssistantText(text2) {
2710
3367
  spinner3.stop();
2711
- if (text2.trim()) console.log(pc7.cyan(text2.trim()));
3368
+ if (text2.trim()) console.log(pc8.cyan(text2.trim()));
2712
3369
  },
2713
3370
  onToolCall(call) {
2714
3371
  spinner3.stop();
2715
3372
  const arg = call.name === "run_command" ? call.arguments.command : call.arguments.path;
2716
- console.log(pc7.dim(` \u2192 ${call.name}${arg ? ` ${String(arg)}` : ""}`));
3373
+ console.log(pc8.dim(` \u2192 ${call.name}${arg ? ` ${String(arg)}` : ""}`));
2717
3374
  spinner3.start(t("ui.running", { tool: call.name }));
2718
3375
  },
2719
3376
  onToolResult(_call, result) {
2720
3377
  spinner3.stop();
2721
3378
  const head = result.output.split("\n")[0] ?? "";
2722
- console.log((result.ok ? pc7.green(" \u2713 ") : pc7.red(" \u2717 ")) + pc7.dim(head.slice(0, 120)));
3379
+ console.log((result.ok ? pc8.green(" \u2713 ") : pc8.red(" \u2717 ")) + pc8.dim(head.slice(0, 120)));
2723
3380
  },
2724
3381
  onReprompt(attempt) {
2725
3382
  spinner3.stop();
2726
- console.log(pc7.yellow(" " + t("run.reprompt", { attempt })));
3383
+ console.log(pc8.yellow(" " + t("run.reprompt", { attempt })));
2727
3384
  },
2728
3385
  onCorrection() {
2729
3386
  spinner3.stop();
2730
- console.log(pc7.yellow(" \u21BB " + t("run.autocorrect")));
3387
+ console.log(pc8.yellow(" \u21BB " + t("run.autocorrect")));
2731
3388
  }
2732
3389
  };
2733
3390
  }
@@ -2738,11 +3395,11 @@ async function setup() {
2738
3395
  }
2739
3396
 
2740
3397
  // src/cli/commands/init.ts
2741
- import pc8 from "picocolors";
3398
+ import pc9 from "picocolors";
2742
3399
 
2743
3400
  // src/core/scaffold/init.ts
2744
3401
  import { mkdir as mkdir3, writeFile as writeFile4, access } from "fs/promises";
2745
- import { dirname as dirname3, join as join3 } from "path";
3402
+ import { dirname as dirname3, join as join5 } from "path";
2746
3403
 
2747
3404
  // src/core/scaffold/templates.ts
2748
3405
  function polyTemplates(locale) {
@@ -2976,458 +3633,54 @@ A mudan\xE7a em termos simples \u2014 o comportamento que o usu\xE1rio vai realm
2976
3633
 
2977
3634
  - \u2026
2978
3635
  `
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}
3636
+ };
3175
3637
 
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
- }));
3638
+ // src/core/scaffold/init.ts
3639
+ async function scaffoldPoly(workspace, opts) {
3640
+ const templates = polyTemplates(opts.locale);
3641
+ const created = [];
3642
+ const skipped = [];
3643
+ for (const [rel, content] of Object.entries(templates)) {
3644
+ const display = `.poly/${rel}`;
3645
+ const abs = join5(workspace, ".poly", ...rel.split("/"));
3646
+ if (!opts.force && await exists(abs)) {
3647
+ skipped.push(display);
3648
+ continue;
3187
3649
  }
3188
- } catch {
3650
+ await mkdir3(dirname3(abs), { recursive: true });
3651
+ await writeFile4(abs, content, "utf8");
3652
+ created.push(display);
3189
3653
  }
3190
- return [{ id: "t1", title: "task", brief: task }];
3654
+ return { created, skipped };
3191
3655
  }
3192
- function extractJsonArray(text2) {
3193
- const start = text2.indexOf("[");
3194
- const end = text2.lastIndexOf("]");
3195
- if (start === -1 || end <= start) return null;
3656
+ async function exists(path) {
3196
3657
  try {
3197
- const parsed = JSON.parse(text2.slice(start, end + 1));
3198
- return Array.isArray(parsed) ? parsed : null;
3658
+ await access(path);
3659
+ return true;
3199
3660
  } 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;
3661
+ return false;
3361
3662
  }
3362
- };
3363
- function pad(s, n) {
3364
- return s.length >= n ? s : s + " ".repeat(n - s.length);
3365
3663
  }
3366
3664
 
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);
3665
+ // src/cli/commands/init.ts
3666
+ async function init(opts) {
3667
+ const { created, skipped } = await scaffoldPoly(process.cwd(), {
3668
+ force: Boolean(opts.force),
3669
+ locale: getLocale()
3385
3670
  });
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}`);
3671
+ if (created.length === 0) {
3672
+ console.log(pc9.yellow(t("init.allExist")));
3673
+ for (const f of skipped) console.log(pc9.dim(` ${f}`));
3674
+ console.log(pc9.dim(t("init.forceHint")));
3675
+ return;
3421
3676
  }
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")));
3677
+ console.log(pc9.green(t("init.created")));
3678
+ for (const f of created) console.log(pc9.dim(` ${f}`));
3679
+ if (skipped.length > 0) {
3680
+ console.log(pc9.dim(t("init.skipped")));
3681
+ for (const f of skipped) console.log(pc9.dim(` ${f}`));
3430
3682
  }
3683
+ console.log("\n" + t("init.tip"));
3431
3684
  }
3432
3685
 
3433
3686
  // src/cli/commands/models.ts
@@ -3489,7 +3742,7 @@ async function resolveOpenRouterKey() {
3489
3742
  }
3490
3743
 
3491
3744
  // src/cli/commands/prd.ts
3492
- import { writeFile as writeFile5, readFile as readFile6 } from "fs/promises";
3745
+ import { writeFile as writeFile5, readFile as readFile7 } from "fs/promises";
3493
3746
  import { execFile } from "child_process";
3494
3747
  import { promisify as promisify2 } from "util";
3495
3748
  import pc11 from "picocolors";
@@ -3581,13 +3834,13 @@ async function withRetry(fn, opts = {}) {
3581
3834
 
3582
3835
  // src/cli/commands/cli-io.ts
3583
3836
  import { readFileSync, existsSync as existsSync2 } from "fs";
3584
- import { resolve as resolve7 } from "path";
3837
+ import { resolve as resolve8 } from "path";
3585
3838
  var GUIDE_MAX = 12e3;
3586
3839
  function readProjectGuide(files) {
3587
3840
  const parts = [];
3588
3841
  for (const file of files) {
3589
3842
  try {
3590
- const path = resolve7(process.cwd(), file);
3843
+ const path = resolve8(process.cwd(), file);
3591
3844
  if (existsSync2(path)) parts.push(`# ${file}
3592
3845
  ${readFileSync(path, "utf8").trim()}`);
3593
3846
  } catch {
@@ -3628,7 +3881,7 @@ async function prd(issueRef, opts) {
3628
3881
  }
3629
3882
  async function loadIssue(issueRef, input) {
3630
3883
  if (input) {
3631
- const raw = input === "-" ? await readStdin() : await readFile6(input, "utf8");
3884
+ const raw = input === "-" ? await readStdin() : await readFile7(input, "utf8");
3632
3885
  return normalize2(JSON.parse(stripBom(raw)));
3633
3886
  }
3634
3887
  const num = numericRef(issueRef);
@@ -3647,7 +3900,7 @@ function normalize2(raw) {
3647
3900
  }
3648
3901
 
3649
3902
  // src/cli/commands/review.ts
3650
- import { writeFile as writeFile6, readFile as readFile7 } from "fs/promises";
3903
+ import { writeFile as writeFile6, readFile as readFile8 } from "fs/promises";
3651
3904
  import { execFile as execFile2 } from "child_process";
3652
3905
  import { promisify as promisify3 } from "util";
3653
3906
  import pc12 from "picocolors";
@@ -3723,7 +3976,7 @@ async function review(prRef, opts) {
3723
3976
  }
3724
3977
  }
3725
3978
  async function loadDiff(num, input) {
3726
- if (input) return input === "-" ? readStdin() : readFile7(input, "utf8");
3979
+ if (input) return input === "-" ? readStdin() : readFile8(input, "utf8");
3727
3980
  const { stdout: stdout2 } = await exec3("gh", ["pr", "diff", num]);
3728
3981
  return stdout2;
3729
3982
  }
@@ -3735,7 +3988,7 @@ async function loadMeta(num, input) {
3735
3988
  }
3736
3989
 
3737
3990
  // src/cli/index.ts
3738
- import { join as join5 } from "path";
3991
+ import { join as join6 } from "path";
3739
3992
 
3740
3993
  // src/core/config/dotenv.ts
3741
3994
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
@@ -3804,7 +4057,7 @@ function buildProgram() {
3804
4057
  }
3805
4058
  async function main() {
3806
4059
  try {
3807
- loadDotenv([join5(configDir(), ".env"), join5(process.cwd(), ".env")]);
4060
+ loadDotenv([join6(configDir(), ".env"), join6(process.cwd(), ".env")]);
3808
4061
  await resolveLocale();
3809
4062
  await buildProgram().parseAsync(process.argv);
3810
4063
  } catch (err) {