@gaberrb/polypus 0.3.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
@@ -2,8 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import { createRequire } from "module";
6
- import pc12 from "picocolors";
5
+ import pc13 from "picocolors";
7
6
 
8
7
  // src/cli/commands/add-agent.ts
9
8
  import pc from "picocolors";
@@ -74,11 +73,13 @@ var en = {
74
73
  "cli.description": "Agentic coding harness that makes any AI API generate and apply code \u2014 OpenRouter, Ollama, and any OpenAI-compatible endpoint.",
75
74
  "cli.opt.lang": "interface language: pt-BR | en",
76
75
  "cli.cmd.setup": "Interactive setup wizard (configure agents, keys, permissions)",
76
+ "cli.cmd.init": "Scaffold a .poly/ workspace (agents.md, skills, SDD spec template, README)",
77
+ "cli.opt.force": "overwrite files that already exist",
77
78
  "cli.cmd.addAgent": "Register a new agent (API key + model)",
78
79
  "cli.cmd.removeAgent": "Remove a configured agent",
79
80
  "cli.cmd.listAgents": "List configured agents",
80
81
  "cli.cmd.run": "Run a coding task with an agent",
81
- "cli.cmd.swarm": "Split a task across multiple agents working in parallel git worktrees",
82
+ "cli.cmd.swarm": "Split a task across multiple agents working in parallel git worktrees (requires 3+ configured agents)",
82
83
  "cli.cmd.models": "Browse OpenRouter models (price, context, tool support)",
83
84
  "cli.cmd.prd": "Generate a PRD from a GitHub issue (uses a free OpenRouter model)",
84
85
  "cli.arg.prdIssue": "issue number to turn into a PRD",
@@ -142,6 +143,7 @@ var en = {
142
143
  "repl.allowShow": "mode={mode} allow=[{allow}]",
143
144
  "repl.historyCleared": "history cleared",
144
145
  "repl.unknown": "Unknown command /{cmd}. Type /help.",
146
+ "repl.pasted": "[Pasted text #{id} +{lines} lines]",
145
147
  "repl.agentSwitched": "active agent \u2192 {name}",
146
148
  "repl.switchedTo": "active agent is now {name}",
147
149
  "repl.noAgentsLeft": "No agents left. Use /add to create one.",
@@ -155,6 +157,7 @@ var en = {
155
157
  " /plan switch to plan mode (read-only)",
156
158
  " /review switch to review mode (confirm each action)",
157
159
  " /bypass switch to bypass mode (auto-approve)",
160
+ " /swarm <task> run a task as a parallel swarm (needs 3+ agents)",
158
161
  " /allow <glob> add a path glob to the allow-list",
159
162
  " /allow show the current allow-list and mode",
160
163
  " /reset clear the conversation history",
@@ -164,6 +167,7 @@ var en = {
164
167
  ].join("\n"),
165
168
  // swarm
166
169
  "swarm.noAgents": "No agents configured. Run `polypus setup` or `polypus add-agent` first.",
170
+ "swarm.needsAgents": "Swarm mode needs at least {min} configured agents (you have {have}). Add more with `polypus add-agent`, or use `polypus run` for a single agent.",
167
171
  "swarm.status": "swarm agents=[{agents}] workspace={workspace}",
168
172
  "swarm.bypassNote": "Workers run in bypass mode inside isolated git worktrees; branches are merged at the end.",
169
173
  "swarm.decomposed": "Decomposed into {n} subtask(s):",
@@ -189,6 +193,12 @@ var en = {
189
193
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) had merge conflicts (kept for inspection):",
190
194
  "swarm.statusDone": "done",
191
195
  "swarm.statusIncomplete": "incomplete",
196
+ // init
197
+ "init.created": "\u2713 .poly scaffolded:",
198
+ "init.skipped": "Kept (already existed):",
199
+ "init.allExist": "Nothing to do \u2014 .poly already has these files:",
200
+ "init.forceHint": "Run `polypus init --force` to overwrite them.",
201
+ "init.tip": "Tip: edit .poly/agents.md \u2014 Polypus loads it into the agent's context automatically.",
192
202
  // wizard
193
203
  "wizard.title": " polypus setup ",
194
204
  "wizard.intro": [
@@ -274,7 +284,8 @@ var en = {
274
284
  "welcome.hints": "Type your task and press Enter \xB7 ESC cancels \xB7 /help \xB7 /exit",
275
285
  "welcome.firstRun": "No agents configured yet \u2014 let's set you up.",
276
286
  // agent system prompt
277
- "prompt.language": "Communicate with the user in {language}."
287
+ "prompt.language": "Communicate with the user in {language}.",
288
+ "prompt.projectInstructions": "Project-specific operating instructions follow, loaded from `.poly/agents.md`. Treat them as authoritative for how to work in THIS repo. Paths they reference (e.g. skills/*.md, ../context.md, ../rules.md) are relative to the `.poly/` directory \u2014 read those files when relevant before acting:"
278
289
  };
279
290
  var ptBR = {
280
291
  "common.default": "padr\xE3o",
@@ -283,11 +294,13 @@ var ptBR = {
283
294
  "cli.description": "Harness ag\xEAntico que faz qualquer API de IA gerar e aplicar c\xF3digo \u2014 OpenRouter, Ollama e qualquer endpoint compat\xEDvel com OpenAI.",
284
295
  "cli.opt.lang": "idioma da interface: pt-BR | en",
285
296
  "cli.cmd.setup": "Assistente de configura\xE7\xE3o interativo (agentes, chaves, permiss\xF5es)",
297
+ "cli.cmd.init": "Cria um workspace .poly/ (agents.md, skills, template de spec SDD, README)",
298
+ "cli.opt.force": "sobrescreve arquivos que j\xE1 existem",
286
299
  "cli.cmd.addAgent": "Cadastra um novo agente (chave de API + modelo)",
287
300
  "cli.cmd.removeAgent": "Remove um agente configurado",
288
301
  "cli.cmd.listAgents": "Lista os agentes configurados",
289
302
  "cli.cmd.run": "Executa uma tarefa de c\xF3digo com um agente",
290
- "cli.cmd.swarm": "Divide uma tarefa entre v\xE1rios agentes trabalhando em paralelo em git worktrees",
303
+ "cli.cmd.swarm": "Divide uma tarefa entre v\xE1rios agentes em git worktrees paralelas (requer 3+ agentes configurados)",
291
304
  "cli.cmd.models": "Explora os modelos do OpenRouter (pre\xE7o, contexto, suporte a tools)",
292
305
  "cli.cmd.prd": "Gera um PRD a partir de uma issue do GitHub (usa um modelo gratuito do OpenRouter)",
293
306
  "cli.arg.prdIssue": "n\xFAmero da issue para transformar em PRD",
@@ -348,6 +361,7 @@ var ptBR = {
348
361
  "repl.allowShow": "modo={mode} allow=[{allow}]",
349
362
  "repl.historyCleared": "hist\xF3rico limpo",
350
363
  "repl.unknown": "Comando desconhecido /{cmd}. Digite /help.",
364
+ "repl.pasted": "[Texto colado #{id} +{lines} linhas]",
351
365
  "repl.agentSwitched": "agente ativo \u2192 {name}",
352
366
  "repl.switchedTo": "agente ativo agora \xE9 {name}",
353
367
  "repl.noAgentsLeft": "Nenhum agente restante. Use /add para criar um.",
@@ -361,6 +375,7 @@ var ptBR = {
361
375
  " /plan muda para o modo plan (somente leitura)",
362
376
  " /review muda para o modo review (confirma cada a\xE7\xE3o)",
363
377
  " /bypass muda para o modo bypass (aprova automaticamente)",
378
+ " /swarm <task> roda a tarefa como swarm paralelo (requer 3+ agentes)",
364
379
  " /allow <glob> adiciona um glob de caminho \xE0 allow-list",
365
380
  " /allow mostra a allow-list e o modo atuais",
366
381
  " /reset limpa o hist\xF3rico da conversa",
@@ -369,6 +384,7 @@ var ptBR = {
369
384
  "Qualquer outra coisa \xE9 enviada ao agente como tarefa."
370
385
  ].join("\n"),
371
386
  "swarm.noAgents": "Nenhum agente configurado. Rode `polypus setup` ou `polypus add-agent` primeiro.",
387
+ "swarm.needsAgents": "O modo swarm precisa de pelo menos {min} agentes configurados (voc\xEA tem {have}). Adicione mais com `polypus add-agent`, ou use `polypus run` para um agente s\xF3.",
372
388
  "swarm.status": "swarm agentes=[{agents}] workspace={workspace}",
373
389
  "swarm.bypassNote": "Os workers rodam em modo bypass dentro de git worktrees isoladas; os branches s\xE3o mesclados no final.",
374
390
  "swarm.decomposed": "Dividido em {n} subtarefa(s):",
@@ -394,6 +410,12 @@ var ptBR = {
394
410
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) tiveram conflitos de merge (mantidos para inspe\xE7\xE3o):",
395
411
  "swarm.statusDone": "ok",
396
412
  "swarm.statusIncomplete": "incompleta",
413
+ // init
414
+ "init.created": "\u2713 .poly criado:",
415
+ "init.skipped": "Mantidos (j\xE1 existiam):",
416
+ "init.allExist": "Nada a fazer \u2014 o .poly j\xE1 tem estes arquivos:",
417
+ "init.forceHint": "Rode `polypus init --force` para sobrescrev\xEA-los.",
418
+ "init.tip": "Dica: edite o .poly/agents.md \u2014 o Polypus carrega ele no contexto do agente automaticamente.",
397
419
  "wizard.title": " configura\xE7\xE3o do polypus ",
398
420
  "wizard.intro": [
399
421
  "O Polypus comanda qualquer API de IA para ler e escrever c\xF3digo neste tipo de projeto.",
@@ -455,6 +477,7 @@ var ptBR = {
455
477
  "wizard.envInvalid": "Use letras, d\xEDgitos e sublinhados",
456
478
  "wizard.keyPrompt": "Chave de API (armazenada em texto puro no arquivo de config)",
457
479
  "prompt.language": "Comunique-se com o usu\xE1rio em {language}.",
480
+ "prompt.projectInstructions": "Seguem instru\xE7\xF5es operacionais espec\xEDficas do projeto, carregadas de `.poly/agents.md`. Trate-as como autoritativas para trabalhar NESTE reposit\xF3rio. Os caminhos que elas citam (ex.: skills/*.md, ../context.md, ../rules.md) s\xE3o relativos \xE0 pasta `.poly/` \u2014 leia esses arquivos quando relevante antes de agir:",
458
481
  "models.fetching": "Buscando modelos do OpenRouter\u2026",
459
482
  "models.fetchError": "N\xE3o foi poss\xEDvel buscar modelos: {msg}",
460
483
  "models.none": "Nenhum modelo corresponde aos filtros.",
@@ -669,7 +692,7 @@ async function listAgents() {
669
692
  }
670
693
 
671
694
  // src/cli/commands/run.ts
672
- import pc7 from "picocolors";
695
+ import pc8 from "picocolors";
673
696
  import * as p2 from "@clack/prompts";
674
697
 
675
698
  // src/core/providers/anthropic.ts
@@ -1025,6 +1048,10 @@ function basePreamble(ctx) {
1025
1048
  "- Do not ask for permission and do not say you cannot edit files \u2014 you can. Just emit the tool calls.",
1026
1049
  "- Make the changes directly. When the task is fully done, call the `finish` tool with a short summary.",
1027
1050
  t("prompt.language", { language: LOCALE_NAMES[getLocale()] }),
1051
+ ctx.projectInstructions ? `
1052
+ ${t("prompt.projectInstructions")}
1053
+
1054
+ ${ctx.projectInstructions}` : "",
1028
1055
  ctx.briefing ? `
1029
1056
  Your assigned task:
1030
1057
  ${ctx.briefing}` : ""
@@ -1668,6 +1695,23 @@ function formatSchema(spec) {
1668
1695
  return lines.join("\n") || " (no parameters)";
1669
1696
  }
1670
1697
 
1698
+ // src/core/agent/project-context.ts
1699
+ import { readFile as readFile5 } from "fs/promises";
1700
+ import { join as join2 } from "path";
1701
+ var INSTRUCTION_FILES = [join2(".poly", "agents.md"), "AGENTS.md"];
1702
+ var MAX_CHARS2 = 8e3;
1703
+ async function loadProjectInstructions(workspace) {
1704
+ for (const rel of INSTRUCTION_FILES) {
1705
+ try {
1706
+ const raw = (await readFile5(join2(workspace, rel), "utf8")).trim();
1707
+ if (!raw) continue;
1708
+ return raw.length > MAX_CHARS2 ? raw.slice(0, MAX_CHARS2) + "\n\u2026(truncated)" : raw;
1709
+ } catch {
1710
+ }
1711
+ }
1712
+ return void 0;
1713
+ }
1714
+
1671
1715
  // src/core/agent/loop.ts
1672
1716
  function looksLikeStall(text2) {
1673
1717
  const lc = text2.toLowerCase();
@@ -1710,10 +1754,12 @@ async function runAgent(opts) {
1710
1754
  const maxReprompts = opts.maxReprompts ?? 3;
1711
1755
  const driver = makeDriver(agent.toolMode, toolSpecs());
1712
1756
  const ctx = { workspace: opts.workspace, permissions };
1713
- const messages = opts.history && opts.history.length > 0 ? [...opts.history, { role: "user", content: opts.task }] : [
1714
- { role: "system", content: driver.systemPrompt(opts.promptContext) },
1757
+ const seeding = !(opts.history && opts.history.length > 0);
1758
+ const promptContext = seeding && opts.promptContext.projectInstructions === void 0 ? { ...opts.promptContext, projectInstructions: await loadProjectInstructions(opts.workspace) } : opts.promptContext;
1759
+ const messages = seeding ? [
1760
+ { role: "system", content: driver.systemPrompt(promptContext) },
1715
1761
  { role: "user", content: opts.task }
1716
- ];
1762
+ ] : [...opts.history, { role: "user", content: opts.task }];
1717
1763
  let consecutiveNoTool = 0;
1718
1764
  let lastFailSig = "";
1719
1765
  let failStreak = 0;
@@ -1818,8 +1864,6 @@ ${guidance}`;
1818
1864
  }
1819
1865
 
1820
1866
  // src/ui/repl.ts
1821
- import * as readline from "readline/promises";
1822
- import { stdin, stdout } from "process";
1823
1867
  import pc6 from "picocolors";
1824
1868
 
1825
1869
  // src/ui/wizard.ts
@@ -2182,6 +2226,23 @@ async function promptApiKey(provider) {
2182
2226
 
2183
2227
  // src/ui/banner.ts
2184
2228
  import pc5 from "picocolors";
2229
+
2230
+ // src/core/version.ts
2231
+ import { createRequire } from "module";
2232
+ function resolveVersion() {
2233
+ const require2 = createRequire(import.meta.url);
2234
+ for (const rel of ["../package.json", "../../package.json"]) {
2235
+ try {
2236
+ const version = require2(rel).version;
2237
+ if (typeof version === "string" && version.length > 0) return version;
2238
+ } catch {
2239
+ }
2240
+ }
2241
+ return "0.0.0";
2242
+ }
2243
+ var VERSION = resolveVersion();
2244
+
2245
+ // src/ui/banner.ts
2185
2246
  var RESET = "\x1B[0m";
2186
2247
  var useColor = (Boolean(process.stdout.isTTY) || Boolean(process.env.FORCE_COLOR)) && !process.env.NO_COLOR;
2187
2248
  var animated = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && !process.env.POLYPUS_NO_ANIM;
@@ -2255,7 +2316,7 @@ function colorChar(rowIdx, ch) {
2255
2316
  function renderArtRow(rowIdx, line) {
2256
2317
  return [...center(line)].map((ch) => colorChar(rowIdx, ch)).join("");
2257
2318
  }
2258
- var tagline = () => c1(t("welcome.tagline")) + pc5.dim(" v0.1.0");
2319
+ var tagline = () => c1(t("welcome.tagline")) + pc5.dim(` v${VERSION}`);
2259
2320
  function authorLine() {
2260
2321
  return pc5.dim("by ") + c2(AUTHOR.name) + pc5.dim(" \xB7 ") + c1(AUTHOR.github) + pc5.dim(" \xB7 ") + c1(AUTHOR.linkedin);
2261
2322
  }
@@ -2324,6 +2385,136 @@ function promptLabel(mode) {
2324
2385
  return c2("\u{1F419} polypus") + pc5.dim(`(${mode})`) + c3(" \u203A ");
2325
2386
  }
2326
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
+
2327
2518
  // src/ui/repl.ts
2328
2519
  async function startRepl(ctx) {
2329
2520
  for (; ; ) {
@@ -2353,16 +2544,6 @@ async function startRepl(ctx) {
2353
2544
  await handleCommand(cmd, arg, ctx);
2354
2545
  }
2355
2546
  }
2356
- async function readLine(prompt) {
2357
- const rl = readline.createInterface({ input: stdin, output: stdout });
2358
- try {
2359
- return await rl.question(prompt);
2360
- } catch {
2361
- return null;
2362
- } finally {
2363
- rl.close();
2364
- }
2365
- }
2366
2547
  async function handleCommand(cmd, arg, ctx) {
2367
2548
  const { session } = ctx;
2368
2549
  switch (cmd) {
@@ -2387,6 +2568,18 @@ async function handleCommand(cmd, arg, ctx) {
2387
2568
  session.history = [];
2388
2569
  console.log(pc6.dim(t("repl.historyCleared")));
2389
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
+ }
2390
2583
  case "agents":
2391
2584
  printAgents(ctx.getConfig(), session.agentName);
2392
2585
  return;
@@ -2450,125 +2643,537 @@ async function removeAgent2(name, ctx) {
2450
2643
  }
2451
2644
  }
2452
2645
 
2453
- // src/ui/spinner.ts
2454
- var RESET2 = "\x1B[0m";
2455
- var isTTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
2456
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2457
- var violet = (s) => isTTY ? `\x1B[38;2;167;139;250m${s}${RESET2}` : s;
2458
- var dim = (s) => isTTY ? `\x1B[2m${s}${RESET2}` : s;
2459
- var Spinner = class {
2460
- timer;
2461
- frame = 0;
2462
- startedAt = 0;
2463
- label = "";
2464
- suffix = "";
2465
- /** Extra dim text appended after the elapsed time (e.g. token count). */
2466
- setSuffix(suffix) {
2467
- 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();
2468
2658
  }
2469
- /** Start (or, if already running, just update the label). */
2470
- start(label) {
2471
- this.label = label;
2472
- if (!isTTY) return;
2473
- if (this.timer) return;
2474
- this.startedAt = Date.now();
2475
- this.render();
2476
- this.timer = setInterval(() => this.render(), 90);
2477
- 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"]);
2478
2663
  }
2479
- /** Erase the spinner line and stop animating. */
2480
- stop() {
2481
- if (this.timer) {
2482
- clearInterval(this.timer);
2483
- 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;
2484
2697
  }
2485
- if (isTTY) process.stdout.write("\r\x1B[K");
2486
- }
2487
- render() {
2488
- const f = violet(FRAMES[this.frame = (this.frame + 1) % FRAMES.length]);
2489
- const secs = Math.floor((Date.now() - this.startedAt) / 1e3);
2490
- const time = secs > 0 ? dim(` (${secs}s)`) : "";
2491
- const suffix = this.suffix ? dim(` \xB7 ${this.suffix}`) : "";
2492
- process.stdout.write(`\r\x1B[K${f} \u{1F419} ${dim(this.label + "\u2026")}${time}${suffix}`);
2698
+ return { branch, ok: false, conflicts };
2493
2699
  }
2494
- };
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
+ }
2495
2705
 
2496
- // src/cli/commands/run.ts
2497
- async function run(task, opts) {
2498
- let config = await loadConfig();
2499
- const agentConfig = resolveAgent(config, opts.agent);
2500
- const workspace = process.cwd();
2501
- const session = {
2502
- agentName: agentConfig.name,
2503
- mode: opts.mode ?? config.permissions.mode,
2504
- allow: config.permissions.allow,
2505
- deny: config.permissions.deny,
2506
- allowedCommands: config.permissions.allowedCommands,
2507
- maxSteps: opts.maxSteps ? Number(opts.maxSteps) : void 0,
2508
- history: []
2509
- };
2510
- const runTask = async (taskText) => {
2511
- const active = resolveAgent(config, session.agentName);
2512
- const resolved2 = createProvider(active);
2513
- await executeTask(taskText, resolved2, workspace, session);
2514
- };
2515
- if (task) {
2516
- const resolved2 = createProvider(agentConfig);
2517
- console.log(
2518
- pc7.dim(
2519
- t("run.status", {
2520
- name: resolved2.config.name,
2521
- provider: resolved2.config.provider,
2522
- model: resolved2.config.model,
2523
- toolMode: resolved2.toolMode,
2524
- mode: session.mode
2525
- })
2526
- )
2527
- );
2528
- await executeTask(task, resolved2, workspace, session);
2529
- return;
2530
- }
2531
- const resolved = createProvider(agentConfig);
2532
- await printWelcome({
2533
- agentName: resolved.config.name,
2534
- provider: resolved.config.provider,
2535
- model: resolved.config.model,
2536
- toolMode: resolved.toolMode,
2537
- mode: session.mode,
2538
- 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: []
2539
2712
  });
2540
- const ctx = {
2541
- session,
2542
- runTask,
2543
- getConfig: () => config,
2544
- reload: async () => {
2545
- config = await loadConfig();
2546
- }
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
2547
2730
  };
2548
- await startRepl(ctx);
2549
2731
  }
2550
- async function executeTask(task, resolved, workspace, session) {
2551
- const spinner3 = new Spinner();
2552
- const controller = new AbortController();
2553
- const cancel2 = listenForCancel(controller);
2554
- const permissions = new PermissionEngine({
2555
- mode: session.mode,
2556
- policy: { workspace, allow: session.allow, deny: session.deny },
2557
- allowedCommands: session.allowedCommands,
2558
- confirm: async (req) => {
2559
- spinner3.stop();
2560
- cancel2.pause();
2561
- const ok = await confirmAction(req);
2562
- cancel2.resume();
2563
- return ok;
2564
- }
2565
- });
2566
- spinner3.start(t("ui.thinking"));
2567
- let result;
2568
- try {
2569
- result = await runAgent({
2570
- task,
2571
- workspace,
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,
2572
3177
  agent: resolved,
2573
3178
  permissions,
2574
3179
  promptContext: { workspace, mode: session.mode, allow: session.allow },
@@ -2583,16 +3188,16 @@ async function executeTask(task, resolved, workspace, session) {
2583
3188
  }
2584
3189
  session.history = result.messages;
2585
3190
  if (result.reason === "finished") {
2586
- console.log(pc7.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
3191
+ console.log(pc8.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
2587
3192
  } else if (result.reason === "cancelled") {
2588
- console.log(pc7.dim("\n" + t("run.cancelled")));
3193
+ console.log(pc8.dim("\n" + t("run.cancelled")));
2589
3194
  } else if (result.reason === "stalled" || result.reason === "maxsteps") {
2590
- console.log(pc7.yellow("\n" + t("run.stopped", { steps: result.steps })));
3195
+ console.log(pc8.yellow("\n" + t("run.stopped", { steps: result.steps })));
2591
3196
  }
2592
3197
  if (result.usage.promptTokens || result.usage.completionTokens) {
2593
3198
  const total = result.usage.promptTokens + result.usage.completionTokens;
2594
3199
  console.log(
2595
- pc7.dim(
3200
+ pc8.dim(
2596
3201
  "\u21B3 " + t("ui.tokens", {
2597
3202
  total: fmtTokens(total),
2598
3203
  in: fmtTokens(result.usage.promptTokens),
@@ -2633,7 +3238,7 @@ function listenForCancel(controller) {
2633
3238
  return { pause: detach, resume: attach, dispose: detach };
2634
3239
  }
2635
3240
  async function confirmAction(req) {
2636
- if (req.preview) console.log(pc7.dim(req.preview));
3241
+ if (req.preview) console.log(pc8.dim(req.preview));
2637
3242
  const answer = await p2.confirm({ message: t("run.confirm", { summary: req.summary }) });
2638
3243
  if (p2.isCancel(answer)) return false;
2639
3244
  return answer === true;
@@ -2649,26 +3254,26 @@ function renderEvents(spinner3) {
2649
3254
  },
2650
3255
  onAssistantText(text2) {
2651
3256
  spinner3.stop();
2652
- if (text2.trim()) console.log(pc7.cyan(text2.trim()));
3257
+ if (text2.trim()) console.log(pc8.cyan(text2.trim()));
2653
3258
  },
2654
3259
  onToolCall(call) {
2655
3260
  spinner3.stop();
2656
3261
  const arg = call.name === "run_command" ? call.arguments.command : call.arguments.path;
2657
- console.log(pc7.dim(` \u2192 ${call.name}${arg ? ` ${String(arg)}` : ""}`));
3262
+ console.log(pc8.dim(` \u2192 ${call.name}${arg ? ` ${String(arg)}` : ""}`));
2658
3263
  spinner3.start(t("ui.running", { tool: call.name }));
2659
3264
  },
2660
3265
  onToolResult(_call, result) {
2661
3266
  spinner3.stop();
2662
3267
  const head = result.output.split("\n")[0] ?? "";
2663
- 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)));
2664
3269
  },
2665
3270
  onReprompt(attempt) {
2666
3271
  spinner3.stop();
2667
- console.log(pc7.yellow(" " + t("run.reprompt", { attempt })));
3272
+ console.log(pc8.yellow(" " + t("run.reprompt", { attempt })));
2668
3273
  },
2669
3274
  onCorrection() {
2670
3275
  spinner3.stop();
2671
- console.log(pc7.yellow(" \u21BB " + t("run.autocorrect")));
3276
+ console.log(pc8.yellow(" \u21BB " + t("run.autocorrect")));
2672
3277
  }
2673
3278
  };
2674
3279
  }
@@ -2678,405 +3283,297 @@ async function setup() {
2678
3283
  await runWizard();
2679
3284
  }
2680
3285
 
2681
- // src/cli/commands/swarm.ts
2682
- import pc8 from "picocolors";
3286
+ // src/cli/commands/init.ts
3287
+ import pc9 from "picocolors";
2683
3288
 
2684
- // src/core/git/worktree.ts
2685
- import { mkdtemp } from "fs/promises";
2686
- import { tmpdir } from "os";
2687
- import { join as join2 } from "path";
2688
- import { simpleGit } from "simple-git";
2689
- async function ensureRepo(workspace) {
2690
- const git = simpleGit(workspace);
2691
- if (!await git.checkIsRepo()) {
2692
- await git.init();
2693
- }
2694
- const identity = await identityArgs(git);
2695
- const hasHead = await git.raw(["rev-parse", "--verify", "HEAD"]).then(() => true).catch(() => false);
2696
- if (!hasHead) {
2697
- await git.raw([...identity, "commit", "--allow-empty", "-m", "polypus: initial commit"]);
2698
- }
2699
- return git;
2700
- }
2701
- async function identityArgs(git) {
2702
- const email = await git.raw(["config", "user.email"]).catch(() => "");
2703
- if (email.trim()) return [];
2704
- return ["-c", "user.email=polypus@local", "-c", "user.name=Polypus"];
2705
- }
2706
- async function createWorktree(git, label) {
2707
- const branch = `polypus/${label}-${Date.now().toString(36)}`;
2708
- const path = await mkdtemp(join2(tmpdir(), "polypus-wt-"));
2709
- await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
2710
- return { path, branch };
2711
- }
2712
- async function commitWorktree(wt, message) {
2713
- const wtGit = simpleGit(wt.path);
2714
- await wtGit.add(["-A"]);
2715
- const status = await wtGit.status();
2716
- if (status.staged.length === 0 && status.files.length === 0) return false;
2717
- const identity = await identityArgs(wtGit);
2718
- await wtGit.raw([...identity, "commit", "-m", message]);
2719
- return true;
2720
- }
2721
- async function mergeWorktreeBranch(git, branch) {
2722
- try {
2723
- const identity = await identityArgs(git);
2724
- await git.raw([...identity, "merge", "--no-edit", branch]);
2725
- return { branch, ok: true, conflicts: [] };
2726
- } catch (err) {
2727
- const status = await git.status().catch(() => void 0);
2728
- const conflicts = status?.conflicted ?? [];
2729
- await git.raw(["merge", "--abort"]).catch(() => void 0);
2730
- if (conflicts.length === 0) {
2731
- throw err;
2732
- }
2733
- return { branch, ok: false, conflicts };
2734
- }
2735
- }
2736
- async function removeWorktree(git, wt) {
2737
- await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
2738
- await git.raw(["branch", "-D", wt.branch]).catch(() => void 0);
2739
- }
3289
+ // src/core/scaffold/init.ts
3290
+ import { mkdir as mkdir3, writeFile as writeFile4, access } from "fs/promises";
3291
+ import { dirname as dirname3, join as join4 } from "path";
2740
3292
 
2741
- // src/core/agent/worker.ts
2742
- async function runWorker(subtask, agent, wt, allow, deny, events) {
2743
- const permissions = new PermissionEngine({
2744
- mode: "bypass",
2745
- policy: { workspace: wt.path, allow, deny },
2746
- allowedCommands: []
2747
- });
2748
- const result = await runAgent({
2749
- task: subtask.brief,
2750
- workspace: wt.path,
2751
- agent,
2752
- permissions,
2753
- promptContext: { workspace: wt.path, mode: "bypass", allow, briefing: subtask.brief },
2754
- events
2755
- });
2756
- const committed = await commitWorktree(wt, `polypus(${subtask.id}): ${subtask.title}`);
2757
- return {
2758
- subtask,
2759
- agentName: agent.config.name,
2760
- branch: wt.branch,
2761
- finished: result.finished,
2762
- summary: result.summary,
2763
- committed,
2764
- steps: result.steps
2765
- };
3293
+ // src/core/scaffold/templates.ts
3294
+ function polyTemplates(locale) {
3295
+ return locale === "pt-BR" ? PT : EN;
2766
3296
  }
3297
+ var EN = {
3298
+ "agents.md": `# agents.md \u2014 how an AI agent operates this repo
2767
3299
 
2768
- // src/core/agent/orchestrator.ts
2769
- async function runSwarm(opts) {
2770
- const lead = opts.agents[0];
2771
- if (!lead) throw new Error("Swarm requires at least one agent.");
2772
- const maxSubtasks = opts.maxSubtasks ?? Math.max(opts.agents.length, 2);
2773
- const git = await ensureRepo(opts.workspace);
2774
- const subtasks = await decompose(lead, opts.task, maxSubtasks);
2775
- opts.events?.onDecomposed?.(subtasks);
2776
- const worktrees = [];
2777
- for (const subtask of subtasks) {
2778
- worktrees.push(await createWorktree(git, subtask.id));
2779
- }
2780
- const outcomes = await Promise.all(
2781
- subtasks.map(async (subtask, i) => {
2782
- const agent = opts.agents[i % opts.agents.length];
2783
- const wt = worktrees[i];
2784
- opts.events?.onWorkerStart?.(subtask, agent.config.name);
2785
- const outcome = await runWorker(
2786
- subtask,
2787
- agent,
2788
- wt,
2789
- opts.allow,
2790
- opts.deny,
2791
- opts.events?.workerEvents?.(subtask)
2792
- );
2793
- opts.events?.onWorkerDone?.(outcome);
2794
- return outcome;
2795
- })
2796
- );
2797
- const merges = [];
2798
- for (const outcome of outcomes) {
2799
- if (!outcome.committed) continue;
2800
- const merge = await mergeWorktreeBranch(git, outcome.branch);
2801
- merges.push(merge);
2802
- opts.events?.onMerge?.(merge);
2803
- }
2804
- const conflicted = new Set(merges.filter((m) => !m.ok).map((m) => m.branch));
2805
- for (const wt of worktrees) {
2806
- if (conflicted.has(wt.branch)) {
2807
- await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
2808
- } else {
2809
- await removeWorktree(git, wt);
2810
- }
2811
- }
2812
- return { subtasks, outcomes, merges };
2813
- }
2814
- var DECOMPOSE_SYSTEM = [
2815
- "You are a tech lead splitting a coding task into independent subtasks that can be done in parallel.",
2816
- 'Return ONLY a JSON array. Each item: {"title": string, "brief": string}.',
2817
- "Make subtasks touch DIFFERENT files/areas to minimize merge conflicts.",
2818
- "Keep the list small (prefer 2-4 items). Each brief must be self-contained and actionable."
2819
- ].join("\n");
2820
- async function decompose(lead, task, maxSubtasks) {
2821
- try {
2822
- const res = await lead.provider.chat({
2823
- messages: [
2824
- { role: "system", content: DECOMPOSE_SYSTEM },
2825
- { role: "user", content: `Task:
2826
- ${task}
3300
+ > Local workspace under \`.poly/\`. Conditions any AI agent (Polypus, Claude, \u2026)
3301
+ > to work the way this project expects. **Polypus loads this file automatically**
3302
+ > into the agent's system prompt on every run, so keep it accurate and lean.
3303
+
3304
+ ## Role
3305
+
3306
+ You implement changes end to end \u2014 from understanding the task to a reviewable
3307
+ result \u2014 respecting the rules below.
3308
+
3309
+ ## Golden rules
3310
+
3311
+ 1. Green before a PR: the project builds, type-checks and tests pass.
3312
+ 2. Keep docs/changelog in sync with behavior changes.
3313
+ 3. Confirm before irreversible actions (publishing, deleting, force-pushing).
3314
+ 4. Small, targeted changes over broad rewrites.
3315
+
3316
+ ## Skills index
3317
+
3318
+ | Skill | When to use |
3319
+ |-------|-------------|
3320
+ | [skills/coding.md](skills/coding.md) | Technical standards for any code change |
3321
+ | [skills/spec-driven.md](skills/spec-driven.md) | Write a spec before non-trivial work |
3322
+
3323
+ ## Environment
3324
+
3325
+ - Describe the OS, shell, package manager and any tooling the agent needs.
3326
+ - Note where credentials/CLIs live and how commands are run.
3327
+ `,
3328
+ "README.md": `# .poly \u2014 your project's AI operating manual
3329
+
3330
+ \`.poly/\` is a small, local workspace that teaches AI agents how to work in THIS
3331
+ repository. Gitignore it to keep it personal, or commit it to standardize the
3332
+ workflow across your team.
3333
+
3334
+ ## What's inside
3335
+
3336
+ - **\`agents.md\`** \u2014 the entry point: role, golden rules and an index of skills.
3337
+ Polypus loads it automatically into the agent's system prompt on every run.
3338
+ - **\`skills/\`** \u2014 focused how-to guides the agent reads when relevant.
3339
+ - **\`templates/spec.md\`** \u2014 a lean Spec-Driven Development (SDD) template.
3340
+
3341
+ ## How it works
3342
+
3343
+ 1. You describe a task to the agent.
3344
+ 2. The agent reads \`agents.md\`, follows the golden rules and opens the skills it needs.
3345
+ 3. For non-trivial work, it writes a spec first from \`templates/spec.md\`.
3346
+
3347
+ ## Extend it
3348
+
3349
+ - Edit \`agents.md\` to encode your conventions.
3350
+ - Add one skill file per recurring workflow (releases, reviews, migrations\u2026).
3351
+ - Reference new skills from \`agents.md\` so the agent can discover them.
3352
+
3353
+ Regenerate any missing files with \`polypus init\` (existing files are preserved;
3354
+ use \`--force\` to overwrite).
3355
+ `,
3356
+ "skills/coding.md": `# skill: coding
3357
+
3358
+ Technical standards for changes in this repo.
3359
+
3360
+ ## Principles
3361
+
3362
+ - Match the style, naming and structure of the surrounding code.
3363
+ - Prefer small, targeted edits over broad rewrites.
3364
+ - Add or update tests with every behavior change.
3365
+
3366
+ ## Checklist before opening a PR
3367
+
3368
+ - [ ] Builds and type-checks
3369
+ - [ ] Tests pass
3370
+ - [ ] Docs / changelog updated when behavior changed
3371
+ `,
3372
+ "skills/spec-driven.md": `# skill: spec-driven development
3373
+
3374
+ For anything non-trivial, write a short spec BEFORE coding.
3375
+
3376
+ ## Flow
3377
+
3378
+ 1. Copy \`templates/spec.md\` into your issue (or \`specs/<slug>.md\`).
3379
+ 2. Fill **Why / What / Acceptance criteria / Out of scope**.
3380
+ 3. Get a thumbs-up, then implement to the acceptance criteria.
3381
+ 4. Keep the spec updated if scope changes.
3382
+
3383
+ Lean by design: if a section is empty, delete it.
3384
+ `,
3385
+ "templates/spec.md": `# Spec: <title>
3386
+
3387
+ > Status: draft \xB7 Owner: <name> \xB7 Updated: <yyyy-mm-dd>
3388
+
3389
+ ## Why
3390
+
3391
+ What problem are we solving, and for whom? Why now?
3392
+
3393
+ ## What
3394
+
3395
+ The change in plain terms \u2014 the behavior a user will actually see.
3396
+
3397
+ ## Acceptance criteria
3398
+
3399
+ - [ ] Observable outcome 1
3400
+ - [ ] Observable outcome 2
3401
+
3402
+ ## Out of scope
3403
+
3404
+ - Things we are explicitly NOT doing here.
3405
+
3406
+ ## Notes / open questions
3407
+
3408
+ - \u2026
3409
+ `
3410
+ };
3411
+ var PT = {
3412
+ "agents.md": `# agents.md \u2014 como um agente de IA opera este reposit\xF3rio
3413
+
3414
+ > Workspace local em \`.poly/\`. Condiciona qualquer agente de IA (Polypus, Claude\u2026)
3415
+ > a trabalhar do jeito que este projeto espera. **O Polypus carrega este arquivo
3416
+ > automaticamente** no system prompt do agente a cada execu\xE7\xE3o \u2014 mantenha-o
3417
+ > preciso e enxuto.
3418
+
3419
+ ## Papel
3420
+
3421
+ Voc\xEA implementa mudan\xE7as de ponta a ponta \u2014 do entendimento da tarefa a um
3422
+ resultado revis\xE1vel \u2014 respeitando as regras abaixo.
3423
+
3424
+ ## Regras de ouro
3425
+
3426
+ 1. Verde antes do PR: o projeto builda, passa no type-check e nos testes.
3427
+ 2. Mantenha docs/changelog em sincronia com mudan\xE7as de comportamento.
3428
+ 3. Confirme antes de a\xE7\xF5es irrevers\xEDveis (publicar, deletar, force-push).
3429
+ 4. Mudan\xE7as pequenas e focadas em vez de reescritas amplas.
3430
+
3431
+ ## \xCDndice de skills
3432
+
3433
+ | Skill | Quando usar |
3434
+ |-------|-------------|
3435
+ | [skills/coding.md](skills/coding.md) | Padr\xF5es t\xE9cnicos para qualquer mudan\xE7a de c\xF3digo |
3436
+ | [skills/spec-driven.md](skills/spec-driven.md) | Escrever um spec antes de trabalho n\xE3o-trivial |
3437
+
3438
+ ## Ambiente
3439
+
3440
+ - Descreva SO, shell, gerenciador de pacotes e ferramentas que o agente precisa.
3441
+ - Anote onde ficam credenciais/CLIs e como os comandos s\xE3o executados.
3442
+ `,
3443
+ "README.md": `# .poly \u2014 o manual de opera\xE7\xE3o de IA do seu projeto
3444
+
3445
+ O \`.poly/\` \xE9 um workspace local e pequeno que ensina agentes de IA a trabalhar
3446
+ NESTE reposit\xF3rio. Coloque no .gitignore para mant\xEA-lo pessoal, ou commite para
3447
+ padronizar o fluxo entre o time.
3448
+
3449
+ ## O que tem dentro
3450
+
3451
+ - **\`agents.md\`** \u2014 o ponto de entrada: papel, regras de ouro e um \xEDndice de skills.
3452
+ O Polypus carrega ele automaticamente no system prompt do agente a cada execu\xE7\xE3o.
3453
+ - **\`skills/\`** \u2014 guias pr\xE1ticos e focados que o agente l\xEA quando relevante.
3454
+ - **\`templates/spec.md\`** \u2014 um template enxuto de Spec-Driven Development (SDD).
3455
+
3456
+ ## Como funciona
3457
+
3458
+ 1. Voc\xEA descreve uma tarefa ao agente.
3459
+ 2. O agente l\xEA o \`agents.md\`, segue as regras de ouro e abre as skills necess\xE1rias.
3460
+ 3. Para trabalho n\xE3o-trivial, escreve um spec primeiro a partir de \`templates/spec.md\`.
3461
+
3462
+ ## Como estender
3463
+
3464
+ - Edite o \`agents.md\` para codificar suas conven\xE7\xF5es.
3465
+ - Adicione um arquivo de skill por fluxo recorrente (releases, reviews, migra\xE7\xF5es\u2026).
3466
+ - Referencie as novas skills no \`agents.md\` para o agente descobri-las.
3467
+
3468
+ Regenere arquivos que faltarem com \`polypus init\` (os existentes s\xE3o preservados;
3469
+ use \`--force\` para sobrescrever).
3470
+ `,
3471
+ "skills/coding.md": `# skill: coding
2827
3472
 
2828
- Return at most ${maxSubtasks} subtasks as a JSON array.` }
2829
- ],
2830
- params: { temperature: 0 }
2831
- });
2832
- const parsed = extractJsonArray(res.content);
2833
- if (parsed && parsed.length > 0) {
2834
- return parsed.slice(0, maxSubtasks).map((item, i) => ({
2835
- id: `t${i + 1}`,
2836
- title: String(item.title ?? `subtask ${i + 1}`),
2837
- brief: String(item.brief ?? item.title ?? task)
2838
- }));
3473
+ Padr\xF5es t\xE9cnicos para mudan\xE7as neste reposit\xF3rio.
3474
+
3475
+ ## Princ\xEDpios
3476
+
3477
+ - Siga o estilo, a nomenclatura e a estrutura do c\xF3digo ao redor.
3478
+ - Prefira edi\xE7\xF5es pequenas e focadas a reescritas amplas.
3479
+ - Adicione ou atualize testes a cada mudan\xE7a de comportamento.
3480
+
3481
+ ## Checklist antes de abrir um PR
3482
+
3483
+ - [ ] Builda e passa no type-check
3484
+ - [ ] Testes passam
3485
+ - [ ] Docs / changelog atualizados quando o comportamento mudou
3486
+ `,
3487
+ "skills/spec-driven.md": `# skill: spec-driven development
3488
+
3489
+ Para qualquer coisa n\xE3o-trivial, escreva um spec curto ANTES de codar.
3490
+
3491
+ ## Fluxo
3492
+
3493
+ 1. Copie \`templates/spec.md\` para a issue (ou \`specs/<slug>.md\`).
3494
+ 2. Preencha **Por qu\xEA / O qu\xEA / Crit\xE9rios de aceite / Fora de escopo**.
3495
+ 3. Valide com um "ok", ent\xE3o implemente at\xE9 os crit\xE9rios de aceite.
3496
+ 4. Mantenha o spec atualizado se o escopo mudar.
3497
+
3498
+ Enxuto por design: se uma se\xE7\xE3o ficar vazia, apague-a.
3499
+ `,
3500
+ "templates/spec.md": `# Spec: <t\xEDtulo>
3501
+
3502
+ > Status: rascunho \xB7 Dono: <nome> \xB7 Atualizado: <aaaa-mm-dd>
3503
+
3504
+ ## Por qu\xEA
3505
+
3506
+ Que problema estamos resolvendo, e para quem? Por que agora?
3507
+
3508
+ ## O qu\xEA
3509
+
3510
+ A mudan\xE7a em termos simples \u2014 o comportamento que o usu\xE1rio vai realmente ver.
3511
+
3512
+ ## Crit\xE9rios de aceite
3513
+
3514
+ - [ ] Resultado observ\xE1vel 1
3515
+ - [ ] Resultado observ\xE1vel 2
3516
+
3517
+ ## Fora de escopo
3518
+
3519
+ - Coisas que explicitamente N\xC3O faremos aqui.
3520
+
3521
+ ## Notas / d\xFAvidas em aberto
3522
+
3523
+ - \u2026
3524
+ `
3525
+ };
3526
+
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;
2839
3538
  }
2840
- } catch {
3539
+ await mkdir3(dirname3(abs), { recursive: true });
3540
+ await writeFile4(abs, content, "utf8");
3541
+ created.push(display);
2841
3542
  }
2842
- return [{ id: "t1", title: "task", brief: task }];
3543
+ return { created, skipped };
2843
3544
  }
2844
- function extractJsonArray(text2) {
2845
- const start = text2.indexOf("[");
2846
- const end = text2.lastIndexOf("]");
2847
- if (start === -1 || end <= start) return null;
3545
+ async function exists(path) {
2848
3546
  try {
2849
- const parsed = JSON.parse(text2.slice(start, end + 1));
2850
- return Array.isArray(parsed) ? parsed : null;
3547
+ await access(path);
3548
+ return true;
2851
3549
  } catch {
2852
- return null;
2853
- }
2854
- }
2855
-
2856
- // src/ui/swarm-view.ts
2857
- var RESET3 = "\x1B[0m";
2858
- var FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2859
- function describeToolCall(call) {
2860
- const raw = call.name === "run_command" ? call.arguments.command : call.arguments.path;
2861
- const arg = typeof raw === "string" ? raw : "";
2862
- const short = arg.length > 40 ? arg.slice(0, 39) + "\u2026" : arg;
2863
- return short ? `${call.name} ${short}` : call.name;
2864
- }
2865
- var SwarmView = class {
2866
- constructor(leadName, opts = {}) {
2867
- this.leadName = leadName;
2868
- this.tty = opts.tty ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
2869
- this.color = opts.color ?? this.tty;
2870
- this.write = opts.sink ?? ((s) => process.stdout.write(s));
2871
- }
2872
- leadName;
2873
- tty;
2874
- color;
2875
- write;
2876
- workers = /* @__PURE__ */ new Map();
2877
- order = [];
2878
- phase = "decomposing";
2879
- frame = 0;
2880
- lastLines = 0;
2881
- timer;
2882
- start() {
2883
- if (!this.tty) {
2884
- this.write(`\u{1F419} ${t("swarm.view.header", { lead: this.leadName })} \u2014 ${t("swarm.view.decomposing")}
2885
- `);
2886
- return;
2887
- }
2888
- this.flush();
2889
- this.timer = setInterval(() => {
2890
- this.frame = (this.frame + 1) % FRAMES2.length;
2891
- this.flush();
2892
- }, 110);
2893
- this.timer.unref?.();
2894
- }
2895
- setSubtasks(subtasks) {
2896
- this.phase = "running";
2897
- for (const s of subtasks) {
2898
- this.workers.set(s.id, { id: s.id, title: s.title, agent: "", status: "pending", action: "", steps: 0 });
2899
- this.order.push(s.id);
2900
- }
2901
- if (!this.tty) {
2902
- this.write(` ${t("swarm.decomposed", { n: subtasks.length })}
2903
- `);
2904
- for (const s of subtasks) this.write(` ${s.id}: ${s.title}
2905
- `);
2906
- }
2907
- this.flush();
2908
- }
2909
- workerStart(id, agent) {
2910
- const w = this.workers.get(id);
2911
- if (!w) return;
2912
- w.agent = agent;
2913
- w.status = "running";
2914
- if (!this.tty) this.write(` \u25B6 ${id} [${agent}] ${w.title}
2915
- `);
2916
- this.flush();
2917
- }
2918
- workerAction(id, action) {
2919
- const w = this.workers.get(id);
2920
- if (!w) return;
2921
- w.action = action;
2922
- this.flush();
2923
- }
2924
- workerStep(id, n) {
2925
- const w = this.workers.get(id);
2926
- if (!w) return;
2927
- w.steps = n;
2928
- this.flush();
2929
- }
2930
- workerDone(o) {
2931
- const w = this.workers.get(o.subtask.id);
2932
- if (!w) return;
2933
- w.status = o.finished ? "done" : "stopped";
2934
- w.steps = o.steps;
2935
- w.branch = o.branch;
2936
- w.action = "";
2937
- if (!this.tty) {
2938
- const tag = o.finished ? "\u2713" : "\u25A0";
2939
- const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
2940
- this.write(` ${tag} ${o.subtask.id} (${t("swarm.view.steps", { n: o.steps })}, ${changes})
2941
- `);
2942
- }
2943
- this.flush();
2944
- }
2945
- merge(r) {
2946
- for (const w of this.workers.values()) {
2947
- if (w.branch === r.branch) w.merge = r.ok ? "ok" : "conflict";
2948
- }
2949
- if (!this.tty) {
2950
- this.write(r.ok ? ` \u2935 ${t("swarm.merged", { branch: r.branch })}
2951
- ` : ` \u2717 ${t("swarm.mergeConflict", { branch: r.branch })}
2952
- `);
2953
- }
2954
- this.flush();
2955
- }
2956
- stop() {
2957
- this.phase = "done";
2958
- if (this.timer) {
2959
- clearInterval(this.timer);
2960
- this.timer = void 0;
2961
- }
2962
- this.flush();
2963
- }
2964
- /** Content lines of the dashboard (no cursor control). Exposed for tests. */
2965
- frameLines() {
2966
- const spin = this.dim(FRAMES2[this.frame]);
2967
- const lead = `\u{1F419} ${t("swarm.view.header", { lead: this.leadName })}`;
2968
- const lines = [];
2969
- if (this.phase === "decomposing") {
2970
- lines.push(`${spin} ${lead}`);
2971
- lines.push(" " + this.dim(t("swarm.view.decomposing")));
2972
- return lines;
2973
- }
2974
- lines.push(`${this.phase === "running" ? spin : " "} ${lead}`);
2975
- lines.push("");
2976
- for (const id of this.order) {
2977
- const w = this.workers.get(id);
2978
- lines.push(this.row(w, spin));
2979
- }
2980
- return lines;
2981
- }
2982
- // -------------------------------------------------------------------------
2983
- row(w, spin) {
2984
- const icon = w.status === "running" ? spin : w.status === "done" ? this.c("\u2713", "32") : w.status === "stopped" ? this.c("\u25A0", "33") : this.dim("\xB7");
2985
- const status = this.statusLabel(w);
2986
- 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 }))) : "";
2987
- const action = w.action ? w.action : this.dim("\u2014");
2988
- return ` ${icon} ${pad(w.id, 4)} ${pad(status, 12)} ${pad(`[${w.agent}]`, 14)} ${action}${meta}`;
2989
- }
2990
- statusLabel(w) {
2991
- if (w.merge === "conflict") return this.c(t("swarm.view.conflict"), "31");
2992
- if (w.status === "running") return this.c(t("swarm.view.running"), "36");
2993
- if (w.status === "done") return this.c(t("swarm.view.done"), "32");
2994
- if (w.status === "stopped") return this.c(t("swarm.view.stopped"), "33");
2995
- return this.dim(t("swarm.view.pending"));
2996
- }
2997
- /** Redraw the block in place (TTY) by clearing the previous frame first. */
2998
- flush() {
2999
- if (!this.tty) return;
3000
- const lines = this.frameLines();
3001
- let s = "";
3002
- if (this.lastLines > 0) s += `\x1B[${this.lastLines}A`;
3003
- s += "\x1B[0J";
3004
- s += lines.join("\n") + "\n";
3005
- this.write(s);
3006
- this.lastLines = lines.length;
3007
- }
3008
- c(s, code) {
3009
- return this.color ? `\x1B[${code}m${s}${RESET3}` : s;
3010
- }
3011
- dim(s) {
3012
- return this.color ? `\x1B[2m${s}${RESET3}` : s;
3550
+ return false;
3013
3551
  }
3014
- };
3015
- function pad(s, n) {
3016
- return s.length >= n ? s : s + " ".repeat(n - s.length);
3017
3552
  }
3018
3553
 
3019
- // src/cli/commands/swarm.ts
3020
- async function swarm(task, opts) {
3021
- const config = await loadConfig();
3022
- const selected = opts.agents ? opts.agents.split(",").map((s) => s.trim()).filter(Boolean) : config.agents.map((a) => a.name);
3023
- if (selected.length === 0) {
3024
- throw new Error(t("swarm.noAgents"));
3025
- }
3026
- const resolved = selected.map((name) => {
3027
- const a = config.agents.find((x) => x.name === name);
3028
- if (!a) throw new Error(t("agent.notFound", { name }));
3029
- 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()
3030
3559
  });
3031
- console.log(
3032
- pc8.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace: process.cwd() }))
3033
- );
3034
- console.log(pc8.yellow(t("swarm.bypassNote") + "\n"));
3035
- const view = new SwarmView(resolved[0].config.name);
3036
- view.start();
3037
- let result;
3038
- try {
3039
- result = await runSwarm({
3040
- task,
3041
- workspace: process.cwd(),
3042
- agents: resolved,
3043
- allow: config.permissions.allow,
3044
- deny: config.permissions.deny,
3045
- maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0,
3046
- events: {
3047
- onDecomposed: (subtasks) => view.setSubtasks(subtasks),
3048
- onWorkerStart: (subtask, agentName) => view.workerStart(subtask.id, agentName),
3049
- onWorkerDone: (outcome) => view.workerDone(outcome),
3050
- onMerge: (merge) => view.merge(merge),
3051
- workerEvents: (subtask) => ({
3052
- onToolCall: (call) => view.workerAction(subtask.id, describeToolCall(call)),
3053
- onStep: (step) => view.workerStep(subtask.id, step)
3054
- })
3055
- }
3056
- });
3057
- } finally {
3058
- view.stop();
3059
- }
3060
- console.log("");
3061
- console.log(pc8.bold("\n" + t("swarm.summary")));
3062
- for (const o of result.outcomes) {
3063
- const status = o.finished ? pc8.green(t("swarm.statusDone")) : pc8.yellow(t("swarm.statusIncomplete"));
3064
- const committed = o.committed ? "" : pc8.dim(` (${t("swarm.noChanges")})`);
3065
- console.log(` ${pc8.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;
3066
3565
  }
3067
- const conflicts = result.merges.filter((m) => !m.ok);
3068
- if (conflicts.length > 0) {
3069
- console.log(pc8.red("\n" + t("swarm.conflictsHeader", { n: conflicts.length })));
3070
- for (const m of conflicts) {
3071
- console.log(pc8.red(` ${m.branch}: ${m.conflicts.join(", ")}`));
3072
- }
3073
- } else {
3074
- console.log(pc8.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}`));
3075
3571
  }
3572
+ console.log("\n" + t("init.tip"));
3076
3573
  }
3077
3574
 
3078
3575
  // src/cli/commands/models.ts
3079
- import pc9 from "picocolors";
3576
+ import pc10 from "picocolors";
3080
3577
  import * as p3 from "@clack/prompts";
3081
3578
  async function models(opts) {
3082
3579
  const apiKey = await resolveOpenRouterKey();
@@ -3085,9 +3582,9 @@ async function models(opts) {
3085
3582
  let all;
3086
3583
  try {
3087
3584
  all = await listOpenRouterModels(apiKey);
3088
- spin.stop(pc9.green("\u2713 OpenRouter"));
3585
+ spin.stop(pc10.green("\u2713 OpenRouter"));
3089
3586
  } catch (err) {
3090
- spin.stop(pc9.red(t("models.fetchError", { msg: err.message })), 2);
3587
+ spin.stop(pc10.red(t("models.fetchError", { msg: err.message })), 2);
3091
3588
  return;
3092
3589
  }
3093
3590
  const filtered = filterModels(all, {
@@ -3101,26 +3598,26 @@ async function models(opts) {
3101
3598
  printModelsTable(filtered, limit, all.length);
3102
3599
  }
3103
3600
  function printModelsTable(models2, limit, total) {
3104
- console.log(pc9.dim(t("models.legend")));
3601
+ console.log(pc10.dim(t("models.legend")));
3105
3602
  if (models2.length === 0) {
3106
- console.log(pc9.yellow(t("models.none")));
3603
+ console.log(pc10.yellow(t("models.none")));
3107
3604
  return;
3108
3605
  }
3109
3606
  const rows = models2.slice(0, limit);
3110
3607
  console.log(
3111
- " " + pc9.dim(t("models.colTools").padEnd(6)) + pc9.dim(t("models.colPrice").padEnd(16)) + pc9.dim(t("models.colCtx").padEnd(9)) + pc9.dim(t("models.colModel"))
3608
+ " " + pc10.dim(t("models.colTools").padEnd(6)) + pc10.dim(t("models.colPrice").padEnd(16)) + pc10.dim(t("models.colCtx").padEnd(9)) + pc10.dim(t("models.colModel"))
3112
3609
  );
3113
3610
  for (const m of rows) {
3114
3611
  console.log(" " + modelRow(m));
3115
3612
  }
3116
- console.log(pc9.dim("\n" + t("models.shown", { shown: rows.length, total })));
3613
+ console.log(pc10.dim("\n" + t("models.shown", { shown: rows.length, total })));
3117
3614
  }
3118
3615
  function modelRow(m) {
3119
- const tools = m.supportsTools ? pc9.green("\u{1F6E0}".padEnd(5)) : pc9.dim("\u2014".padEnd(5));
3616
+ const tools = m.supportsTools ? pc10.green("\u{1F6E0}".padEnd(5)) : pc10.dim("\u2014".padEnd(5));
3120
3617
  const price = `${fmtPrice(m.promptPrice)}/${fmtPrice(m.completionPrice)}`;
3121
- const priceColored = (m.free ? pc9.green : pc9.yellow)(price.padEnd(16));
3122
- const ctx = pc9.cyan(fmtContext(m.contextLength).padEnd(9));
3123
- return `${tools} ${priceColored}${ctx}${pc9.bold(m.id)}`;
3618
+ const priceColored = (m.free ? pc10.green : pc10.yellow)(price.padEnd(16));
3619
+ const ctx = pc10.cyan(fmtContext(m.contextLength).padEnd(9));
3620
+ return `${tools} ${priceColored}${ctx}${pc10.bold(m.id)}`;
3124
3621
  }
3125
3622
  async function resolveOpenRouterKey() {
3126
3623
  if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
@@ -3134,10 +3631,10 @@ async function resolveOpenRouterKey() {
3134
3631
  }
3135
3632
 
3136
3633
  // src/cli/commands/prd.ts
3137
- import { writeFile as writeFile4, readFile as readFile5 } from "fs/promises";
3634
+ import { writeFile as writeFile5, readFile as readFile6 } from "fs/promises";
3138
3635
  import { execFile } from "child_process";
3139
3636
  import { promisify as promisify2 } from "util";
3140
- import pc10 from "picocolors";
3637
+ import pc11 from "picocolors";
3141
3638
 
3142
3639
  // src/core/agent/prd.ts
3143
3640
  var SYSTEM = [
@@ -3265,15 +3762,15 @@ async function prd(issueRef, opts) {
3265
3762
  const guide = readProjectGuide(["context.md"]);
3266
3763
  const markdown = await withRetry(() => generatePrd(issue, provider, guide));
3267
3764
  if (opts.out) {
3268
- await writeFile4(opts.out, markdown + "\n", "utf8");
3269
- console.error(pc10.green(t("prd.wrote", { path: opts.out })));
3765
+ await writeFile5(opts.out, markdown + "\n", "utf8");
3766
+ console.error(pc11.green(t("prd.wrote", { path: opts.out })));
3270
3767
  } else {
3271
3768
  process.stdout.write(markdown + "\n");
3272
3769
  }
3273
3770
  }
3274
3771
  async function loadIssue(issueRef, input) {
3275
3772
  if (input) {
3276
- const raw = input === "-" ? await readStdin() : await readFile5(input, "utf8");
3773
+ const raw = input === "-" ? await readStdin() : await readFile6(input, "utf8");
3277
3774
  return normalize2(JSON.parse(stripBom(raw)));
3278
3775
  }
3279
3776
  const num = numericRef(issueRef);
@@ -3292,10 +3789,10 @@ function normalize2(raw) {
3292
3789
  }
3293
3790
 
3294
3791
  // src/cli/commands/review.ts
3295
- import { writeFile as writeFile5, readFile as readFile6 } from "fs/promises";
3792
+ import { writeFile as writeFile6, readFile as readFile7 } from "fs/promises";
3296
3793
  import { execFile as execFile2 } from "child_process";
3297
3794
  import { promisify as promisify3 } from "util";
3298
- import pc11 from "picocolors";
3795
+ import pc12 from "picocolors";
3299
3796
 
3300
3797
  // src/core/agent/review.ts
3301
3798
  var MAX_DIFF_CHARS = Number(process.env.POLYPUS_MAX_DIFF_CHARS) || 6e4;
@@ -3361,14 +3858,14 @@ async function review(prRef, opts) {
3361
3858
  const guide = readProjectGuide(["rules.md", "context.md"]);
3362
3859
  const markdown = await withRetry(() => reviewDiff(diff, meta, provider, guide));
3363
3860
  if (opts.out) {
3364
- await writeFile5(opts.out, markdown + "\n", "utf8");
3365
- console.error(pc11.green(t("review.wrote", { path: opts.out })));
3861
+ await writeFile6(opts.out, markdown + "\n", "utf8");
3862
+ console.error(pc12.green(t("review.wrote", { path: opts.out })));
3366
3863
  } else {
3367
3864
  process.stdout.write(markdown + "\n");
3368
3865
  }
3369
3866
  }
3370
3867
  async function loadDiff(num, input) {
3371
- if (input) return input === "-" ? readStdin() : readFile6(input, "utf8");
3868
+ if (input) return input === "-" ? readStdin() : readFile7(input, "utf8");
3372
3869
  const { stdout: stdout2 } = await exec3("gh", ["pr", "diff", num]);
3373
3870
  return stdout2;
3374
3871
  }
@@ -3380,7 +3877,7 @@ async function loadMeta(num, input) {
3380
3877
  }
3381
3878
 
3382
3879
  // src/cli/index.ts
3383
- import { join as join3 } from "path";
3880
+ import { join as join5 } from "path";
3384
3881
 
3385
3882
  // src/core/config/dotenv.ts
3386
3883
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
@@ -3409,12 +3906,11 @@ function loadDotenv(paths) {
3409
3906
  }
3410
3907
 
3411
3908
  // src/cli/index.ts
3412
- var { version: pkgVersion } = createRequire(import.meta.url)("../package.json");
3413
3909
  async function launchInteractive() {
3414
3910
  const config = await loadConfig();
3415
3911
  if (config.agents.length === 0) {
3416
3912
  console.log(banner());
3417
- console.log(" " + pc12.yellow(t("welcome.firstRun")) + "\n");
3913
+ console.log(" " + pc13.yellow(t("welcome.firstRun")) + "\n");
3418
3914
  await setup();
3419
3915
  }
3420
3916
  await run(void 0, {});
@@ -3435,8 +3931,9 @@ async function resolveLocale() {
3435
3931
  }
3436
3932
  function buildProgram() {
3437
3933
  const program = new Command();
3438
- program.name("polypus").description(t("cli.description")).version(pkgVersion).option("--lang <locale>", t("cli.opt.lang")).action(() => launchInteractive());
3934
+ program.name("polypus").description(t("cli.description")).version(VERSION).option("--lang <locale>", t("cli.opt.lang")).action(() => launchInteractive());
3439
3935
  program.command("setup").description(t("cli.cmd.setup")).action(() => setup());
3936
+ program.command("init").option("--force", t("cli.opt.force")).description(t("cli.cmd.init")).action((opts) => init(opts));
3440
3937
  program.command("add-agent").argument("<name>", t("cli.arg.addAgentName")).requiredOption("--provider <provider>", t("cli.opt.provider")).requiredOption("--model <model>", t("cli.opt.model")).option("--api-key <key>", t("cli.opt.apiKey")).option("--base-url <url>", t("cli.opt.baseUrl")).option("--tool-mode <mode>", t("cli.opt.toolMode"), "auto").option("--set-default", t("cli.opt.setDefault")).description(t("cli.cmd.addAgent")).action((name, opts) => addAgent(name, opts));
3441
3938
  program.command("remove-agent").argument("<name>", t("cli.arg.removeAgentName")).description(t("cli.cmd.removeAgent")).action((name) => removeAgent(name));
3442
3939
  program.command("list-agents").alias("agents").description(t("cli.cmd.listAgents")).action(() => listAgents());
@@ -3449,11 +3946,11 @@ function buildProgram() {
3449
3946
  }
3450
3947
  async function main() {
3451
3948
  try {
3452
- loadDotenv([join3(configDir(), ".env"), join3(process.cwd(), ".env")]);
3949
+ loadDotenv([join5(configDir(), ".env"), join5(process.cwd(), ".env")]);
3453
3950
  await resolveLocale();
3454
3951
  await buildProgram().parseAsync(process.argv);
3455
3952
  } catch (err) {
3456
- console.error(pc12.red(`\u2717 ${err.message}`));
3953
+ console.error(pc13.red(`\u2717 ${err.message}`));
3457
3954
  process.exitCode = 1;
3458
3955
  }
3459
3956
  }