@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/README.md +28 -0
- package/dist/index.js +1046 -549
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
1714
|
-
|
|
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(
|
|
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/
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
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
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
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
|
-
|
|
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/
|
|
2497
|
-
async function
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
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
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
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
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
const
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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(
|
|
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(
|
|
3193
|
+
console.log(pc8.dim("\n" + t("run.cancelled")));
|
|
2589
3194
|
} else if (result.reason === "stalled" || result.reason === "maxsteps") {
|
|
2590
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 ?
|
|
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(
|
|
3272
|
+
console.log(pc8.yellow(" " + t("run.reprompt", { attempt })));
|
|
2668
3273
|
},
|
|
2669
3274
|
onCorrection() {
|
|
2670
3275
|
spinner3.stop();
|
|
2671
|
-
console.log(
|
|
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/
|
|
2682
|
-
import
|
|
3286
|
+
// src/cli/commands/init.ts
|
|
3287
|
+
import pc9 from "picocolors";
|
|
2683
3288
|
|
|
2684
|
-
// src/core/
|
|
2685
|
-
import {
|
|
2686
|
-
import {
|
|
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/
|
|
2742
|
-
|
|
2743
|
-
|
|
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
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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
|
-
|
|
3539
|
+
await mkdir3(dirname3(abs), { recursive: true });
|
|
3540
|
+
await writeFile4(abs, content, "utf8");
|
|
3541
|
+
created.push(display);
|
|
2841
3542
|
}
|
|
2842
|
-
return
|
|
3543
|
+
return { created, skipped };
|
|
2843
3544
|
}
|
|
2844
|
-
function
|
|
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
|
-
|
|
2850
|
-
return
|
|
3547
|
+
await access(path);
|
|
3548
|
+
return true;
|
|
2851
3549
|
} catch {
|
|
2852
|
-
return
|
|
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/
|
|
3020
|
-
async function
|
|
3021
|
-
const
|
|
3022
|
-
|
|
3023
|
-
|
|
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
|
-
|
|
3032
|
-
|
|
3033
|
-
);
|
|
3034
|
-
|
|
3035
|
-
|
|
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
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
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
|
|
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(
|
|
3585
|
+
spin.stop(pc10.green("\u2713 OpenRouter"));
|
|
3089
3586
|
} catch (err) {
|
|
3090
|
-
spin.stop(
|
|
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(
|
|
3601
|
+
console.log(pc10.dim(t("models.legend")));
|
|
3105
3602
|
if (models2.length === 0) {
|
|
3106
|
-
console.log(
|
|
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
|
-
" " +
|
|
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(
|
|
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 ?
|
|
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 ?
|
|
3122
|
-
const ctx =
|
|
3123
|
-
return `${tools} ${priceColored}${ctx}${
|
|
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
|
|
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
|
|
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
|
|
3269
|
-
console.error(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3365
|
-
console.error(
|
|
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() :
|
|
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
|
|
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(" " +
|
|
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(
|
|
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([
|
|
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(
|
|
3953
|
+
console.error(pc13.red(`\u2717 ${err.message}`));
|
|
3457
3954
|
process.exitCode = 1;
|
|
3458
3955
|
}
|
|
3459
3956
|
}
|