@gaberrb/polypus 0.2.2 → 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/dist/index.js CHANGED
@@ -177,6 +177,15 @@ var en = {
177
177
  "swarm.mergeConflict": " conflict merging {branch}",
178
178
  "swarm.summary": "Summary:",
179
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",
180
189
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) had merge conflicts (kept for inspection):",
181
190
  "swarm.statusDone": "done",
182
191
  "swarm.statusIncomplete": "incomplete",
@@ -373,6 +382,15 @@ var ptBR = {
373
382
  "swarm.mergeConflict": " conflito ao mesclar {branch}",
374
383
  "swarm.summary": "Resumo:",
375
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",
376
394
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) tiveram conflitos de merge (mantidos para inspe\xE7\xE3o):",
377
395
  "swarm.statusDone": "ok",
378
396
  "swarm.statusIncomplete": "incompleta",
@@ -2202,9 +2220,9 @@ var TENTACLE_FRAMES = [
2202
2220
  ];
2203
2221
  var WIDTH = Math.max(...[...ART].map((l) => [...l].length));
2204
2222
  function center(line) {
2205
- const pad = WIDTH - [...line].length;
2206
- const left = Math.floor(pad / 2);
2207
- return " ".repeat(left) + line + " ".repeat(pad - left);
2223
+ const pad2 = WIDTH - [...line].length;
2224
+ const left = Math.floor(pad2 / 2);
2225
+ return " ".repeat(left) + line + " ".repeat(pad2 - left);
2208
2226
  }
2209
2227
  var GLYPHS = {
2210
2228
  P: ["\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588 "],
@@ -2835,6 +2853,169 @@ function extractJsonArray(text2) {
2835
2853
  }
2836
2854
  }
2837
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
+
2838
3019
  // src/cli/commands/swarm.ts
2839
3020
  async function swarm(task, opts) {
2840
3021
  const config = await loadConfig();
@@ -2851,15 +3032,32 @@ async function swarm(task, opts) {
2851
3032
  pc8.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace: process.cwd() }))
2852
3033
  );
2853
3034
  console.log(pc8.yellow(t("swarm.bypassNote") + "\n"));
2854
- const result = await runSwarm({
2855
- task,
2856
- workspace: process.cwd(),
2857
- agents: resolved,
2858
- allow: config.permissions.allow,
2859
- deny: config.permissions.deny,
2860
- maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0,
2861
- events: renderSwarmEvents()
2862
- });
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("");
2863
3061
  console.log(pc8.bold("\n" + t("swarm.summary")));
2864
3062
  for (const o of result.outcomes) {
2865
3063
  const status = o.finished ? pc8.green(t("swarm.statusDone")) : pc8.yellow(t("swarm.statusIncomplete"));
@@ -2876,26 +3074,6 @@ async function swarm(task, opts) {
2876
3074
  console.log(pc8.green("\n" + t("swarm.allMerged")));
2877
3075
  }
2878
3076
  }
2879
- function renderSwarmEvents() {
2880
- return {
2881
- onDecomposed(subtasks) {
2882
- console.log(pc8.bold(t("swarm.decomposed", { n: subtasks.length })));
2883
- for (const s of subtasks) console.log(pc8.dim(` ${s.id}: ${s.title}`));
2884
- console.log("");
2885
- },
2886
- onWorkerStart(subtask, agentName) {
2887
- console.log(pc8.cyan(t("swarm.workerStart", { id: subtask.id, agent: agentName })));
2888
- },
2889
- onWorkerDone(o) {
2890
- const head = o.finished ? pc8.green(t("swarm.workerDone", { id: o.subtask.id })) : pc8.yellow(t("swarm.workerStopped", { id: o.subtask.id }));
2891
- const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
2892
- console.log(head + pc8.dim(t("swarm.workerMeta", { steps: o.steps, changes })));
2893
- },
2894
- onMerge(m) {
2895
- console.log(m.ok ? pc8.dim(t("swarm.merged", { branch: m.branch })) : pc8.red(t("swarm.mergeConflict", { branch: m.branch })));
2896
- }
2897
- };
2898
- }
2899
3077
 
2900
3078
  // src/cli/commands/models.ts
2901
3079
  import pc9 from "picocolors";
