@gaberrb/polypus 0.2.1 → 0.3.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/README.md +1 -1
- package/dist/index.js +511 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import
|
|
6
|
+
import pc12 from "picocolors";
|
|
7
7
|
|
|
8
8
|
// src/cli/commands/add-agent.ts
|
|
9
9
|
import pc from "picocolors";
|
|
@@ -80,6 +80,16 @@ var en = {
|
|
|
80
80
|
"cli.cmd.run": "Run a coding task with an agent",
|
|
81
81
|
"cli.cmd.swarm": "Split a task across multiple agents working in parallel git worktrees",
|
|
82
82
|
"cli.cmd.models": "Browse OpenRouter models (price, context, tool support)",
|
|
83
|
+
"cli.cmd.prd": "Generate a PRD from a GitHub issue (uses a free OpenRouter model)",
|
|
84
|
+
"cli.arg.prdIssue": "issue number to turn into a PRD",
|
|
85
|
+
"cli.cmd.review": "Review a pull request diff (uses a free OpenRouter model)",
|
|
86
|
+
"cli.arg.reviewPr": "pull request number to review",
|
|
87
|
+
"cli.opt.out": "write output to this file instead of stdout",
|
|
88
|
+
"cli.opt.input": 'read input from a file (or "-" for stdin) instead of calling gh',
|
|
89
|
+
"prd.wrote": "\u2713 PRD written to {path}",
|
|
90
|
+
"review.wrote": "\u2713 Review written to {path}",
|
|
91
|
+
"cli.invalidRef": "Invalid number '{ref}': expected a numeric issue/PR number.",
|
|
92
|
+
"cli.stdinTty": "--input - expects piped stdin, but none was provided.",
|
|
83
93
|
"cli.opt.search": "filter by id/name substring",
|
|
84
94
|
"cli.opt.toolsOnly": "only models that support tool-calling",
|
|
85
95
|
"cli.opt.free": "only free models",
|
|
@@ -167,6 +177,15 @@ var en = {
|
|
|
167
177
|
"swarm.mergeConflict": " conflict merging {branch}",
|
|
168
178
|
"swarm.summary": "Summary:",
|
|
169
179
|
"swarm.allMerged": "\u2713 All committed branches merged cleanly.",
|
|
180
|
+
"swarm.view.header": "Swarm \xB7 orchestrator [{lead}]",
|
|
181
|
+
"swarm.view.decomposing": "splitting the task\u2026",
|
|
182
|
+
"swarm.view.pending": "queued",
|
|
183
|
+
"swarm.view.running": "running",
|
|
184
|
+
"swarm.view.done": "done",
|
|
185
|
+
"swarm.view.stopped": "stopped",
|
|
186
|
+
"swarm.view.conflict": "conflict",
|
|
187
|
+
"swarm.view.step": "step {n}",
|
|
188
|
+
"swarm.view.steps": "{n} steps",
|
|
170
189
|
"swarm.conflictsHeader": "\u26A0 {n} branch(es) had merge conflicts (kept for inspection):",
|
|
171
190
|
"swarm.statusDone": "done",
|
|
172
191
|
"swarm.statusIncomplete": "incomplete",
|
|
@@ -270,6 +289,16 @@ var ptBR = {
|
|
|
270
289
|
"cli.cmd.run": "Executa uma tarefa de c\xF3digo com um agente",
|
|
271
290
|
"cli.cmd.swarm": "Divide uma tarefa entre v\xE1rios agentes trabalhando em paralelo em git worktrees",
|
|
272
291
|
"cli.cmd.models": "Explora os modelos do OpenRouter (pre\xE7o, contexto, suporte a tools)",
|
|
292
|
+
"cli.cmd.prd": "Gera um PRD a partir de uma issue do GitHub (usa um modelo gratuito do OpenRouter)",
|
|
293
|
+
"cli.arg.prdIssue": "n\xFAmero da issue para transformar em PRD",
|
|
294
|
+
"cli.cmd.review": "Revisa o diff de um pull request (usa um modelo gratuito do OpenRouter)",
|
|
295
|
+
"cli.arg.reviewPr": "n\xFAmero do pull request a revisar",
|
|
296
|
+
"cli.opt.out": "grava a sa\xEDda neste arquivo em vez do stdout",
|
|
297
|
+
"cli.opt.input": 'l\xEA a entrada de um arquivo (ou "-" para stdin) em vez de chamar o gh',
|
|
298
|
+
"prd.wrote": "\u2713 PRD gravado em {path}",
|
|
299
|
+
"review.wrote": "\u2713 Review gravado em {path}",
|
|
300
|
+
"cli.invalidRef": "N\xFAmero inv\xE1lido '{ref}': esperado um n\xFAmero de issue/PR.",
|
|
301
|
+
"cli.stdinTty": "--input - espera entrada via pipe (stdin), mas nenhuma foi fornecida.",
|
|
273
302
|
"cli.opt.search": "filtra por trecho do id/nome",
|
|
274
303
|
"cli.opt.toolsOnly": "apenas modelos com suporte a tool-calling",
|
|
275
304
|
"cli.opt.free": "apenas modelos gratuitos",
|
|
@@ -353,6 +382,15 @@ var ptBR = {
|
|
|
353
382
|
"swarm.mergeConflict": " conflito ao mesclar {branch}",
|
|
354
383
|
"swarm.summary": "Resumo:",
|
|
355
384
|
"swarm.allMerged": "\u2713 Todos os branches commitados foram mesclados sem conflito.",
|
|
385
|
+
"swarm.view.header": "Swarm \xB7 orquestrador [{lead}]",
|
|
386
|
+
"swarm.view.decomposing": "dividindo a tarefa\u2026",
|
|
387
|
+
"swarm.view.pending": "na fila",
|
|
388
|
+
"swarm.view.running": "executando",
|
|
389
|
+
"swarm.view.done": "conclu\xEDdo",
|
|
390
|
+
"swarm.view.stopped": "parado",
|
|
391
|
+
"swarm.view.conflict": "conflito",
|
|
392
|
+
"swarm.view.step": "passo {n}",
|
|
393
|
+
"swarm.view.steps": "{n} passos",
|
|
356
394
|
"swarm.conflictsHeader": "\u26A0 {n} branch(es) tiveram conflitos de merge (mantidos para inspe\xE7\xE3o):",
|
|
357
395
|
"swarm.statusDone": "ok",
|
|
358
396
|
"swarm.statusIncomplete": "incompleta",
|
|
@@ -1448,6 +1486,14 @@ function getTool(name) {
|
|
|
1448
1486
|
// src/core/agent/correction.ts
|
|
1449
1487
|
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
1450
1488
|
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
1489
|
+
function truncationGuidance(toolName) {
|
|
1490
|
+
const fileHint = toolName === "write_file" || toolName === "edit_file" ? " Write large files in parts: create the file with the first chunk via write_file, then append the rest with edit_file in the next steps." : "";
|
|
1491
|
+
return [
|
|
1492
|
+
"AUTO-CORRECTION \u2014 your previous response was cut off at the output token limit,",
|
|
1493
|
+
"so the tool call was incomplete (e.g. 'content' missing or partial).",
|
|
1494
|
+
"Do NOT resend the same large output \u2014 produce a smaller one this time." + fileHint
|
|
1495
|
+
].join("\n");
|
|
1496
|
+
}
|
|
1451
1497
|
async function buildCorrection(call, output, deps) {
|
|
1452
1498
|
const deterministic = await deterministicCorrection(call, output, deps);
|
|
1453
1499
|
if (deterministic) return deterministic;
|
|
@@ -1455,6 +1501,7 @@ async function buildCorrection(call, output, deps) {
|
|
|
1455
1501
|
return null;
|
|
1456
1502
|
}
|
|
1457
1503
|
async function deterministicCorrection(call, output, deps) {
|
|
1504
|
+
if (deps.truncated) return truncationGuidance(call.name);
|
|
1458
1505
|
const path = typeof call.arguments.path === "string" ? call.arguments.path : void 0;
|
|
1459
1506
|
if (call.name === "edit_file" && /was not found/i.test(output) && path) {
|
|
1460
1507
|
const search = typeof call.arguments.search === "string" ? call.arguments.search : "";
|
|
@@ -1694,7 +1741,18 @@ async function runAgent(opts) {
|
|
|
1694
1741
|
const { toolCalls, text: text2 } = driver.parse(response);
|
|
1695
1742
|
messages.push(driver.assistantMessage(response, toolCalls));
|
|
1696
1743
|
if (text2) events?.onAssistantText?.(text2);
|
|
1744
|
+
const truncated = response.finishReason === "length" || response.finishReason === "max_tokens";
|
|
1697
1745
|
if (toolCalls.length === 0) {
|
|
1746
|
+
if (truncated && autoCorrect) {
|
|
1747
|
+
if (consecutiveNoTool < maxReprompts) {
|
|
1748
|
+
consecutiveNoTool++;
|
|
1749
|
+
const guidance = truncationGuidance();
|
|
1750
|
+
events?.onCorrection?.({ id: "trunc", name: "", arguments: {} }, guidance);
|
|
1751
|
+
messages.push({ role: "user", content: guidance });
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
return { finished: false, reason: "stalled", steps: step, messages, usage };
|
|
1755
|
+
}
|
|
1698
1756
|
const stalled = text2.trim().length === 0 || looksLikeStall(text2);
|
|
1699
1757
|
if (stalled) {
|
|
1700
1758
|
if (consecutiveNoTool < maxReprompts) {
|
|
@@ -1725,6 +1783,7 @@ async function runAgent(opts) {
|
|
|
1725
1783
|
workspace: opts.workspace,
|
|
1726
1784
|
allow: opts.promptContext.allow,
|
|
1727
1785
|
toolSpec: tool?.spec,
|
|
1786
|
+
truncated,
|
|
1728
1787
|
// Only spend a fixer-LLM call once the model has already repeated a
|
|
1729
1788
|
// failing call — the first failure gets deterministic help for free.
|
|
1730
1789
|
escalate: sig === lastFailSig ? makeLLMEscalator(agent.provider) : void 0
|
|
@@ -1735,6 +1794,12 @@ async function runAgent(opts) {
|
|
|
1735
1794
|
${guidance}`;
|
|
1736
1795
|
events?.onCorrection?.(call, guidance);
|
|
1737
1796
|
}
|
|
1797
|
+
} else if (result.ok && truncated && autoCorrect) {
|
|
1798
|
+
const guidance = truncationGuidance(call.name);
|
|
1799
|
+
resultText = `${result.output}
|
|
1800
|
+
|
|
1801
|
+
${guidance}`;
|
|
1802
|
+
events?.onCorrection?.(call, guidance);
|
|
1738
1803
|
}
|
|
1739
1804
|
messages.push(driver.toolResultMessage(call, resultText));
|
|
1740
1805
|
if (result.ok) {
|
|
@@ -2155,9 +2220,9 @@ var TENTACLE_FRAMES = [
|
|
|
2155
2220
|
];
|
|
2156
2221
|
var WIDTH = Math.max(...[...ART].map((l) => [...l].length));
|
|
2157
2222
|
function center(line) {
|
|
2158
|
-
const
|
|
2159
|
-
const left = Math.floor(
|
|
2160
|
-
return " ".repeat(left) + line + " ".repeat(
|
|
2223
|
+
const pad2 = WIDTH - [...line].length;
|
|
2224
|
+
const left = Math.floor(pad2 / 2);
|
|
2225
|
+
return " ".repeat(left) + line + " ".repeat(pad2 - left);
|
|
2161
2226
|
}
|
|
2162
2227
|
var GLYPHS = {
|
|
2163
2228
|
P: ["\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588 "],
|
|
@@ -2788,6 +2853,169 @@ function extractJsonArray(text2) {
|
|
|
2788
2853
|
}
|
|
2789
2854
|
}
|
|
2790
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;
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
function pad(s, n) {
|
|
3016
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2791
3019
|
// src/cli/commands/swarm.ts
|
|
2792
3020
|
async function swarm(task, opts) {
|
|
2793
3021
|
const config = await loadConfig();
|
|
@@ -2804,15 +3032,32 @@ async function swarm(task, opts) {
|
|
|
2804
3032
|
pc8.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace: process.cwd() }))
|
|
2805
3033
|
);
|
|
2806
3034
|
console.log(pc8.yellow(t("swarm.bypassNote") + "\n"));
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3035
|
+
const view = new SwarmView(resolved[0].config.name);
|
|
3036
|
+
view.start();
|
|
3037
|
+
let result;
|
|
3038
|
+
try {
|
|
3039
|
+
result = await runSwarm({
|
|
3040
|
+
task,
|
|
3041
|
+
workspace: process.cwd(),
|
|
3042
|
+
agents: resolved,
|
|
3043
|
+
allow: config.permissions.allow,
|
|
3044
|
+
deny: config.permissions.deny,
|
|
3045
|
+
maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0,
|
|
3046
|
+
events: {
|
|
3047
|
+
onDecomposed: (subtasks) => view.setSubtasks(subtasks),
|
|
3048
|
+
onWorkerStart: (subtask, agentName) => view.workerStart(subtask.id, agentName),
|
|
3049
|
+
onWorkerDone: (outcome) => view.workerDone(outcome),
|
|
3050
|
+
onMerge: (merge) => view.merge(merge),
|
|
3051
|
+
workerEvents: (subtask) => ({
|
|
3052
|
+
onToolCall: (call) => view.workerAction(subtask.id, describeToolCall(call)),
|
|
3053
|
+
onStep: (step) => view.workerStep(subtask.id, step)
|
|
3054
|
+
})
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
} finally {
|
|
3058
|
+
view.stop();
|
|
3059
|
+
}
|
|
3060
|
+
console.log("");
|
|
2816
3061
|
console.log(pc8.bold("\n" + t("swarm.summary")));
|
|
2817
3062
|
for (const o of result.outcomes) {
|
|
2818
3063
|
const status = o.finished ? pc8.green(t("swarm.statusDone")) : pc8.yellow(t("swarm.statusIncomplete"));
|
|
@@ -2829,26 +3074,6 @@ async function swarm(task, opts) {
|
|
|
2829
3074
|
console.log(pc8.green("\n" + t("swarm.allMerged")));
|
|
2830
3075
|
}
|
|
2831
3076
|
}
|
|
2832
|
-
function renderSwarmEvents() {
|
|
2833
|
-
return {
|
|
2834
|
-
onDecomposed(subtasks) {
|
|
2835
|
-
console.log(pc8.bold(t("swarm.decomposed", { n: subtasks.length })));
|
|
2836
|
-
for (const s of subtasks) console.log(pc8.dim(` ${s.id}: ${s.title}`));
|
|
2837
|
-
console.log("");
|
|
2838
|
-
},
|
|
2839
|
-
onWorkerStart(subtask, agentName) {
|
|
2840
|
-
console.log(pc8.cyan(t("swarm.workerStart", { id: subtask.id, agent: agentName })));
|
|
2841
|
-
},
|
|
2842
|
-
onWorkerDone(o) {
|
|
2843
|
-
const head = o.finished ? pc8.green(t("swarm.workerDone", { id: o.subtask.id })) : pc8.yellow(t("swarm.workerStopped", { id: o.subtask.id }));
|
|
2844
|
-
const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
|
|
2845
|
-
console.log(head + pc8.dim(t("swarm.workerMeta", { steps: o.steps, changes })));
|
|
2846
|
-
},
|
|
2847
|
-
onMerge(m) {
|
|
2848
|
-
console.log(m.ok ? pc8.dim(t("swarm.merged", { branch: m.branch })) : pc8.red(t("swarm.mergeConflict", { branch: m.branch })));
|
|
2849
|
-
}
|
|
2850
|
-
};
|
|
2851
|
-
}
|
|
2852
3077
|
|
|
2853
3078
|
// src/cli/commands/models.ts
|
|
2854
3079
|
import pc9 from "picocolors";
|
|
@@ -2908,17 +3133,263 @@ async function resolveOpenRouterKey() {
|
|
|
2908
3133
|
}
|
|
2909
3134
|
}
|
|
2910
3135
|
|
|
3136
|
+
// src/cli/commands/prd.ts
|
|
3137
|
+
import { writeFile as writeFile4, readFile as readFile5 } from "fs/promises";
|
|
3138
|
+
import { execFile } from "child_process";
|
|
3139
|
+
import { promisify as promisify2 } from "util";
|
|
3140
|
+
import pc10 from "picocolors";
|
|
3141
|
+
|
|
3142
|
+
// src/core/agent/prd.ts
|
|
3143
|
+
var SYSTEM = [
|
|
3144
|
+
"You are a product analyst. You turn a GitHub issue into a concise, structured PRD",
|
|
3145
|
+
"(Product Requirements Document) in Markdown.",
|
|
3146
|
+
"Rules:",
|
|
3147
|
+
"- Use ONLY information present in the issue and its comments. Do NOT invent scope,",
|
|
3148
|
+
" numbers, deadlines, or stakeholders. If something is unknown, write 'A definir'.",
|
|
3149
|
+
"- Be objective and short; prefer bullet points over prose.",
|
|
3150
|
+
"- Write in the same language as the issue (Portuguese if the issue is in Portuguese)."
|
|
3151
|
+
].join("\n");
|
|
3152
|
+
function buildPrdPrompt(issue) {
|
|
3153
|
+
const comments = (issue.comments ?? []).map((c, i) => `Coment\xE1rio ${i + 1}${c.author ? ` (@${c.author})` : ""}:
|
|
3154
|
+
${c.body}`).join("\n\n");
|
|
3155
|
+
return [
|
|
3156
|
+
`Issue${issue.number ? ` #${issue.number}` : ""}: ${issue.title}`,
|
|
3157
|
+
"",
|
|
3158
|
+
"Corpo:",
|
|
3159
|
+
issue.body || "(vazio)",
|
|
3160
|
+
comments ? `
|
|
3161
|
+
${comments}` : "",
|
|
3162
|
+
"",
|
|
3163
|
+
"Gere um PRD com EXATAMENTE estas se\xE7\xF5es (H2 com ##):",
|
|
3164
|
+
"## Contexto / Problema",
|
|
3165
|
+
"## Objetivo",
|
|
3166
|
+
"## Escopo (in / out)",
|
|
3167
|
+
"## Requisitos funcionais",
|
|
3168
|
+
"## Crit\xE9rios de aceite",
|
|
3169
|
+
"## Riscos e alternativas"
|
|
3170
|
+
].join("\n");
|
|
3171
|
+
}
|
|
3172
|
+
async function generatePrd(issue, provider, projectContext) {
|
|
3173
|
+
const messages = [{ role: "system", content: SYSTEM }];
|
|
3174
|
+
if (projectContext) {
|
|
3175
|
+
messages.push({
|
|
3176
|
+
role: "system",
|
|
3177
|
+
content: `Project context (for grounding; do not restate verbatim):
|
|
3178
|
+
${projectContext}`
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
messages.push({ role: "user", content: buildPrdPrompt(issue) });
|
|
3182
|
+
const res = await provider.chat({
|
|
3183
|
+
messages,
|
|
3184
|
+
params: { maxTokens: 2e3, temperature: 0.2 }
|
|
3185
|
+
});
|
|
3186
|
+
const text2 = res.content.trim();
|
|
3187
|
+
if (!text2) throw new Error("The model returned an empty PRD.");
|
|
3188
|
+
return text2;
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// src/core/agent/free-provider.ts
|
|
3192
|
+
function resolveFreeProvider(model) {
|
|
3193
|
+
if (!process.env.OPENROUTER_API_KEY) {
|
|
3194
|
+
throw new Error(
|
|
3195
|
+
"OPENROUTER_API_KEY is not set. Export it (or add it as a repo secret in CI) before running this command."
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
const config = {
|
|
3199
|
+
name: "openrouter-free",
|
|
3200
|
+
provider: "openrouter",
|
|
3201
|
+
model,
|
|
3202
|
+
apiKey: "${OPENROUTER_API_KEY}",
|
|
3203
|
+
toolMode: "native"
|
|
3204
|
+
};
|
|
3205
|
+
return createProvider(config);
|
|
3206
|
+
}
|
|
3207
|
+
var DEFAULT_PRD_MODEL = process.env.POLYPUS_PRD_MODEL ?? "openai/gpt-oss-120b:free";
|
|
3208
|
+
var DEFAULT_REVIEW_MODEL = process.env.POLYPUS_REVIEW_MODEL ?? "openai/gpt-oss-120b:free";
|
|
3209
|
+
async function withRetry(fn, opts = {}) {
|
|
3210
|
+
const attempts = opts.attempts ?? 4;
|
|
3211
|
+
const baseMs = opts.baseMs ?? 2e3;
|
|
3212
|
+
let lastErr;
|
|
3213
|
+
for (let i = 0; i < attempts; i++) {
|
|
3214
|
+
try {
|
|
3215
|
+
return await fn();
|
|
3216
|
+
} catch (err) {
|
|
3217
|
+
lastErr = err;
|
|
3218
|
+
const msg = err?.message ?? "";
|
|
3219
|
+
const transient = /\b429\b|\b5\d\d\b|rate|timeout|ETIMEDOUT|ECONNRESET|EAI_AGAIN/i.test(msg);
|
|
3220
|
+
if (!transient || i === attempts - 1) throw err;
|
|
3221
|
+
await new Promise((r) => setTimeout(r, baseMs * 2 ** i));
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
throw lastErr;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
// src/cli/commands/cli-io.ts
|
|
3228
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
3229
|
+
import { resolve as resolve7 } from "path";
|
|
3230
|
+
var GUIDE_MAX = 12e3;
|
|
3231
|
+
function readProjectGuide(files) {
|
|
3232
|
+
const parts = [];
|
|
3233
|
+
for (const file of files) {
|
|
3234
|
+
try {
|
|
3235
|
+
const path = resolve7(process.cwd(), file);
|
|
3236
|
+
if (existsSync2(path)) parts.push(`# ${file}
|
|
3237
|
+
${readFileSync(path, "utf8").trim()}`);
|
|
3238
|
+
} catch {
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
if (parts.length === 0) return void 0;
|
|
3242
|
+
const joined = parts.join("\n\n");
|
|
3243
|
+
return joined.length > GUIDE_MAX ? joined.slice(0, GUIDE_MAX) + "\n\u2026(truncated)" : joined;
|
|
3244
|
+
}
|
|
3245
|
+
function numericRef(ref) {
|
|
3246
|
+
const num = ref.replace(/^#/, "");
|
|
3247
|
+
if (!/^\d+$/.test(num)) throw new Error(t("cli.invalidRef", { ref }));
|
|
3248
|
+
return num;
|
|
3249
|
+
}
|
|
3250
|
+
async function readStdin() {
|
|
3251
|
+
if (process.stdin.isTTY) throw new Error(t("cli.stdinTty"));
|
|
3252
|
+
const chunks = [];
|
|
3253
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
3254
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3255
|
+
}
|
|
3256
|
+
function stripBom(s) {
|
|
3257
|
+
return s.charCodeAt(0) === 65279 ? s.slice(1) : s;
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// src/cli/commands/prd.ts
|
|
3261
|
+
var exec2 = promisify2(execFile);
|
|
3262
|
+
async function prd(issueRef, opts) {
|
|
3263
|
+
const issue = await loadIssue(issueRef, opts.input);
|
|
3264
|
+
const { provider } = resolveFreeProvider(opts.model ?? DEFAULT_PRD_MODEL);
|
|
3265
|
+
const guide = readProjectGuide(["context.md"]);
|
|
3266
|
+
const markdown = await withRetry(() => generatePrd(issue, provider, guide));
|
|
3267
|
+
if (opts.out) {
|
|
3268
|
+
await writeFile4(opts.out, markdown + "\n", "utf8");
|
|
3269
|
+
console.error(pc10.green(t("prd.wrote", { path: opts.out })));
|
|
3270
|
+
} else {
|
|
3271
|
+
process.stdout.write(markdown + "\n");
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
async function loadIssue(issueRef, input) {
|
|
3275
|
+
if (input) {
|
|
3276
|
+
const raw = input === "-" ? await readStdin() : await readFile5(input, "utf8");
|
|
3277
|
+
return normalize2(JSON.parse(stripBom(raw)));
|
|
3278
|
+
}
|
|
3279
|
+
const num = numericRef(issueRef);
|
|
3280
|
+
const { stdout: stdout2 } = await exec2("gh", ["issue", "view", num, "--json", "number,title,body,comments"]);
|
|
3281
|
+
const data = normalize2(JSON.parse(stdout2));
|
|
3282
|
+
data.number ??= Number(num);
|
|
3283
|
+
return data;
|
|
3284
|
+
}
|
|
3285
|
+
function normalize2(raw) {
|
|
3286
|
+
return {
|
|
3287
|
+
number: raw.number,
|
|
3288
|
+
title: raw.title ?? "",
|
|
3289
|
+
body: raw.body ?? "",
|
|
3290
|
+
comments: (raw.comments ?? []).map((c) => ({ author: c.author?.login, body: c.body ?? "" }))
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
// src/cli/commands/review.ts
|
|
3295
|
+
import { writeFile as writeFile5, readFile as readFile6 } from "fs/promises";
|
|
3296
|
+
import { execFile as execFile2 } from "child_process";
|
|
3297
|
+
import { promisify as promisify3 } from "util";
|
|
3298
|
+
import pc11 from "picocolors";
|
|
3299
|
+
|
|
3300
|
+
// src/core/agent/review.ts
|
|
3301
|
+
var MAX_DIFF_CHARS = Number(process.env.POLYPUS_MAX_DIFF_CHARS) || 6e4;
|
|
3302
|
+
var SYSTEM2 = [
|
|
3303
|
+
"You are a senior code reviewer. Review the pull request diff below and report",
|
|
3304
|
+
"concrete findings in Markdown.",
|
|
3305
|
+
"Rules:",
|
|
3306
|
+
"- Focus on correctness bugs, security risks, and clear, actionable improvements.",
|
|
3307
|
+
"- Reference file paths (and line hints when visible) so findings are easy to locate.",
|
|
3308
|
+
"- Be concise. Skip style nitpicks unless they hide a real problem.",
|
|
3309
|
+
"- If there are no relevant problems, say so briefly instead of inventing issues.",
|
|
3310
|
+
"- Write in the same language as the PR description (Portuguese if it is in Portuguese)."
|
|
3311
|
+
].join("\n");
|
|
3312
|
+
function clampDiff(diff, max = MAX_DIFF_CHARS) {
|
|
3313
|
+
if (diff.length <= max) return diff;
|
|
3314
|
+
const dropped = diff.length - max;
|
|
3315
|
+
return `${diff.slice(0, max)}
|
|
3316
|
+
|
|
3317
|
+
\u2026 [diff truncado: ${dropped} caracteres omitidos para caber no contexto do modelo]`;
|
|
3318
|
+
}
|
|
3319
|
+
function buildReviewPrompt(diff, meta) {
|
|
3320
|
+
return [
|
|
3321
|
+
`PR${meta.number ? ` #${meta.number}` : ""}: ${meta.title}`,
|
|
3322
|
+
"",
|
|
3323
|
+
"Descri\xE7\xE3o:",
|
|
3324
|
+
meta.body || "(vazia)",
|
|
3325
|
+
"",
|
|
3326
|
+
"Diff:",
|
|
3327
|
+
"```diff",
|
|
3328
|
+
clampDiff(diff),
|
|
3329
|
+
"```",
|
|
3330
|
+
"",
|
|
3331
|
+
"Liste os achados agrupados por severidade (\u{1F534} bug, \u{1F7E1} aten\xE7\xE3o, \u{1F7E2} sugest\xE3o)."
|
|
3332
|
+
].join("\n");
|
|
3333
|
+
}
|
|
3334
|
+
async function reviewDiff(diff, meta, provider, projectGuide) {
|
|
3335
|
+
if (!diff.trim()) return "_Sem altera\xE7\xF5es no diff para revisar._";
|
|
3336
|
+
const messages = [{ role: "system", content: SYSTEM2 }];
|
|
3337
|
+
if (projectGuide) {
|
|
3338
|
+
messages.push({
|
|
3339
|
+
role: "system",
|
|
3340
|
+
content: `Project context and conventions to review against:
|
|
3341
|
+
${projectGuide}`
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
messages.push({ role: "user", content: buildReviewPrompt(diff, meta) });
|
|
3345
|
+
const res = await provider.chat({
|
|
3346
|
+
messages,
|
|
3347
|
+
params: { maxTokens: 1500, temperature: 0.2 }
|
|
3348
|
+
});
|
|
3349
|
+
const text2 = res.content.trim();
|
|
3350
|
+
if (!text2) throw new Error("The model returned an empty review.");
|
|
3351
|
+
return text2;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
// src/cli/commands/review.ts
|
|
3355
|
+
var exec3 = promisify3(execFile2);
|
|
3356
|
+
async function review(prRef, opts) {
|
|
3357
|
+
const num = opts.input ? prRef.replace(/^#/, "") : numericRef(prRef);
|
|
3358
|
+
const diff = await loadDiff(num, opts.input);
|
|
3359
|
+
const meta = await loadMeta(num, opts.input);
|
|
3360
|
+
const { provider } = resolveFreeProvider(opts.model ?? DEFAULT_REVIEW_MODEL);
|
|
3361
|
+
const guide = readProjectGuide(["rules.md", "context.md"]);
|
|
3362
|
+
const markdown = await withRetry(() => reviewDiff(diff, meta, provider, guide));
|
|
3363
|
+
if (opts.out) {
|
|
3364
|
+
await writeFile5(opts.out, markdown + "\n", "utf8");
|
|
3365
|
+
console.error(pc11.green(t("review.wrote", { path: opts.out })));
|
|
3366
|
+
} else {
|
|
3367
|
+
process.stdout.write(markdown + "\n");
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
async function loadDiff(num, input) {
|
|
3371
|
+
if (input) return input === "-" ? readStdin() : readFile6(input, "utf8");
|
|
3372
|
+
const { stdout: stdout2 } = await exec3("gh", ["pr", "diff", num]);
|
|
3373
|
+
return stdout2;
|
|
3374
|
+
}
|
|
3375
|
+
async function loadMeta(num, input) {
|
|
3376
|
+
if (input) return { number: Number(num) || void 0, title: `PR ${num}`, body: "" };
|
|
3377
|
+
const { stdout: stdout2 } = await exec3("gh", ["pr", "view", num, "--json", "number,title,body"]);
|
|
3378
|
+
const raw = JSON.parse(stdout2);
|
|
3379
|
+
return { number: raw.number, title: raw.title ?? "", body: raw.body ?? "" };
|
|
3380
|
+
}
|
|
3381
|
+
|
|
2911
3382
|
// src/cli/index.ts
|
|
2912
3383
|
import { join as join3 } from "path";
|
|
2913
3384
|
|
|
2914
3385
|
// src/core/config/dotenv.ts
|
|
2915
|
-
import { existsSync as
|
|
3386
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2916
3387
|
function loadDotenv(paths) {
|
|
2917
3388
|
for (const path of paths) {
|
|
2918
|
-
if (!
|
|
3389
|
+
if (!existsSync3(path)) continue;
|
|
2919
3390
|
let text2;
|
|
2920
3391
|
try {
|
|
2921
|
-
text2 =
|
|
3392
|
+
text2 = readFileSync2(path, "utf8");
|
|
2922
3393
|
} catch {
|
|
2923
3394
|
continue;
|
|
2924
3395
|
}
|
|
@@ -2943,7 +3414,7 @@ async function launchInteractive() {
|
|
|
2943
3414
|
const config = await loadConfig();
|
|
2944
3415
|
if (config.agents.length === 0) {
|
|
2945
3416
|
console.log(banner());
|
|
2946
|
-
console.log(" " +
|
|
3417
|
+
console.log(" " + pc12.yellow(t("welcome.firstRun")) + "\n");
|
|
2947
3418
|
await setup();
|
|
2948
3419
|
}
|
|
2949
3420
|
await run(void 0, {});
|
|
@@ -2972,6 +3443,8 @@ function buildProgram() {
|
|
|
2972
3443
|
program.command("run").argument("[task]", t("cli.arg.runTask")).option("--agent <name>", t("cli.opt.agent")).option("--mode <mode>", t("cli.opt.mode")).option("--max-steps <n>", t("cli.opt.maxSteps")).description(t("cli.cmd.run")).action((task, opts) => run(task, opts));
|
|
2973
3444
|
program.command("swarm").argument("<task>", t("cli.arg.swarmTask")).option("--agents <names>", t("cli.opt.agents")).option("--max-subtasks <n>", t("cli.opt.maxSubtasks")).description(t("cli.cmd.swarm")).action((task, opts) => swarm(task, opts));
|
|
2974
3445
|
program.command("models").option("--search <text>", t("cli.opt.search")).option("--tools", t("cli.opt.toolsOnly")).option("--free", t("cli.opt.free")).option("--max-price <usd>", t("cli.opt.maxPrice")).option("--sort <order>", t("cli.opt.sort")).option("--limit <n>", t("cli.opt.limit")).description(t("cli.cmd.models")).action((opts) => models(opts));
|
|
3446
|
+
program.command("prd").argument("<issue>", t("cli.arg.prdIssue")).option("--out <file>", t("cli.opt.out")).option("--model <model>", t("cli.opt.model")).option("--input <file>", t("cli.opt.input")).description(t("cli.cmd.prd")).action((issue, opts) => prd(issue, opts));
|
|
3447
|
+
program.command("review").argument("<pr>", t("cli.arg.reviewPr")).option("--out <file>", t("cli.opt.out")).option("--model <model>", t("cli.opt.model")).option("--input <file>", t("cli.opt.input")).description(t("cli.cmd.review")).action((pr, opts) => review(pr, opts));
|
|
2975
3448
|
return program;
|
|
2976
3449
|
}
|
|
2977
3450
|
async function main() {
|
|
@@ -2980,7 +3453,7 @@ async function main() {
|
|
|
2980
3453
|
await resolveLocale();
|
|
2981
3454
|
await buildProgram().parseAsync(process.argv);
|
|
2982
3455
|
} catch (err) {
|
|
2983
|
-
console.error(
|
|
3456
|
+
console.error(pc12.red(`\u2717 ${err.message}`));
|
|
2984
3457
|
process.exitCode = 1;
|
|
2985
3458
|
}
|
|
2986
3459
|
}
|