@gaberrb/polypus 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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",
@@ -164,6 +165,7 @@ var en = {
164
165
  ].join("\n"),
165
166
  // swarm
166
167
  "swarm.noAgents": "No agents configured. Run `polypus setup` or `polypus add-agent` first.",
168
+ "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
169
  "swarm.status": "swarm agents=[{agents}] workspace={workspace}",
168
170
  "swarm.bypassNote": "Workers run in bypass mode inside isolated git worktrees; branches are merged at the end.",
169
171
  "swarm.decomposed": "Decomposed into {n} subtask(s):",
@@ -177,9 +179,24 @@ var en = {
177
179
  "swarm.mergeConflict": " conflict merging {branch}",
178
180
  "swarm.summary": "Summary:",
179
181
  "swarm.allMerged": "\u2713 All committed branches merged cleanly.",
182
+ "swarm.view.header": "Swarm \xB7 orchestrator [{lead}]",
183
+ "swarm.view.decomposing": "splitting the task\u2026",
184
+ "swarm.view.pending": "queued",
185
+ "swarm.view.running": "running",
186
+ "swarm.view.done": "done",
187
+ "swarm.view.stopped": "stopped",
188
+ "swarm.view.conflict": "conflict",
189
+ "swarm.view.step": "step {n}",
190
+ "swarm.view.steps": "{n} steps",
180
191
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) had merge conflicts (kept for inspection):",
181
192
  "swarm.statusDone": "done",
182
193
  "swarm.statusIncomplete": "incomplete",
194
+ // init
195
+ "init.created": "\u2713 .poly scaffolded:",
196
+ "init.skipped": "Kept (already existed):",
197
+ "init.allExist": "Nothing to do \u2014 .poly already has these files:",
198
+ "init.forceHint": "Run `polypus init --force` to overwrite them.",
199
+ "init.tip": "Tip: edit .poly/agents.md \u2014 Polypus loads it into the agent's context automatically.",
183
200
  // wizard
184
201
  "wizard.title": " polypus setup ",