@@ -2991,12 +3169,18 @@ ${comments}` : "",
2991
3169
  "## Riscos e alternativas"
2992
3170
  ].join("\n");
2993
3171
  }
2994
- async function generatePrd(issue, provider) {
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) });
2995
3182
  const res = await provider.chat({
2996
- messages: [
2997
- { role: "system", content: SYSTEM },
2998
- { role: "user", content: buildPrdPrompt(issue) }
2999
- ],
3183
+ messages,
3000
3184
  params: { maxTokens: 2e3, temperature: 0.2 }
3001
3185
  });
3002
3186
  const text2 = res.content.trim();
@@ -3041,6 +3225,23 @@ async function withRetry(fn, opts = {}) {
3041
3225
  }
3042
3226
 
3043
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
+ }
3044
3245
  function numericRef(ref) {
3045
3246
  const num = ref.replace(/^#/, "");
3046
3247
  if (!/^\d+$/.test(num)) throw new Error(t("cli.invalidRef", { ref }));
@@ -3061,7 +3262,8 @@ var exec2 = promisify2(execFile);
3061
3262
  async function prd(issueRef, opts) {
3062
3263
  const issue = await loadIssue(issueRef, opts.input);
3063
3264
  const { provider } = resolveFreeProvider(opts.model ?? DEFAULT_PRD_MODEL);
3064
- const markdown = await withRetry(() => generatePrd(issue, provider));
3265
+ const guide = readProjectGuide(["context.md"]);
3266
+ const markdown = await withRetry(() => generatePrd(issue, provider, guide));
3065
3267
  if (opts.out) {
3066
3268
  await writeFile4(opts.out, markdown + "\n", "utf8");
3067
3269
  console.error(pc10.green(t("prd.wrote", { path: opts.out })));
@@ -3129,13 +3331,19 @@ function buildReviewPrompt(diff, meta) {
3129
3331
  "Liste os achados agrupados por severidade (\u{1F534} bug, \u{1F7E1} aten\xE7\xE3o, \u{1F7E2} sugest\xE3o)."
3130
3332
  ].join("\n");
3131
3333
  }
3132
- async function reviewDiff(diff, meta, provider) {
3334
+ async function reviewDiff(diff, meta, provider, projectGuide) {
3133
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) });
3134
3345
  const res = await provider.chat({
3135
- messages: [
3136
- { role: "system", content: SYSTEM2 },
3137
- { role: "user", content: buildReviewPrompt(diff, meta) }
3138
- ],
3346
+ messages,
3139
3347
  params: { maxTokens: 1500, temperature: 0.2 }
3140
3348
  });
3141
3349
  const text2 = res.content.trim();
@@ -3150,7 +3358,8 @@ async function review(prRef, opts) {
3150
3358
  const diff = await loadDiff(num, opts.input);
3151
3359
  const meta = await loadMeta(num, opts.input);
3152
3360
  const { provider } = resolveFreeProvider(opts.model ?? DEFAULT_REVIEW_MODEL);
3153
- const markdown = await withRetry(() => reviewDiff(diff, meta, provider));
3361
+ const guide = readProjectGuide(["rules.md", "context.md"]);
3362
+ const markdown = await withRetry(() => reviewDiff(diff, meta, provider, guide));
3154
3363
  if (opts.out) {
3155
3364
  await writeFile5(opts.out, markdown + "\n", "utf8");
3156
3365
  console.error(pc11.green(t("review.wrote", { path: opts.out })));
@@ -3174,13 +3383,13 @@ async function loadMeta(num, input) {
3174
3383
  import { join as join3 } from "path";
3175
3384
 
3176
3385
  // src/core/config/dotenv.ts
3177
- import { existsSync as existsSync2, readFileSync } from "fs";
3386
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
3178
3387
  function loadDotenv(paths) {
3179
3388
  for (const path of paths) {
3180
- if (!existsSync2(path)) continue;
3389
+ if (!existsSync3(path)) continue;
3181
3390
  let text2;
3182
3391
  try {
3183
- text2 = readFileSync(path, "utf8");
3392
+ text2 = readFileSync2(path, "utf8");
3184
3393
  } catch {
3185
3394
  continue;
3186
3395
  }