185
202
  "wizard.intro": [
@@ -265,7 +282,8 @@ var en = {
265
282
  "welcome.hints": "Type your task and press Enter \xB7 ESC cancels \xB7 /help \xB7 /exit",
266
283
  "welcome.firstRun": "No agents configured yet \u2014 let's set you up.",
267
284
  // agent system prompt
268
- "prompt.language": "Communicate with the user in {language}."
285
+ "prompt.language": "Communicate with the user in {language}.",
286
+ "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:"
269
287
  };
270
288
  var ptBR = {
271
289
  "common.default": "padr\xE3o",
@@ -274,11 +292,13 @@ var ptBR = {
274
292
  "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.",
275
293
  "cli.opt.lang": "idioma da interface: pt-BR | en",
276
294
  "cli.cmd.setup": "Assistente de configura\xE7\xE3o interativo (agentes, chaves, permiss\xF5es)",
295
+ "cli.cmd.init": "Cria um workspace .poly/ (agents.md, skills, template de spec SDD, README)",
296
+ "cli.opt.force": "sobrescreve arquivos que j\xE1 existem",
277
297
  "cli.cmd.addAgent": "Cadastra um novo agente (chave de API + modelo)",
278
298
  "cli.cmd.removeAgent": "Remove um agente configurado",
279
299
  "cli.cmd.listAgents": "Lista os agentes configurados",
280
300
  "cli.cmd.run": "Executa uma tarefa de c\xF3digo com um agente",
281
- "cli.cmd.swarm": "Divide uma tarefa entre v\xE1rios agentes trabalhando em paralelo em git worktrees",
301
+ "cli.cmd.swarm": "Divide uma tarefa entre v\xE1rios agentes em git worktrees paralelas (requer 3+ agentes configurados)",
282
302
  "cli.cmd.models": "Explora os modelos do OpenRouter (pre\xE7o, contexto, suporte a tools)",
283
303
  "cli.cmd.prd": "Gera um PRD a partir de uma issue do GitHub (usa um modelo gratuito do OpenRouter)",
284
304
  "cli.arg.prdIssue": "n\xFAmero da issue para transformar em PRD",
@@ -360,6 +380,7 @@ var ptBR = {
360
380
  "Qualquer outra coisa \xE9 enviada ao agente como tarefa."
361
381
  ].join("\n"),
362
382
  "swarm.noAgents": "Nenhum agente configurado. Rode `polypus setup` ou `polypus add-agent` primeiro.",
383
+ "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.",
363
384
  "swarm.status": "swarm agentes=[{agents}] workspace={workspace}",
364
385
  "swarm.bypassNote": "Os workers rodam em modo bypass dentro de git worktrees isoladas; os branches s\xE3o mesclados no final.",
365
386
  "swarm.decomposed": "Dividido em {n} subtarefa(s):",
@@ -373,9 +394,24 @@ var ptBR = {
373
394
  "swarm.mergeConflict": " conflito ao mesclar {branch}",
374
395
  "swarm.summary": "Resumo:",
375
396
  "swarm.allMerged": "\u2713 Todos os branches commitados foram mesclados sem conflito.",
397
+ "swarm.view.header": "Swarm \xB7 orquestrador [{lead}]",
398
+ "swarm.view.decomposing": "dividindo a tarefa\u2026",
399
+ "swarm.view.pending": "na fila",
400
+ "swarm.view.running": "executando",
401
+ "swarm.view.done": "conclu\xEDdo",
402
+ "swarm.view.stopped": "parado",
403
+ "swarm.view.conflict": "conflito",
404
+ "swarm.view.step": "passo {n}",
405
+ "swarm.view.steps": "{n} passos",
376
406
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) tiveram conflitos de merge (mantidos para inspe\xE7\xE3o):",
377
407
  "swarm.statusDone": "ok",
378
408
  "swarm.statusIncomplete": "incompleta",
409
+ // init
410
+ "init.created": "\u2713 .poly criado:",
411
+ "init.skipped": "Mantidos (j\xE1 existiam):",
412
+ "init.allExist": "Nada a fazer \u2014 o .poly j\xE1 tem estes arquivos:",
413
+ "init.forceHint": "Rode `polypus init --force` para sobrescrev\xEA-los.",
414
+ "init.tip": "Dica: edite o .poly/agents.md \u2014 o Polypus carrega ele no contexto do agente automaticamente.",
379
415
  "wizard.title": " configura\xE7\xE3o do polypus ",
380
416
  "wizard.intro": [
381
417
  "O Polypus comanda qualquer API de IA para ler e escrever c\xF3digo neste tipo de projeto.",
@@ -437,6 +473,7 @@ var ptBR = {
437
473
  "wizard.envInvalid": "Use letras, d\xEDgitos e sublinhados",
438
474
  "wizard.keyPrompt": "Chave de API (armazenada em texto puro no arquivo de config)",
439
475
  "prompt.language": "Comunique-se com o usu\xE1rio em {language}.",
476
+ "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:",
440
477
  "models.fetching": "Buscando modelos do OpenRouter\u2026",
441
478
  "models.fetchError": "N\xE3o foi poss\xEDvel buscar modelos: {msg}",
442
479
  "models.none": "Nenhum modelo corresponde aos filtros.",
@@ -1007,6 +1044,10 @@ function basePreamble(ctx) {
1007
1044
  "- Do not ask for permission and do not say you cannot edit files \u2014 you can. Just emit the tool calls.",
1008
1045
  "- Make the changes directly. When the task is fully done, call the `finish` tool with a short summary.",
1009
1046
  t("prompt.language", { language: LOCALE_NAMES[getLocale()] }),
1047
+ ctx.projectInstructions ? `
1048
+ ${t("prompt.projectInstructions")}
1049
+
1050
+ ${ctx.projectInstructions}` : "",
1010
1051
  ctx.briefing ? `
1011
1052
  Your assigned task:
1012
1053
  ${ctx.briefing}` : ""
@@ -1650,6 +1691,23 @@ function formatSchema(spec) {
1650
1691
  return lines.join("\n") || " (no parameters)";
1651
1692
  }
1652
1693
 
1694
+ // 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"];
1698
+ var MAX_CHARS2 = 8e3;
1699
+ async function loadProjectInstructions(workspace) {
1700
+ for (const rel of INSTRUCTION_FILES) {
1701
+ try {
1702
+ const raw = (await readFile5(join2(workspace, rel), "utf8")).trim();
1703
+ if (!raw) continue;
1704
+ return raw.length > MAX_CHARS2 ? raw.slice(0, MAX_CHARS2) + "\n\u2026(truncated)" : raw;
1705
+ } catch {
1706
+ }
1707
+ }
1708
+ return void 0;
1709
+ }
1710
+
1653
1711
  // src/core/agent/loop.ts
1654
1712
  function looksLikeStall(text2) {
1655
1713
  const lc = text2.toLowerCase();
@@ -1692,10 +1750,12 @@ async function runAgent(opts) {
1692
1750
  const maxReprompts = opts.maxReprompts ?? 3;
1693
1751
  const driver = makeDriver(agent.toolMode, toolSpecs());
1694
1752
  const ctx = { workspace: opts.workspace, permissions };
1695
- const messages = opts.history && opts.history.length > 0 ? [...opts.history, { role: "user", content: opts.task }] : [
1696
- { role: "system", content: driver.systemPrompt(opts.promptContext) },
1753
+ const seeding = !(opts.history && opts.history.length > 0);
1754
+ const promptContext = seeding && opts.promptContext.projectInstructions === void 0 ? { ...opts.promptContext, projectInstructions: await loadProjectInstructions(opts.workspace) } : opts.promptContext;
1755
+ const messages = seeding ? [
1756
+ { role: "system", content: driver.systemPrompt(promptContext) },
1697
1757
  { role: "user", content: opts.task }
1698
- ];
1758
+ ] : [...opts.history, { role: "user", content: opts.task }];
1699
1759
  let consecutiveNoTool = 0;
1700
1760
  let lastFailSig = "";
1701
1761
  let failStreak = 0;
@@ -2164,6 +2224,23 @@ async function promptApiKey(provider) {
2164
2224
 
2165
2225
  // src/ui/banner.ts
2166
2226
  import pc5 from "picocolors";
2227
+
2228
+ // src/core/version.ts
2229
+ import { createRequire } from "module";
2230
+ function resolveVersion() {
2231
+ const require2 = createRequire(import.meta.url);
2232
+ for (const rel of ["../package.json", "../../package.json"]) {
2233
+ try {
2234
+ const version = require2(rel).version;
2235
+ if (typeof version === "string" && version.length > 0) return version;
2236
+ } catch {
2237
+ }
2238
+ }
2239
+ return "0.0.0";
2240
+ }
2241
+ var VERSION = resolveVersion();
2242
+
2243
+ // src/ui/banner.ts
2167
2244
  var RESET = "\x1B[0m";
2168
2245
  var useColor = (Boolean(process.stdout.isTTY) || Boolean(process.env.FORCE_COLOR)) && !process.env.NO_COLOR;
2169
2246
  var animated = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && !process.env.POLYPUS_NO_ANIM;
@@ -2202,9 +2279,9 @@ var TENTACLE_FRAMES = [
2202
2279
  ];
2203
2280
  var WIDTH = Math.max(...[...ART].map((l) => [...l].length));
2204
2281
  function center(line) {
2205
- const pad = WIDTH - [...line].length;
2206
- const left = Math.floor(pad / 2);
2207
- return " ".repeat(left) + line + " ".repeat(pad - left);
2282
+ const pad2 = WIDTH - [...line].length;
2283
+ const left = Math.floor(pad2 / 2);
2284
+ return " ".repeat(left) + line + " ".repeat(pad2 - left);
2208
2285
  }
2209
2286
  var GLYPHS = {
2210
2287
  P: ["\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588 "],
@@ -2237,7 +2314,7 @@ function colorChar(rowIdx, ch) {
2237
2314
  function renderArtRow(rowIdx, line) {
2238
2315
  return [...center(line)].map((ch) => colorChar(rowIdx, ch)).join("");
2239
2316
  }
2240
- var tagline = () => c1(t("welcome.tagline")) + pc5.dim(" v0.1.0");
2317
+ var tagline = () => c1(t("welcome.tagline")) + pc5.dim(` v${VERSION}`);
2241
2318
  function authorLine() {
2242
2319
  return pc5.dim("by ") + c2(AUTHOR.name) + pc5.dim(" \xB7 ") + c1(AUTHOR.github) + pc5.dim(" \xB7 ") + c1(AUTHOR.linkedin);
2243
2320
  }
@@ -2660,13 +2737,302 @@ async function setup() {
2660
2737
  await runWizard();
2661
2738
  }
2662
2739
 
2663
- // src/cli/commands/swarm.ts
2740
+ // src/cli/commands/init.ts
2664
2741
  import pc8 from "picocolors";
2665
2742
 
2743
+ // src/core/scaffold/init.ts
2744
+ import { mkdir as mkdir3, writeFile as writeFile4, access } from "fs/promises";
2745
+ import { dirname as dirname3, join as join3 } from "path";
2746
+
2747
+ // src/core/scaffold/templates.ts
2748
+ function polyTemplates(locale) {
2749
+ return locale === "pt-BR" ? PT : EN;
2750
+ }
2751
+ var EN = {
2752
+ "agents.md": `# agents.md \u2014 how an AI agent operates this repo
2753
+
2754
+ > Local workspace under \`.poly/\`. Conditions any AI agent (Polypus, Claude, \u2026)
2755
+ > to work the way this project expects. **Polypus loads this file automatically**
2756
+ > into the agent's system prompt on every run, so keep it accurate and lean.
2757
+
2758
+ ## Role
2759
+
2760
+ You implement changes end to end \u2014 from understanding the task to a reviewable
2761
+ result \u2014 respecting the rules below.
2762
+
2763
+ ## Golden rules
2764
+
2765
+ 1. Green before a PR: the project builds, type-checks and tests pass.
2766
+ 2. Keep docs/changelog in sync with behavior changes.
2767
+ 3. Confirm before irreversible actions (publishing, deleting, force-pushing).
2768
+ 4. Small, targeted changes over broad rewrites.
2769
+
2770
+ ## Skills index
2771
+
2772
+ | Skill | When to use |
2773
+ |-------|-------------|
2774
+ | [skills/coding.md](skills/coding.md) | Technical standards for any code change |
2775
+ | [skills/spec-driven.md](skills/spec-driven.md) | Write a spec before non-trivial work |
2776
+
2777
+ ## Environment
2778
+
2779
+ - Describe the OS, shell, package manager and any tooling the agent needs.
2780
+ - Note where credentials/CLIs live and how commands are run.
2781
+ `,
2782
+ "README.md": `# .poly \u2014 your project's AI operating manual
2783
+
2784
+ \`.poly/\` is a small, local workspace that teaches AI agents how to work in THIS
2785
+ repository. Gitignore it to keep it personal, or commit it to standardize the
2786
+ workflow across your team.
2787
+
2788
+ ## What's inside
2789
+
2790
+ - **\`agents.md\`** \u2014 the entry point: role, golden rules and an index of skills.
2791
+ Polypus loads it automatically into the agent's system prompt on every run.
2792
+ - **\`skills/\`** \u2014 focused how-to guides the agent reads when relevant.
2793
+ - **\`templates/spec.md\`** \u2014 a lean Spec-Driven Development (SDD) template.
2794
+
2795
+ ## How it works
2796
+
2797
+ 1. You describe a task to the agent.
2798
+ 2. The agent reads \`agents.md\`, follows the golden rules and opens the skills it needs.
2799
+ 3. For non-trivial work, it writes a spec first from \`templates/spec.md\`.
2800
+
2801
+ ## Extend it
2802
+
2803
+ - Edit \`agents.md\` to encode your conventions.
2804
+ - Add one skill file per recurring workflow (releases, reviews, migrations\u2026).
2805
+ - Reference new skills from \`agents.md\` so the agent can discover them.
2806
+
2807
+ Regenerate any missing files with \`polypus init\` (existing files are preserved;
2808
+ use \`--force\` to overwrite).
2809
+ `,
2810
+ "skills/coding.md": `# skill: coding
2811
+
2812
+ Technical standards for changes in this repo.
2813
+
2814
+ ## Principles
2815
+
2816
+ - Match the style, naming and structure of the surrounding code.
2817
+ - Prefer small, targeted edits over broad rewrites.
2818
+ - Add or update tests with every behavior change.
2819
+
2820
+ ## Checklist before opening a PR
2821
+
2822
+ - [ ] Builds and type-checks
2823
+ - [ ] Tests pass
2824
+ - [ ] Docs / changelog updated when behavior changed
2825
+ `,
2826
+ "skills/spec-driven.md": `# skill: spec-driven development
2827
+
2828
+ For anything non-trivial, write a short spec BEFORE coding.
2829
+
2830
+ ## Flow
2831
+
2832
+ 1. Copy \`templates/spec.md\` into your issue (or \`specs/<slug>.md\`).
2833
+ 2. Fill **Why / What / Acceptance criteria / Out of scope**.
2834
+ 3. Get a thumbs-up, then implement to the acceptance criteria.
2835
+ 4. Keep the spec updated if scope changes.
2836
+
2837
+ Lean by design: if a section is empty, delete it.
2838
+ `,
2839
+ "templates/spec.md": `# Spec: <title>
2840
+
2841
+ > Status: draft \xB7 Owner: <name> \xB7 Updated: <yyyy-mm-dd>
2842
+
2843
+ ## Why
2844
+
2845
+ What problem are we solving, and for whom? Why now?
2846
+
2847
+ ## What
2848
+
2849
+ The change in plain terms \u2014 the behavior a user will actually see.
2850
+
2851
+ ## Acceptance criteria
2852
+
2853
+ - [ ] Observable outcome 1
2854
+ - [ ] Observable outcome 2
2855
+
2856
+ ## Out of scope
2857
+
2858
+ - Things we are explicitly NOT doing here.
2859
+
2860
+ ## Notes / open questions
2861
+
2862
+ - \u2026
2863
+ `
2864
+ };
2865
+ var PT = {
2866
+ "agents.md": `# agents.md \u2014 como um agente de IA opera este reposit\xF3rio
2867
+
2868
+ > Workspace local em \`.poly/\`. Condiciona qualquer agente de IA (Polypus, Claude\u2026)
2869
+ > a trabalhar do jeito que este projeto espera. **O Polypus carrega este arquivo
2870
+ > automaticamente** no system prompt do agente a cada execu\xE7\xE3o \u2014 mantenha-o
2871
+ > preciso e enxuto.
2872
+
2873
+ ## Papel
2874
+
2875
+ Voc\xEA implementa mudan\xE7as de ponta a ponta \u2014 do entendimento da tarefa a um
2876
+ resultado revis\xE1vel \u2014 respeitando as regras abaixo.
2877
+
2878
+ ## Regras de ouro
2879
+
2880
+ 1. Verde antes do PR: o projeto builda, passa no type-check e nos testes.
2881
+ 2. Mantenha docs/changelog em sincronia com mudan\xE7as de comportamento.
2882
+ 3. Confirme antes de a\xE7\xF5es irrevers\xEDveis (publicar, deletar, force-push).
2883
+ 4. Mudan\xE7as pequenas e focadas em vez de reescritas amplas.
2884
+
2885
+ ## \xCDndice de skills
2886
+
2887
+ | Skill | Quando usar |
2888
+ |-------|-------------|
2889
+ | [skills/coding.md](skills/coding.md) | Padr\xF5es t\xE9cnicos para qualquer mudan\xE7a de c\xF3digo |
2890
+ | [skills/spec-driven.md](skills/spec-driven.md) | Escrever um spec antes de trabalho n\xE3o-trivial |
2891
+
2892
+ ## Ambiente
2893
+
2894
+ - Descreva SO, shell, gerenciador de pacotes e ferramentas que o agente precisa.
2895
+ - Anote onde ficam credenciais/CLIs e como os comandos s\xE3o executados.
2896
+ `,
2897
+ "README.md": `# .poly \u2014 o manual de opera\xE7\xE3o de IA do seu projeto
2898
+
2899
+ O \`.poly/\` \xE9 um workspace local e pequeno que ensina agentes de IA a trabalhar
2900
+ NESTE reposit\xF3rio. Coloque no .gitignore para mant\xEA-lo pessoal, ou commite para
2901
+ padronizar o fluxo entre o time.
2902
+
2903
+ ## O que tem dentro
2904
+
2905
+ - **\`agents.md\`** \u2014 o ponto de entrada: papel, regras de ouro e um \xEDndice de skills.
2906
+ O Polypus carrega ele automaticamente no system prompt do agente a cada execu\xE7\xE3o.
2907
+ - **\`skills/\`** \u2014 guias pr\xE1ticos e focados que o agente l\xEA quando relevante.
2908
+ - **\`templates/spec.md\`** \u2014 um template enxuto de Spec-Driven Development (SDD).
2909
+
2910
+ ## Como funciona
2911
+
2912
+ 1. Voc\xEA descreve uma tarefa ao agente.
2913
+ 2. O agente l\xEA o \`agents.md\`, segue as regras de ouro e abre as skills necess\xE1rias.
2914
+ 3. Para trabalho n\xE3o-trivial, escreve um spec primeiro a partir de \`templates/spec.md\`.
2915
+
2916
+ ## Como estender
2917
+
2918
+ - Edite o \`agents.md\` para codificar suas conven\xE7\xF5es.
2919
+ - Adicione um arquivo de skill por fluxo recorrente (releases, reviews, migra\xE7\xF5es\u2026).
2920
+ - Referencie as novas skills no \`agents.md\` para o agente descobri-las.
2921
+
2922
+ Regenere arquivos que faltarem com \`polypus init\` (os existentes s\xE3o preservados;
2923
+ use \`--force\` para sobrescrever).
2924
+ `,
2925
+ "skills/coding.md": `# skill: coding
2926
+
2927
+ Padr\xF5es t\xE9cnicos para mudan\xE7as neste reposit\xF3rio.
2928
+
2929
+ ## Princ\xEDpios
2930
+
2931
+ - Siga o estilo, a nomenclatura e a estrutura do c\xF3digo ao redor.
2932
+ - Prefira edi\xE7\xF5es pequenas e focadas a reescritas amplas.
2933
+ - Adicione ou atualize testes a cada mudan\xE7a de comportamento.
2934
+
2935
+ ## Checklist antes de abrir um PR
2936
+
2937
+ - [ ] Builda e passa no type-check
2938
+ - [ ] Testes passam
2939
+ - [ ] Docs / changelog atualizados quando o comportamento mudou
2940
+ `,
2941
+ "skills/spec-driven.md": `# skill: spec-driven development
2942
+
2943
+ Para qualquer coisa n\xE3o-trivial, escreva um spec curto ANTES de codar.
2944
+
2945
+ ## Fluxo
2946
+
2947
+ 1. Copie \`templates/spec.md\` para a issue (ou \`specs/<slug>.md\`).
2948
+ 2. Preencha **Por qu\xEA / O qu\xEA / Crit\xE9rios de aceite / Fora de escopo**.
2949
+ 3. Valide com um "ok", ent\xE3o implemente at\xE9 os crit\xE9rios de aceite.
2950
+ 4. Mantenha o spec atualizado se o escopo mudar.
2951
+
2952
+ Enxuto por design: se uma se\xE7\xE3o ficar vazia, apague-a.
2953
+ `,
2954
+ "templates/spec.md": `# Spec: <t\xEDtulo>
2955
+
2956
+ > Status: rascunho \xB7 Dono: <nome> \xB7 Atualizado: <aaaa-mm-dd>
2957
+
2958
+ ## Por qu\xEA
2959
+
2960
+ Que problema estamos resolvendo, e para quem? Por que agora?
2961
+
2962
+ ## O qu\xEA
2963
+
2964
+ A mudan\xE7a em termos simples \u2014 o comportamento que o usu\xE1rio vai realmente ver.
2965
+
2966
+ ## Crit\xE9rios de aceite
2967
+
2968
+ - [ ] Resultado observ\xE1vel 1
2969
+ - [ ] Resultado observ\xE1vel 2
2970
+
2971
+ ## Fora de escopo
2972
+
2973
+ - Coisas que explicitamente N\xC3O faremos aqui.
2974
+
2975
+ ## Notas / d\xFAvidas em aberto
2976
+
2977
+ - \u2026
2978
+ `
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
+
2666
3032
  // src/core/git/worktree.ts
2667
3033
  import { mkdtemp } from "fs/promises";
2668
3034
  import { tmpdir } from "os";
2669
- import { join as join2 } from "path";
3035
+ import { join as join4 } from "path";
2670
3036
  import { simpleGit } from "simple-git";
2671
3037
  async function ensureRepo(workspace) {
2672
3038
  const git = simpleGit(workspace);
@@ -2687,7 +3053,7 @@ async function identityArgs(git) {
2687
3053
  }
2688
3054
  async function createWorktree(git, label) {
2689
3055
  const branch = `polypus/${label}-${Date.now().toString(36)}`;
2690
- const path = await mkdtemp(join2(tmpdir(), "polypus-wt-"));
3056
+ const path = await mkdtemp(join4(tmpdir(), "polypus-wt-"));
2691
3057
  await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
2692
3058
  return { path, branch };
2693
3059
  }
@@ -2835,9 +3201,179 @@ function extractJsonArray(text2) {
2835
3201
  }
2836
3202
  }
2837
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;
3361
+ }
3362
+ };
3363
+ function pad(s, n) {
3364
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
3365
+ }
3366
+
2838
3367
  // src/cli/commands/swarm.ts
3368
+ var MIN_SWARM_AGENTS = 3;
3369
+ function canSwarm(agentCount) {
3370
+ return agentCount >= MIN_SWARM_AGENTS;
3371
+ }
2839
3372
  async function swarm(task, opts) {
2840
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
+ }
2841
3377
  const selected = opts.agents ? opts.agents.split(",").map((s) => s.trim()).filter(Boolean) : config.agents.map((a) => a.name);
2842
3378
  if (selected.length === 0) {
2843
3379
  throw new Error(t("swarm.noAgents"));
@@ -2848,57 +3384,54 @@ async function swarm(task, opts) {
2848
3384
  return createProvider(a);
2849
3385
  });
2850
3386
  console.log(
2851
- pc8.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace: process.cwd() }))
3387
+ pc9.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace: process.cwd() }))
2852
3388
  );
2853
- console.log(pc8.yellow(t("swarm.bypassNote") + "\n"));
2854
- const result = await runSwarm({
2855
- task,
2856
- workspace: process.cwd(),
2857
- agents: resolved,
2858
- allow: config.permissions.allow,
2859
- deny: config.permissions.deny,
2860
- maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0,
2861
- events: renderSwarmEvents()
2862
- });
2863
- console.log(pc8.bold("\n" + t("swarm.summary")));
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")));
2864
3417
  for (const o of result.outcomes) {
2865
- const status = o.finished ? pc8.green(t("swarm.statusDone")) : pc8.yellow(t("swarm.statusIncomplete"));
2866
- const committed = o.committed ? "" : pc8.dim(` (${t("swarm.noChanges")})`);
2867
- console.log(` ${pc8.bold(o.subtask.id)} [${o.agentName}] ${status}${committed} \u2014 ${o.subtask.title}`);
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}`);
2868
3421
  }
2869
3422
  const conflicts = result.merges.filter((m) => !m.ok);
2870
3423
  if (conflicts.length > 0) {
2871
- console.log(pc8.red("\n" + t("swarm.conflictsHeader", { n: conflicts.length })));
3424
+ console.log(pc9.red("\n" + t("swarm.conflictsHeader", { n: conflicts.length })));
2872
3425
  for (const m of conflicts) {
2873
- console.log(pc8.red(` ${m.branch}: ${m.conflicts.join(", ")}`));
3426
+ console.log(pc9.red(` ${m.branch}: ${m.conflicts.join(", ")}`));
2874
3427
  }
2875
3428
  } else {
2876
- console.log(pc8.green("\n" + t("swarm.allMerged")));
3429
+ console.log(pc9.green("\n" + t("swarm.allMerged")));
2877
3430
  }
2878
3431
  }
2879
- function renderSwarmEvents() {
2880
- return {
2881
- onDecomposed(subtasks) {
2882
- console.log(pc8.bold(t("swarm.decomposed", { n: subtasks.length })));
2883
- for (const s of subtasks) console.log(pc8.dim(` ${s.id}: ${s.title}`));
2884
- console.log("");
2885
- },
2886
- onWorkerStart(subtask, agentName) {
2887
- console.log(pc8.cyan(t("swarm.workerStart", { id: subtask.id, agent: agentName })));
2888
- },
2889
- onWorkerDone(o) {
2890
- const head = o.finished ? pc8.green(t("swarm.workerDone", { id: o.subtask.id })) : pc8.yellow(t("swarm.workerStopped", { id: o.subtask.id }));
2891
- const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
2892
- console.log(head + pc8.dim(t("swarm.workerMeta", { steps: o.steps, changes })));
2893
- },
2894
- onMerge(m) {
2895
- console.log(m.ok ? pc8.dim(t("swarm.merged", { branch: m.branch })) : pc8.red(t("swarm.mergeConflict", { branch: m.branch })));
2896
- }
2897
- };
2898
- }
2899
3432
 
2900
3433
  // src/cli/commands/models.ts
2901
- import pc9 from "picocolors";
3434
+ import pc10 from "picocolors";
2902
3435
  import * as p3 from "@clack/prompts";
2903
3436
  async function models(opts) {
2904
3437
  const apiKey = await resolveOpenRouterKey();
@@ -2907,9 +3440,9 @@ async function models(opts) {
2907
3440
  let all;
2908
3441
  try {
2909
3442
  all = await listOpenRouterModels(apiKey);
2910
- spin.stop(pc9.green("\u2713 OpenRouter"));
3443
+ spin.stop(pc10.green("\u2713 OpenRouter"));
2911
3444
  } catch (err) {
2912
- spin.stop(pc9.red(t("models.fetchError", { msg: err.message })), 2);
3445
+ spin.stop(pc10.red(t("models.fetchError", { msg: err.message })), 2);
2913
3446
  return;
2914
3447
  }
2915
3448
  const filtered = filterModels(all, {
@@ -2923,26 +3456,26 @@ async function models(opts) {
2923
3456
  printModelsTable(filtered, limit, all.length);
2924
3457
  }
2925
3458
  function printModelsTable(models2, limit, total) {
2926
- console.log(pc9.dim(t("models.legend")));
3459
+ console.log(pc10.dim(t("models.legend")));
2927
3460
  if (models2.length === 0) {
2928
- console.log(pc9.yellow(t("models.none")));
3461
+ console.log(pc10.yellow(t("models.none")));
2929
3462
  return;
2930
3463
  }
2931
3464
  const rows = models2.slice(0, limit);
2932
3465
  console.log(
2933
- " " + 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"))
3466
+ " " + 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"))
2934
3467
  );
2935
3468
  for (const m of rows) {
2936
3469
  console.log(" " + modelRow(m));
2937
3470
  }
2938
- console.log(pc9.dim("\n" + t("models.shown", { shown: rows.length, total })));
3471
+ console.log(pc10.dim("\n" + t("models.shown", { shown: rows.length, total })));
2939
3472
  }
2940
3473
  function modelRow(m) {
2941
- const tools = m.supportsTools ? pc9.green("\u{1F6E0}".padEnd(5)) : pc9.dim("\u2014".padEnd(5));
3474
+ const tools = m.supportsTools ? pc10.green("\u{1F6E0}".padEnd(5)) : pc10.dim("\u2014".padEnd(5));
2942
3475
  const price = `${fmtPrice(m.promptPrice)}/${fmtPrice(m.completionPrice)}`;
2943
- const priceColored = (m.free ? pc9.green : pc9.yellow)(price.padEnd(16));
2944
- const ctx = pc9.cyan(fmtContext(m.contextLength).padEnd(9));
2945
- return `${tools} ${priceColored}${ctx}${pc9.bold(m.id)}`;
3476
+ const priceColored = (m.free ? pc10.green : pc10.yellow)(price.padEnd(16));
3477
+ const ctx = pc10.cyan(fmtContext(m.contextLength).padEnd(9));
3478
+ return `${tools} ${priceColored}${ctx}${pc10.bold(m.id)}`;
2946
3479
  }
2947
3480
  async function resolveOpenRouterKey() {
2948
3481
  if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
@@ -2956,10 +3489,10 @@ async function resolveOpenRouterKey() {
2956
3489
  }
2957
3490
 
2958
3491
  // src/cli/commands/prd.ts
2959
- import { writeFile as writeFile4, readFile as readFile5 } from "fs/promises";
3492
+ import { writeFile as writeFile5, readFile as readFile6 } from "fs/promises";
2960
3493
  import { execFile } from "child_process";
2961
3494
  import { promisify as promisify2 } from "util";
2962
- import pc10 from "picocolors";
3495
+ import pc11 from "picocolors";
2963
3496
 
2964
3497
  // src/core/agent/prd.ts
2965
3498
  var SYSTEM = [
@@ -2991,12 +3524,18 @@ ${comments}` : "",
2991
3524
  "## Riscos e alternativas"
2992
3525
  ].join("\n");
2993
3526
  }
2994
- async function generatePrd(issue, provider) {
3527
+ async function generatePrd(issue, provider, projectContext) {
3528
+ const messages = [{ role: "system", content: SYSTEM }];
3529
+ if (projectContext) {
3530
+ messages.push({
3531
+ role: "system",
3532
+ content: `Project context (for grounding; do not restate verbatim):
3533
+ ${projectContext}`
3534
+ });
3535
+ }
3536
+ messages.push({ role: "user", content: buildPrdPrompt(issue) });
2995
3537
  const res = await provider.chat({
2996
- messages: [
2997
- { role: "system", content: SYSTEM },
2998
- { role: "user", content: buildPrdPrompt(issue) }
2999
- ],
3538
+ messages,
3000
3539
  params: { maxTokens: 2e3, temperature: 0.2 }
3001
3540
  });
3002
3541
  const text2 = res.content.trim();
@@ -3041,6 +3580,23 @@ async function withRetry(fn, opts = {}) {
3041
3580
  }
3042
3581
 
3043
3582
  // src/cli/commands/cli-io.ts
3583
+ import { readFileSync, existsSync as existsSync2 } from "fs";
3584
+ import { resolve as resolve7 } from "path";
3585
+ var GUIDE_MAX = 12e3;
3586
+ function readProjectGuide(files) {
3587
+ const parts = [];
3588
+ for (const file of files) {
3589
+ try {
3590
+ const path = resolve7(process.cwd(), file);
3591
+ if (existsSync2(path)) parts.push(`# ${file}
3592
+ ${readFileSync(path, "utf8").trim()}`);
3593
+ } catch {
3594
+ }
3595
+ }
3596
+ if (parts.length === 0) return void 0;
3597
+ const joined = parts.join("\n\n");
3598
+ return joined.length > GUIDE_MAX ? joined.slice(0, GUIDE_MAX) + "\n\u2026(truncated)" : joined;
3599
+ }
3044
3600
  function numericRef(ref) {
3045
3601
  const num = ref.replace(/^#/, "");
3046
3602
  if (!/^\d+$/.test(num)) throw new Error(t("cli.invalidRef", { ref }));
@@ -3061,17 +3617,18 @@ var exec2 = promisify2(execFile);
3061
3617
  async function prd(issueRef, opts) {
3062
3618
  const issue = await loadIssue(issueRef, opts.input);
3063
3619
  const { provider } = resolveFreeProvider(opts.model ?? DEFAULT_PRD_MODEL);
3064
- const markdown = await withRetry(() => generatePrd(issue, provider));
3620
+ const guide = readProjectGuide(["context.md"]);
3621
+ const markdown = await withRetry(() => generatePrd(issue, provider, guide));
3065
3622
  if (opts.out) {
3066
- await writeFile4(opts.out, markdown + "\n", "utf8");
3067
- console.error(pc10.green(t("prd.wrote", { path: opts.out })));
3623
+ await writeFile5(opts.out, markdown + "\n", "utf8");
3624
+ console.error(pc11.green(t("prd.wrote", { path: opts.out })));
3068
3625
  } else {
3069
3626
  process.stdout.write(markdown + "\n");
3070
3627
  }
3071
3628
  }
3072
3629
  async function loadIssue(issueRef, input) {
3073
3630
  if (input) {
3074
- const raw = input === "-" ? await readStdin() : await readFile5(input, "utf8");
3631
+ const raw = input === "-" ? await readStdin() : await readFile6(input, "utf8");
3075
3632
  return normalize2(JSON.parse(stripBom(raw)));
3076
3633
  }
3077
3634
  const num = numericRef(issueRef);
@@ -3090,10 +3647,10 @@ function normalize2(raw) {
3090
3647
  }
3091
3648
 
3092
3649
  // src/cli/commands/review.ts
3093
- import { writeFile as writeFile5, readFile as readFile6 } from "fs/promises";
3650
+ import { writeFile as writeFile6, readFile as readFile7 } from "fs/promises";
3094
3651
  import { execFile as execFile2 } from "child_process";
3095
3652
  import { promisify as promisify3 } from "util";
3096
- import pc11 from "picocolors";
3653
+ import pc12 from "picocolors";
3097
3654
 
3098
3655
  // src/core/agent/review.ts
3099
3656
  var MAX_DIFF_CHARS = Number(process.env.POLYPUS_MAX_DIFF_CHARS) || 6e4;
@@ -3129,13 +3686,19 @@ function buildReviewPrompt(diff, meta) {
3129
3686
  "Liste os achados agrupados por severidade (\u{1F534} bug, \u{1F7E1} aten\xE7\xE3o, \u{1F7E2} sugest\xE3o)."
3130
3687
  ].join("\n");
3131
3688
  }
3132
- async function reviewDiff(diff, meta, provider) {
3689
+ async function reviewDiff(diff, meta, provider, projectGuide) {
3133
3690
  if (!diff.trim()) return "_Sem altera\xE7\xF5es no diff para revisar._";
3691
+ const messages = [{ role: "system", content: SYSTEM2 }];
3692
+ if (projectGuide) {
3693
+ messages.push({
3694
+ role: "system",
3695
+ content: `Project context and conventions to review against:
3696
+ ${projectGuide}`
3697
+ });
3698
+ }
3699
+ messages.push({ role: "user", content: buildReviewPrompt(diff, meta) });
3134
3700
  const res = await provider.chat({
3135
- messages: [
3136
- { role: "system", content: SYSTEM2 },
3137
- { role: "user", content: buildReviewPrompt(diff, meta) }
3138
- ],
3701
+ messages,
3139
3702
  params: { maxTokens: 1500, temperature: 0.2 }
3140
3703
  });
3141
3704
  const text2 = res.content.trim();
@@ -3150,16 +3713,17 @@ async function review(prRef, opts) {
3150
3713
  const diff = await loadDiff(num, opts.input);
3151
3714
  const meta = await loadMeta(num, opts.input);
3152
3715
  const { provider } = resolveFreeProvider(opts.model ?? DEFAULT_REVIEW_MODEL);
3153
- const markdown = await withRetry(() => reviewDiff(diff, meta, provider));
3716
+ const guide = readProjectGuide(["rules.md", "context.md"]);
3717
+ const markdown = await withRetry(() => reviewDiff(diff, meta, provider, guide));
3154
3718
  if (opts.out) {
3155
- await writeFile5(opts.out, markdown + "\n", "utf8");
3156
- console.error(pc11.green(t("review.wrote", { path: opts.out })));
3719
+ await writeFile6(opts.out, markdown + "\n", "utf8");
3720
+ console.error(pc12.green(t("review.wrote", { path: opts.out })));
3157
3721
  } else {
3158
3722
  process.stdout.write(markdown + "\n");
3159
3723
  }
3160
3724
  }
3161
3725
  async function loadDiff(num, input) {
3162
- if (input) return input === "-" ? readStdin() : readFile6(input, "utf8");
3726
+ if (input) return input === "-" ? readStdin() : readFile7(input, "utf8");
3163
3727
  const { stdout: stdout2 } = await exec3("gh", ["pr", "diff", num]);
3164
3728
  return stdout2;
3165
3729
  }
@@ -3171,16 +3735,16 @@ async function loadMeta(num, input) {
3171
3735
  }
3172
3736
 
3173
3737
  // src/cli/index.ts
3174
- import { join as join3 } from "path";
3738
+ import { join as join5 } from "path";
3175
3739
 
3176
3740
  // src/core/config/dotenv.ts
3177
- import { existsSync as existsSync2, readFileSync } from "fs";
3741
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
3178
3742
  function loadDotenv(paths) {
3179
3743
  for (const path of paths) {
3180
- if (!existsSync2(path)) continue;
3744
+ if (!existsSync3(path)) continue;
3181
3745
  let text2;
3182
3746
  try {
3183
- text2 = readFileSync(path, "utf8");
3747
+ text2 = readFileSync2(path, "utf8");
3184
3748
  } catch {
3185
3749
  continue;
3186
3750
  }
@@ -3200,12 +3764,11 @@ function loadDotenv(paths) {
3200
3764
  }
3201
3765
 
3202
3766
  // src/cli/index.ts
3203
- var { version: pkgVersion } = createRequire(import.meta.url)("../package.json");
3204
3767
  async function launchInteractive() {
3205
3768
  const config = await loadConfig();
3206
3769
  if (config.agents.length === 0) {
3207
3770
  console.log(banner());
3208
- console.log(" " + pc12.yellow(t("welcome.firstRun")) + "\n");
3771
+ console.log(" " + pc13.yellow(t("welcome.firstRun")) + "\n");
3209
3772
  await setup();
3210
3773
  }
3211
3774
  await run(void 0, {});
@@ -3226,8 +3789,9 @@ async function resolveLocale() {
3226
3789
  }
3227
3790
  function buildProgram() {
3228
3791
  const program = new Command();
3229
- program.name("polypus").description(t("cli.description")).version(pkgVersion).option("--lang <locale>", t("cli.opt.lang")).action(() => launchInteractive());
3792
+ program.name("polypus").description(t("cli.description")).version(VERSION).option("--lang <locale>", t("cli.opt.lang")).action(() => launchInteractive());
3230
3793
  program.command("setup").description(t("cli.cmd.setup")).action(() => setup());
3794
+ program.command("init").option("--force", t("cli.opt.force")).description(t("cli.cmd.init")).action((opts) => init(opts));
3231
3795
  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));
3232
3796
  program.command("remove-agent").argument("<name>", t("cli.arg.removeAgentName")).description(t("cli.cmd.removeAgent")).action((name) => removeAgent(name));
3233
3797
  program.command("list-agents").alias("agents").description(t("cli.cmd.listAgents")).action(() => listAgents());
@@ -3240,11 +3804,11 @@ function buildProgram() {
3240
3804
  }
3241
3805
  async function main() {
3242
3806
  try {
3243
- loadDotenv([join3(configDir(), ".env"), join3(process.cwd(), ".env")]);
3807
+ loadDotenv([join5(configDir(), ".env"), join5(process.cwd(), ".env")]);
3244
3808
  await resolveLocale();
3245
3809
  await buildProgram().parseAsync(process.argv);
3246
3810
  } catch (err) {
3247
- console.error(pc12.red(`\u2717 ${err.message}`));
3811
+ console.error(pc13.red(`\u2717 ${err.message}`));
3248
3812
  process.exitCode = 1;
3249
3813
  }
3250
3814
  }