@callumvass/forgeflow-dev 0.5.0 → 0.6.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/extensions/index.js +610 -482
- package/package.json +1 -1
package/extensions/index.js
CHANGED
|
@@ -5,8 +5,40 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
5
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
-
// ../shared/dist/
|
|
8
|
+
// ../shared/dist/exec.js
|
|
9
9
|
import { spawn } from "child_process";
|
|
10
|
+
function exec(cmd, cwd) {
|
|
11
|
+
return new Promise((resolve2, reject) => {
|
|
12
|
+
const proc = spawn("bash", ["-c", cmd], {
|
|
13
|
+
cwd,
|
|
14
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
15
|
+
});
|
|
16
|
+
let stdout = "";
|
|
17
|
+
let stderr = "";
|
|
18
|
+
proc.stdout.on("data", (d) => {
|
|
19
|
+
stdout += d.toString();
|
|
20
|
+
});
|
|
21
|
+
proc.stderr.on("data", (d) => {
|
|
22
|
+
stderr += d.toString();
|
|
23
|
+
});
|
|
24
|
+
proc.on("close", (code) => {
|
|
25
|
+
if (code !== 0) {
|
|
26
|
+
reject(new Error(`Command failed (exit ${code}): ${cmd}
|
|
27
|
+
${stderr.trim()}`));
|
|
28
|
+
} else {
|
|
29
|
+
resolve2(stdout.trim());
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
proc.on("error", (err) => reject(err));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function execSafe(cmd, cwd) {
|
|
36
|
+
try {
|
|
37
|
+
return await exec(cmd, cwd);
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
10
42
|
|
|
11
43
|
// ../shared/dist/constants.js
|
|
12
44
|
var TOOLS_ALL = ["read", "write", "edit", "bash", "grep", "find"];
|
|
@@ -18,12 +50,8 @@ var SIGNALS = {
|
|
|
18
50
|
blocked: "BLOCKED.md"
|
|
19
51
|
};
|
|
20
52
|
|
|
21
|
-
// ../shared/dist/
|
|
22
|
-
import {
|
|
23
|
-
import * as fs from "fs";
|
|
24
|
-
import * as os from "os";
|
|
25
|
-
import * as path from "path";
|
|
26
|
-
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
|
53
|
+
// ../shared/dist/rendering.js
|
|
54
|
+
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
27
55
|
|
|
28
56
|
// ../shared/dist/types.js
|
|
29
57
|
function emptyUsage() {
|
|
@@ -66,7 +94,131 @@ function sumUsage(stages) {
|
|
|
66
94
|
return total;
|
|
67
95
|
}
|
|
68
96
|
|
|
97
|
+
// ../shared/dist/rendering.js
|
|
98
|
+
function getDisplayItems(messages) {
|
|
99
|
+
const items = [];
|
|
100
|
+
for (const msg of messages) {
|
|
101
|
+
if (msg.role === "assistant") {
|
|
102
|
+
for (const part of msg.content) {
|
|
103
|
+
if (part.type === "text")
|
|
104
|
+
items.push({ type: "text", text: part.text });
|
|
105
|
+
else if (part.type === "toolCall")
|
|
106
|
+
items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return items;
|
|
111
|
+
}
|
|
112
|
+
function formatToolCallShort(name, args, fg) {
|
|
113
|
+
switch (name) {
|
|
114
|
+
case "bash": {
|
|
115
|
+
const cmd = args.command || "...";
|
|
116
|
+
return fg("muted", "$ ") + fg("toolOutput", cmd.length > 60 ? `${cmd.slice(0, 60)}...` : cmd);
|
|
117
|
+
}
|
|
118
|
+
case "read":
|
|
119
|
+
case "write":
|
|
120
|
+
case "edit":
|
|
121
|
+
return fg("muted", `${name} `) + fg("accent", args.file_path || args.path || "...");
|
|
122
|
+
case "grep":
|
|
123
|
+
return fg("muted", "grep ") + fg("accent", `/${args.pattern || ""}/`);
|
|
124
|
+
case "find":
|
|
125
|
+
return fg("muted", "find ") + fg("accent", args.pattern || "*");
|
|
126
|
+
default:
|
|
127
|
+
return fg("accent", name);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function formatUsage(usage, model) {
|
|
131
|
+
const parts = [];
|
|
132
|
+
if (usage.turns)
|
|
133
|
+
parts.push(`${usage.turns}t`);
|
|
134
|
+
if (usage.input)
|
|
135
|
+
parts.push(`\u2191${usage.input < 1e3 ? usage.input : `${Math.round(usage.input / 1e3)}k`}`);
|
|
136
|
+
if (usage.output)
|
|
137
|
+
parts.push(`\u2193${usage.output < 1e3 ? usage.output : `${Math.round(usage.output / 1e3)}k`}`);
|
|
138
|
+
if (usage.cost)
|
|
139
|
+
parts.push(`$${usage.cost.toFixed(4)}`);
|
|
140
|
+
if (model)
|
|
141
|
+
parts.push(model);
|
|
142
|
+
return parts.join(" ");
|
|
143
|
+
}
|
|
144
|
+
function stageIcon(stage, theme) {
|
|
145
|
+
return stage.status === "done" ? theme.fg("success", "\u2713") : stage.status === "running" ? theme.fg("warning", "\u27F3") : stage.status === "failed" ? theme.fg("error", "\u2717") : theme.fg("muted", "\u25CB");
|
|
146
|
+
}
|
|
147
|
+
function renderExpanded(details, theme, toolLabel) {
|
|
148
|
+
const container = new Container();
|
|
149
|
+
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${toolLabel} `)) + theme.fg("accent", details.pipeline), 0, 0));
|
|
150
|
+
container.addChild(new Spacer(1));
|
|
151
|
+
for (const stage of details.stages) {
|
|
152
|
+
const icon = stageIcon(stage, theme);
|
|
153
|
+
container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(stage.name))}`, 0, 0));
|
|
154
|
+
const items = getDisplayItems(stage.messages);
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
if (item.type === "toolCall") {
|
|
157
|
+
container.addChild(new Text(` ${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`, 0, 0));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const output = getFinalOutput(stage.messages);
|
|
161
|
+
if (output) {
|
|
162
|
+
container.addChild(new Spacer(1));
|
|
163
|
+
try {
|
|
164
|
+
const { getMarkdownTheme } = __require("@mariozechner/pi-coding-agent");
|
|
165
|
+
container.addChild(new Markdown(output.trim(), 0, 0, getMarkdownTheme()));
|
|
166
|
+
} catch {
|
|
167
|
+
container.addChild(new Text(theme.fg("toolOutput", output.slice(0, 500)), 0, 0));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const usageStr = formatUsage(stage.usage, stage.model);
|
|
171
|
+
if (usageStr)
|
|
172
|
+
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
173
|
+
container.addChild(new Spacer(1));
|
|
174
|
+
}
|
|
175
|
+
return container;
|
|
176
|
+
}
|
|
177
|
+
function renderResult(result2, expanded, theme, toolLabel) {
|
|
178
|
+
const details = result2.details;
|
|
179
|
+
if (!details || details.stages.length === 0) {
|
|
180
|
+
const text = result2.content[0];
|
|
181
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
182
|
+
}
|
|
183
|
+
return expanded ? renderExpanded(details, theme, toolLabel) : renderCollapsed(details, theme, toolLabel);
|
|
184
|
+
}
|
|
185
|
+
function renderCollapsed(details, theme, toolLabel) {
|
|
186
|
+
let text = theme.fg("toolTitle", theme.bold(`${toolLabel} `)) + theme.fg("accent", details.pipeline);
|
|
187
|
+
for (const stage of details.stages) {
|
|
188
|
+
const icon = stageIcon(stage, theme);
|
|
189
|
+
text += `
|
|
190
|
+
${icon} ${theme.fg("toolTitle", stage.name)}`;
|
|
191
|
+
if (stage.status === "running") {
|
|
192
|
+
const items = getDisplayItems(stage.messages);
|
|
193
|
+
const last = items.filter((i) => i.type === "toolCall").slice(-3);
|
|
194
|
+
for (const item of last) {
|
|
195
|
+
if (item.type === "toolCall") {
|
|
196
|
+
text += `
|
|
197
|
+
${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else if (stage.status === "done" || stage.status === "failed") {
|
|
201
|
+
const preview = stage.output.split("\n")[0]?.slice(0, 80) || "(no output)";
|
|
202
|
+
text += theme.fg("dim", ` ${preview}`);
|
|
203
|
+
const usageStr = formatUsage(stage.usage, stage.model);
|
|
204
|
+
if (usageStr)
|
|
205
|
+
text += ` ${theme.fg("dim", usageStr)}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return new Text(text, 0, 0);
|
|
209
|
+
}
|
|
210
|
+
|
|
69
211
|
// ../shared/dist/run-agent.js
|
|
212
|
+
import { spawn as spawn2 } from "child_process";
|
|
213
|
+
import * as fs from "fs";
|
|
214
|
+
import * as os from "os";
|
|
215
|
+
import * as path from "path";
|
|
216
|
+
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
|
217
|
+
async function resolveRunAgent(injected) {
|
|
218
|
+
if (injected)
|
|
219
|
+
return injected;
|
|
220
|
+
return runAgent;
|
|
221
|
+
}
|
|
70
222
|
function getPiInvocation(args) {
|
|
71
223
|
const currentScript = process.argv[1];
|
|
72
224
|
if (currentScript && fs.existsSync(currentScript)) {
|
|
@@ -213,9 +365,8 @@ function getLastToolCall(messages) {
|
|
|
213
365
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
214
366
|
const msg = messages[i];
|
|
215
367
|
if (msg.role === "assistant") {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const part = parts[j];
|
|
368
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
369
|
+
const part = msg.content[j];
|
|
219
370
|
if (part?.type === "toolCall") {
|
|
220
371
|
const name = part.name;
|
|
221
372
|
const args = part.arguments ?? {};
|
|
@@ -283,7 +434,7 @@ function cleanSignal(cwd, signal) {
|
|
|
283
434
|
}
|
|
284
435
|
|
|
285
436
|
// src/index.ts
|
|
286
|
-
import {
|
|
437
|
+
import { Text as Text2 } from "@mariozechner/pi-tui";
|
|
287
438
|
import { Type } from "@sinclair/typebox";
|
|
288
439
|
|
|
289
440
|
// src/resolve.ts
|
|
@@ -293,6 +444,22 @@ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
|
293
444
|
var AGENTS_DIR = path3.resolve(__dirname, "..", "agents");
|
|
294
445
|
|
|
295
446
|
// src/pipelines/architecture.ts
|
|
447
|
+
function parseCandidates(text) {
|
|
448
|
+
const pattern = /^(?:#{1,4}\s+)?(\d+)\.\s+(.+)$/gm;
|
|
449
|
+
const matches = [...text.matchAll(pattern)];
|
|
450
|
+
if (matches.length === 0) return [];
|
|
451
|
+
const results = [];
|
|
452
|
+
for (let i = 0; i < matches.length; i++) {
|
|
453
|
+
const match = matches[i];
|
|
454
|
+
const num = match[1];
|
|
455
|
+
const name = match[2].replace(/[*#]+/g, "").trim();
|
|
456
|
+
const start = match.index;
|
|
457
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
|
458
|
+
const body = text.slice(start, end).trim();
|
|
459
|
+
results.push({ label: `${num}. ${name}`, body });
|
|
460
|
+
}
|
|
461
|
+
return results;
|
|
462
|
+
}
|
|
296
463
|
async function runArchitecture(cwd, signal, onUpdate, ctx) {
|
|
297
464
|
const stages = [emptyStage("architecture-reviewer")];
|
|
298
465
|
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "architecture", onUpdate };
|
|
@@ -318,29 +485,52 @@ async function runArchitecture(cwd, signal, onUpdate, ctx) {
|
|
|
318
485
|
"Review architecture candidates (edit to highlight your pick)",
|
|
319
486
|
exploreResult.output
|
|
320
487
|
);
|
|
321
|
-
const
|
|
488
|
+
const candidateContext = edited ?? exploreResult.output;
|
|
489
|
+
const candidates = parseCandidates(candidateContext);
|
|
490
|
+
const selectOptions = candidates.length > 1 ? [...candidates.map((c) => c.label), "All candidates", "Skip"] : candidates.length === 1 ? [candidates[0].label, "Skip"] : ["Yes \u2014 generate RFC", "Skip"];
|
|
491
|
+
const action = await ctx.ui.select("Create RFC issues for which candidates?", selectOptions);
|
|
322
492
|
if (action === "Skip" || action == null) {
|
|
323
493
|
return {
|
|
324
494
|
content: [{ type: "text", text: "Architecture review complete. No RFC created." }],
|
|
325
495
|
details: { pipeline: "architecture", stages }
|
|
326
496
|
};
|
|
327
497
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
498
|
+
let selectedCandidates;
|
|
499
|
+
if (action === "All candidates") {
|
|
500
|
+
selectedCandidates = candidates;
|
|
501
|
+
} else if (action === "Yes \u2014 generate RFC") {
|
|
502
|
+
selectedCandidates = [{ label: "RFC", body: candidateContext }];
|
|
503
|
+
} else {
|
|
504
|
+
const match = candidates.find((c) => c.label === action);
|
|
505
|
+
selectedCandidates = match ? [match] : [{ label: "RFC", body: candidateContext }];
|
|
506
|
+
}
|
|
507
|
+
const createdIssues = [];
|
|
508
|
+
for (const candidate of selectedCandidates) {
|
|
509
|
+
const stageName = `architecture-rfc-${selectedCandidates.indexOf(candidate) + 1}`;
|
|
510
|
+
stages.push(emptyStage(stageName));
|
|
511
|
+
const rfcResult = await runAgent(
|
|
512
|
+
"architecture-reviewer",
|
|
513
|
+
`Based on the following architectural analysis, generate a detailed RFC and create a GitHub issue (with label "architecture") for this specific candidate.
|
|
333
514
|
|
|
334
|
-
|
|
335
|
-
${
|
|
336
|
-
{ ...opts, tools: TOOLS_READONLY }
|
|
337
|
-
);
|
|
338
|
-
const issueMatch = rfcResult.output?.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
|
|
339
|
-
const issueNum = issueMatch?.[1];
|
|
340
|
-
const issueUrl = issueMatch?.[0];
|
|
341
|
-
const summary = issueUrl ? `Architecture RFC issue created: ${issueUrl}
|
|
515
|
+
CANDIDATE:
|
|
516
|
+
${candidate.body}
|
|
342
517
|
|
|
343
|
-
|
|
518
|
+
FULL ANALYSIS (for context):
|
|
519
|
+
${candidateContext}`,
|
|
520
|
+
{ ...opts, stageName, tools: TOOLS_READONLY }
|
|
521
|
+
);
|
|
522
|
+
const issueMatch = rfcResult.output?.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
|
|
523
|
+
const issueUrl = issueMatch?.[0];
|
|
524
|
+
const issueNum = issueMatch?.[1];
|
|
525
|
+
if (issueUrl) {
|
|
526
|
+
createdIssues.push(`- ${issueUrl} \u2014 run \`/implement ${issueNum}\` to implement`);
|
|
527
|
+
} else {
|
|
528
|
+
createdIssues.push(`- ${candidate.label}: RFC created (no issue URL found in output)`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const summary = createdIssues.length === 1 ? `Architecture RFC issue created:
|
|
532
|
+
${createdIssues[0]}` : `Architecture RFC issues created (${createdIssues.length}):
|
|
533
|
+
${createdIssues.join("\n")}`;
|
|
344
534
|
return {
|
|
345
535
|
content: [{ type: "text", text: summary }],
|
|
346
536
|
details: { pipeline: "architecture", stages }
|
|
@@ -354,31 +544,91 @@ async function runDiscoverSkills(cwd, query, signal, onUpdate, _ctx) {
|
|
|
354
544
|
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "discover-skills", onUpdate };
|
|
355
545
|
const task = isInstall ? `Install these skills as forgeflow plugins: ${query}` : query ? `Discover skills related to "${query}" \u2014 recommend only, do NOT install.` : "Analyze the project tech stack and discover relevant skills \u2014 recommend only, do NOT install.";
|
|
356
546
|
const tools = isInstall ? TOOLS_ALL : TOOLS_NO_EDIT;
|
|
357
|
-
const
|
|
547
|
+
const result2 = await runAgent("skill-discoverer", task, { ...opts, tools });
|
|
358
548
|
return {
|
|
359
|
-
content: [{ type: "text", text:
|
|
549
|
+
content: [{ type: "text", text: result2.output || "No skills found." }],
|
|
360
550
|
details: { pipeline: "discover-skills", stages }
|
|
361
551
|
};
|
|
362
552
|
}
|
|
363
553
|
|
|
364
|
-
// src/utils/exec.ts
|
|
365
|
-
import { spawn as spawn3 } from "child_process";
|
|
366
|
-
function exec(cmd, cwd) {
|
|
367
|
-
return new Promise((resolve2) => {
|
|
368
|
-
const proc = spawn3("bash", ["-c", cmd], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
369
|
-
let out = "";
|
|
370
|
-
proc.stdout.on("data", (d) => {
|
|
371
|
-
out += d.toString();
|
|
372
|
-
});
|
|
373
|
-
proc.on("close", () => resolve2(out.trim()));
|
|
374
|
-
proc.on("error", () => resolve2(""));
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
554
|
// src/utils/git.ts
|
|
555
|
+
import * as fs4 from "fs";
|
|
556
|
+
import * as path5 from "path";
|
|
557
|
+
|
|
558
|
+
// src/utils/git-workflow.ts
|
|
379
559
|
import * as fs3 from "fs";
|
|
380
560
|
import * as os2 from "os";
|
|
381
561
|
import * as path4 from "path";
|
|
562
|
+
async function setupBranch(cwd, branch, execFn = exec) {
|
|
563
|
+
const aheadStr = await execFn(`git rev-list main..${branch} --count 2>/dev/null || echo 0`, cwd);
|
|
564
|
+
const ahead = parseInt(aheadStr, 10) || 0;
|
|
565
|
+
if (ahead > 0) {
|
|
566
|
+
await execFn(`git checkout ${branch}`, cwd);
|
|
567
|
+
const current2 = await execFn("git branch --show-current", cwd);
|
|
568
|
+
if (current2 !== branch) {
|
|
569
|
+
return { status: "failed", error: `Failed to switch to ${branch} (on ${current2})` };
|
|
570
|
+
}
|
|
571
|
+
return { status: "resumed", ahead };
|
|
572
|
+
}
|
|
573
|
+
await execFn(`git branch -D ${branch} 2>/dev/null; git branch -dr origin/${branch} 2>/dev/null; echo done`, cwd);
|
|
574
|
+
await execFn(`git checkout -b ${branch}`, cwd);
|
|
575
|
+
let current = await execFn("git branch --show-current", cwd);
|
|
576
|
+
if (current !== branch) {
|
|
577
|
+
await execFn(`git checkout ${branch} 2>/dev/null || git checkout -b ${branch}`, cwd);
|
|
578
|
+
current = await execFn("git branch --show-current", cwd);
|
|
579
|
+
}
|
|
580
|
+
if (current !== branch) {
|
|
581
|
+
return { status: "failed", error: `Failed to switch to ${branch} (on ${current})` };
|
|
582
|
+
}
|
|
583
|
+
return { status: "fresh" };
|
|
584
|
+
}
|
|
585
|
+
async function ensurePr(cwd, title, body, branch, execFn = exec) {
|
|
586
|
+
await execFn(`git push -u origin ${branch}`, cwd);
|
|
587
|
+
const existingNum = await findPrNumber(cwd, branch, execFn);
|
|
588
|
+
if (existingNum != null) {
|
|
589
|
+
return { number: existingNum, created: false };
|
|
590
|
+
}
|
|
591
|
+
const tmp = path4.join(os2.tmpdir(), `forgeflow-pr-${Date.now()}.md`);
|
|
592
|
+
try {
|
|
593
|
+
fs3.writeFileSync(tmp, body, "utf-8");
|
|
594
|
+
const createOutput = await execFn(`gh pr create --title "${title}" --body-file "${tmp}" --head ${branch}`, cwd);
|
|
595
|
+
const urlMatch = createOutput.match(/\/pull\/(\d+)/);
|
|
596
|
+
if (urlMatch?.[1]) {
|
|
597
|
+
return { number: parseInt(urlMatch[1], 10), created: true };
|
|
598
|
+
}
|
|
599
|
+
const createdNum = await findPrNumber(cwd, branch, execFn);
|
|
600
|
+
return { number: createdNum ?? 0, created: true };
|
|
601
|
+
} finally {
|
|
602
|
+
try {
|
|
603
|
+
fs3.unlinkSync(tmp);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function findPrNumber(cwd, branch, execFn = exec) {
|
|
609
|
+
const result2 = await execFn(`gh pr list --head "${branch}" --json number --jq '.[0].number'`, cwd);
|
|
610
|
+
if (result2 && result2 !== "null") {
|
|
611
|
+
return parseInt(result2, 10);
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
async function mergePr(cwd, prNumber, execFn = exec) {
|
|
616
|
+
const mergeResult = await execFn(`gh pr merge ${prNumber} --squash --delete-branch`, cwd);
|
|
617
|
+
if (mergeResult.includes("Merged") || mergeResult.includes("merged")) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const prState = await execFn(`gh pr view ${prNumber} --json state --jq '.state'`, cwd);
|
|
621
|
+
if (prState === "MERGED") {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
throw new Error(`Failed to merge PR #${prNumber}. State: ${prState || "unknown"}`);
|
|
625
|
+
}
|
|
626
|
+
async function returnToMain(cwd, execFn = exec) {
|
|
627
|
+
await execFn("git checkout main", cwd);
|
|
628
|
+
await execFn("git pull --rebase", cwd);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/utils/git.ts
|
|
382
632
|
var PR_TEMPLATE_PATHS = [
|
|
383
633
|
".github/pull_request_template.md",
|
|
384
634
|
".github/PULL_REQUEST_TEMPLATE.md",
|
|
@@ -390,9 +640,9 @@ function buildPrBody(cwd, issue) {
|
|
|
390
640
|
const isGitHub = issue.source === "github" && issue.number > 0;
|
|
391
641
|
const defaultBody = isGitHub ? `Closes #${issue.number}` : `Jira: ${issue.key}`;
|
|
392
642
|
for (const rel of PR_TEMPLATE_PATHS) {
|
|
393
|
-
const abs =
|
|
643
|
+
const abs = path5.join(cwd, rel);
|
|
394
644
|
try {
|
|
395
|
-
const template =
|
|
645
|
+
const template = fs4.readFileSync(abs, "utf-8");
|
|
396
646
|
const closeRef = isGitHub ? `Closes #${issue.number}` : `Jira: ${issue.key}`;
|
|
397
647
|
return `${closeRef}
|
|
398
648
|
|
|
@@ -402,18 +652,6 @@ ${template}`;
|
|
|
402
652
|
}
|
|
403
653
|
return defaultBody;
|
|
404
654
|
}
|
|
405
|
-
async function createPr(cwd, title, body, branch) {
|
|
406
|
-
const tmp = path4.join(os2.tmpdir(), `forgeflow-pr-${Date.now()}.md`);
|
|
407
|
-
try {
|
|
408
|
-
fs3.writeFileSync(tmp, body, "utf-8");
|
|
409
|
-
await exec(`gh pr create --title "${title}" --body-file "${tmp}" --head ${branch}`, cwd);
|
|
410
|
-
} finally {
|
|
411
|
-
try {
|
|
412
|
-
fs3.unlinkSync(tmp);
|
|
413
|
-
} catch {
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
655
|
function slugify(text, maxLen = 40) {
|
|
418
656
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, maxLen).replace(/-$/, "");
|
|
419
657
|
}
|
|
@@ -441,7 +679,7 @@ async function resolveIssue(cwd, issueArg) {
|
|
|
441
679
|
return `On branch "${branch}" \u2014 can't detect issue. Use /implement <issue#> or /implement <JIRA-KEY>.`;
|
|
442
680
|
}
|
|
443
681
|
async function resolveGitHubIssue(cwd, issueNum) {
|
|
444
|
-
const issueJson = await
|
|
682
|
+
const issueJson = await execSafe(`gh issue view ${issueNum} --json number,title,body`, cwd);
|
|
445
683
|
if (!issueJson) return `Could not fetch issue #${issueNum}.`;
|
|
446
684
|
let issue;
|
|
447
685
|
try {
|
|
@@ -450,12 +688,11 @@ async function resolveGitHubIssue(cwd, issueNum) {
|
|
|
450
688
|
return `Could not parse issue #${issueNum}.`;
|
|
451
689
|
}
|
|
452
690
|
const branch = `feat/issue-${issueNum}`;
|
|
453
|
-
const
|
|
454
|
-
const existingPR = prJson && prJson !== "null" ? parseInt(prJson, 10) : void 0;
|
|
691
|
+
const existingPR = await findPrNumber(cwd, branch) ?? void 0;
|
|
455
692
|
return { source: "github", key: String(issueNum), ...issue, branch, existingPR };
|
|
456
693
|
}
|
|
457
694
|
async function resolveJiraIssue(cwd, jiraKey, existingBranch) {
|
|
458
|
-
const raw = await
|
|
695
|
+
const raw = await execSafe(`jira issue view ${jiraKey} --raw`, cwd);
|
|
459
696
|
if (!raw) return `Could not fetch Jira issue ${jiraKey}.`;
|
|
460
697
|
let data;
|
|
461
698
|
try {
|
|
@@ -475,8 +712,7 @@ ${fields.acceptance_criteria}`);
|
|
|
475
712
|
if (fields.sprint?.name) bodyParts.push(`**Sprint:** ${fields.sprint.name}`);
|
|
476
713
|
const body = bodyParts.join("\n\n");
|
|
477
714
|
const branch = existingBranch ?? `feat/${jiraKey}-${slugify(title)}`;
|
|
478
|
-
const
|
|
479
|
-
const existingPR = prJson && prJson !== "null" ? parseInt(prJson, 10) : void 0;
|
|
715
|
+
const existingPR = await findPrNumber(cwd, branch) ?? void 0;
|
|
480
716
|
return { source: "jira", key: jiraKey, number: 0, title, body, branch, existingPR };
|
|
481
717
|
}
|
|
482
718
|
|
|
@@ -503,152 +739,60 @@ function updateProgressWidget(ctx, progress, totalCost) {
|
|
|
503
739
|
setForgeflowWidget(ctx, lines);
|
|
504
740
|
}
|
|
505
741
|
|
|
506
|
-
// src/pipelines/review.ts
|
|
507
|
-
async function
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline, onUpdate };
|
|
513
|
-
const extraInstructions = options.customPrompt ? `
|
|
742
|
+
// src/pipelines/review-orchestrator.ts
|
|
743
|
+
async function runReviewPipeline(diff, opts) {
|
|
744
|
+
const { cwd, signal, stages, pipeline = "review", onUpdate, customPrompt } = opts;
|
|
745
|
+
const runAgentFn = await resolveRunAgent(opts.runAgentFn);
|
|
746
|
+
const agentOpts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline, onUpdate };
|
|
747
|
+
const extraInstructions = customPrompt ? `
|
|
514
748
|
|
|
515
749
|
ADDITIONAL INSTRUCTIONS FROM USER:
|
|
516
|
-
${
|
|
750
|
+
${customPrompt}` : "";
|
|
517
751
|
cleanSignal(cwd, "findings");
|
|
518
752
|
stages.push(emptyStage("code-reviewer"));
|
|
519
|
-
await
|
|
753
|
+
await runAgentFn("code-reviewer", `Review the following diff:
|
|
520
754
|
|
|
521
755
|
${diff}${extraInstructions}`, {
|
|
522
|
-
...
|
|
756
|
+
...agentOpts,
|
|
523
757
|
tools: TOOLS_NO_EDIT
|
|
524
758
|
});
|
|
525
759
|
if (!signalExists(cwd, "findings")) {
|
|
526
|
-
return {
|
|
760
|
+
return { passed: true };
|
|
527
761
|
}
|
|
528
762
|
stages.push(emptyStage("review-judge"));
|
|
529
763
|
const findings = readSignal(cwd, "findings") ?? "";
|
|
530
|
-
await
|
|
764
|
+
await runAgentFn(
|
|
531
765
|
"review-judge",
|
|
532
766
|
`Validate the following code review findings against the actual code:
|
|
533
767
|
|
|
534
768
|
${findings}`,
|
|
535
|
-
{ ...
|
|
769
|
+
{ ...agentOpts, tools: TOOLS_NO_EDIT }
|
|
536
770
|
);
|
|
537
771
|
if (!signalExists(cwd, "findings")) {
|
|
538
|
-
return {
|
|
772
|
+
return { passed: true };
|
|
539
773
|
}
|
|
540
774
|
const validatedFindings = readSignal(cwd, "findings") ?? "";
|
|
541
|
-
|
|
542
|
-
const repo = await exec("gh repo view --json nameWithOwner --jq .nameWithOwner", cwd);
|
|
543
|
-
const prNum = options.prNumber;
|
|
544
|
-
const proposalPrompt = `You have validated code review findings for PR #${prNum} in ${repo}.
|
|
545
|
-
|
|
546
|
-
FINDINGS:
|
|
547
|
-
${validatedFindings}
|
|
548
|
-
|
|
549
|
-
Generate ready-to-run \`gh api\` commands to post each finding as a PR review comment. One command per finding.
|
|
550
|
-
|
|
551
|
-
Format each as:
|
|
552
|
-
|
|
553
|
-
**Finding N** \u2014 path/to/file.ts:LINE
|
|
554
|
-
|
|
555
|
-
\`\`\`bash
|
|
556
|
-
gh api repos/${repo}/pulls/${prNum}/comments \\
|
|
557
|
-
--method POST \\
|
|
558
|
-
--field body="<comment>" \\
|
|
559
|
-
--field commit_id="$(gh pr view ${prNum} --repo ${repo} --json headRefOid -q .headRefOid)" \\
|
|
560
|
-
--field path="path/to/file.ts" \\
|
|
561
|
-
--field line=LINE \\
|
|
562
|
-
--field side="RIGHT"
|
|
563
|
-
\`\`\`
|
|
564
|
-
|
|
565
|
-
Comment tone rules:
|
|
566
|
-
- Write like a teammate, not an auditor. Casual, brief, direct.
|
|
567
|
-
- 1-2 short sentences max. Lead with the suggestion, not the problem.
|
|
568
|
-
- Use "might be worth..." / "could we..." / "what about..." / "small thing:"
|
|
569
|
-
- No em dashes, no "Consider...", no "Note that...", no hedging filler.
|
|
570
|
-
- Use GitHub \`\`\`suggestion\`\`\` blocks when proposing code changes.
|
|
571
|
-
- Only generate commands for findings with a specific file + line.
|
|
572
|
-
|
|
573
|
-
After the comments, add the review decision command:
|
|
574
|
-
|
|
575
|
-
\`\`\`bash
|
|
576
|
-
gh pr review ${prNum} --request-changes --body "Left a few comments" --repo ${repo}
|
|
577
|
-
\`\`\`
|
|
578
|
-
|
|
579
|
-
Output ONLY the commands, no other text.`;
|
|
580
|
-
stages.push(emptyStage("propose-comments"));
|
|
581
|
-
await runAgent("review-judge", proposalPrompt, {
|
|
582
|
-
agentsDir: AGENTS_DIR,
|
|
583
|
-
cwd,
|
|
584
|
-
signal,
|
|
585
|
-
stages,
|
|
586
|
-
pipeline,
|
|
587
|
-
onUpdate,
|
|
588
|
-
tools: TOOLS_READONLY
|
|
589
|
-
});
|
|
590
|
-
const commentStage = stages.find((s) => s.name === "propose-comments");
|
|
591
|
-
const proposedCommands = commentStage?.output || "";
|
|
592
|
-
if (proposedCommands && ctx.hasUI) {
|
|
593
|
-
const reviewed = await ctx.ui.editor(
|
|
594
|
-
`Review PR comments for PR #${prNum} (edit or close to skip)`,
|
|
595
|
-
`${validatedFindings}
|
|
596
|
-
|
|
597
|
-
---
|
|
598
|
-
|
|
599
|
-
Proposed commands (run these to post):
|
|
600
|
-
|
|
601
|
-
${proposedCommands}`
|
|
602
|
-
);
|
|
603
|
-
if (reviewed != null) {
|
|
604
|
-
const action = await ctx.ui.select("Post these review comments?", ["Post comments", "Skip"]);
|
|
605
|
-
if (action === "Post comments") {
|
|
606
|
-
const commands = reviewed.match(/```bash\n([\s\S]*?)```/g) || [];
|
|
607
|
-
for (const block of commands) {
|
|
608
|
-
const cmd = block.replace(/```bash\n/, "").replace(/```$/, "").trim();
|
|
609
|
-
if (cmd.startsWith("gh ")) {
|
|
610
|
-
await exec(cmd, cwd);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
return { content: [{ type: "text", text: validatedFindings }], isError: true };
|
|
775
|
+
return { passed: false, findings: validatedFindings };
|
|
618
776
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const pr = await exec("gh pr view --json number --jq .number 2>/dev/null", cwd);
|
|
631
|
-
if (pr && pr !== "") prNumber = pr;
|
|
632
|
-
}
|
|
633
|
-
if (ctx.hasUI && !customPrompt) {
|
|
634
|
-
const extra = await ctx.ui.input("Additional instructions?", "Skip");
|
|
635
|
-
if (extra != null && extra.trim() !== "") {
|
|
636
|
-
customPrompt = extra.trim();
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
const result = await runReviewInline(cwd, signal, onUpdate, ctx, stages, diffCmd, "review", {
|
|
640
|
-
prNumber,
|
|
641
|
-
interactive: ctx.hasUI,
|
|
642
|
-
customPrompt
|
|
777
|
+
|
|
778
|
+
// src/pipelines/agents.ts
|
|
779
|
+
async function runImplementor(cwd, prompt, signal, stages, onUpdate) {
|
|
780
|
+
await runAgent("implementor", prompt, {
|
|
781
|
+
agentsDir: AGENTS_DIR,
|
|
782
|
+
cwd,
|
|
783
|
+
signal,
|
|
784
|
+
stages,
|
|
785
|
+
pipeline: "implement",
|
|
786
|
+
onUpdate,
|
|
787
|
+
tools: TOOLS_ALL
|
|
643
788
|
});
|
|
644
|
-
return { ...result, details: { pipeline: "review", stages } };
|
|
645
789
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
if (
|
|
651
|
-
const findings =
|
|
790
|
+
async function reviewAndFix(cwd, signal, onUpdate, _ctx, stages, pipeline = "implement") {
|
|
791
|
+
const diff = await exec("git diff main...HEAD", cwd);
|
|
792
|
+
if (!diff) return;
|
|
793
|
+
const reviewResult2 = await runReviewPipeline(diff, { cwd, signal, stages, pipeline, onUpdate });
|
|
794
|
+
if (!reviewResult2.passed) {
|
|
795
|
+
const findings = reviewResult2.findings ?? "";
|
|
652
796
|
stages.push(emptyStage("fix-findings"));
|
|
653
797
|
await runAgent(
|
|
654
798
|
"implementor",
|
|
@@ -676,21 +820,56 @@ async function refactorAndReview(cwd, signal, onUpdate, ctx, stages, skipReview,
|
|
|
676
820
|
await reviewAndFix(cwd, signal, onUpdate, ctx, stages, pipeline);
|
|
677
821
|
}
|
|
678
822
|
}
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
823
|
+
function buildImplementorPrompt(issueContext, plan, customPrompt, resolved, autonomous) {
|
|
824
|
+
const isGitHub = resolved.source === "github" && resolved.number > 0;
|
|
825
|
+
const planSection = plan ? `
|
|
826
|
+
|
|
827
|
+
IMPLEMENTATION PLAN:
|
|
828
|
+
${plan}` : "";
|
|
829
|
+
const customPromptSection = customPrompt ? `
|
|
830
|
+
|
|
831
|
+
ADDITIONAL INSTRUCTIONS FROM USER:
|
|
832
|
+
${customPrompt}` : "";
|
|
833
|
+
const branchNote = resolved.branch ? `
|
|
834
|
+
- You should be on branch: ${resolved.branch} \u2014 do NOT create or switch branches.` : "\n- Do NOT create or switch branches.";
|
|
835
|
+
const prNote = resolved.existingPR ? `
|
|
836
|
+
- PR #${resolved.existingPR} already exists for this branch.` : "";
|
|
837
|
+
const closeNote = isGitHub ? `
|
|
838
|
+
- The PR body MUST end with a blank line then 'Closes #${resolved.number}' on its own line (not inline with other text), so the issue auto-closes on merge.` : `
|
|
839
|
+
- The PR body should reference Jira issue ${resolved.key}.`;
|
|
840
|
+
const unresolvedNote = autonomous ? `
|
|
841
|
+
- If the plan has unresolved questions, resolve them yourself using sensible defaults. Do NOT stop and wait.` : "";
|
|
842
|
+
return `Implement the following issue using strict TDD (red-green-refactor).
|
|
843
|
+
|
|
844
|
+
${issueContext}${planSection}${customPromptSection}
|
|
845
|
+
|
|
846
|
+
WORKFLOW:
|
|
847
|
+
1. Read the codebase.
|
|
848
|
+
2. TDD${plan ? " following the plan" : ""}.
|
|
849
|
+
3. Refactor after all tests pass.
|
|
850
|
+
4. Run check command, fix failures.
|
|
851
|
+
5. Commit, push, and create a PR.
|
|
852
|
+
|
|
853
|
+
CONSTRAINTS:${branchNote}${prNote}${closeNote}${unresolvedNote}
|
|
854
|
+
- If blocked, write BLOCKED.md with the reason and stop.`;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/pipelines/planning.ts
|
|
858
|
+
async function resolveQuestions(plan, ctx) {
|
|
859
|
+
const sectionMatch = plan.match(/### Unresolved Questions\n([\s\S]*?)(?=\n###|$)/);
|
|
860
|
+
if (!sectionMatch) return plan;
|
|
861
|
+
const section = sectionMatch[1] ?? "";
|
|
862
|
+
const itemRe = /^(?:[-*]|\d+[.)]+|[a-z][.)]+)\s+(.+(?:\n(?!(?:[-*]|\d+[.)]+|[a-z][.)]+)\s).*)*)/gm;
|
|
863
|
+
const items = [];
|
|
864
|
+
for (const m of section.matchAll(itemRe)) {
|
|
865
|
+
if (m[0] && m[1]) items.push({ full: m[0], text: m[1] });
|
|
866
|
+
}
|
|
867
|
+
if (items.length === 0) return plan;
|
|
868
|
+
let updatedSection = section;
|
|
869
|
+
for (const item of items) {
|
|
870
|
+
const answer = await ctx.ui.input(`${item.text}`, "Skip to use defaults");
|
|
871
|
+
if (answer != null && answer.trim() !== "") {
|
|
872
|
+
updatedSection = updatedSection.replace(item.full, `${item.full}
|
|
694
873
|
**Answer:** ${answer.trim()}`);
|
|
695
874
|
}
|
|
696
875
|
}
|
|
@@ -698,186 +877,120 @@ async function resolveQuestions(plan, ctx) {
|
|
|
698
877
|
${section}`, `### Unresolved Questions
|
|
699
878
|
${updatedSection}`);
|
|
700
879
|
}
|
|
880
|
+
async function runPlanning(cwd, issueContext, customPrompt, opts) {
|
|
881
|
+
const { signal, onUpdate, ctx, interactive, stages } = opts;
|
|
882
|
+
const runAgentFn = await resolveRunAgent(opts.runAgentFn);
|
|
883
|
+
const customPromptSection = customPrompt ? `
|
|
884
|
+
|
|
885
|
+
ADDITIONAL INSTRUCTIONS FROM USER:
|
|
886
|
+
${customPrompt}` : "";
|
|
887
|
+
const planResult = await runAgentFn(
|
|
888
|
+
"planner",
|
|
889
|
+
`Plan the implementation for this issue by producing a sequenced list of test cases.
|
|
890
|
+
|
|
891
|
+
${issueContext}${customPromptSection}`,
|
|
892
|
+
{ agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "implement", onUpdate, tools: TOOLS_READONLY }
|
|
893
|
+
);
|
|
894
|
+
if (planResult.status === "failed") {
|
|
895
|
+
return { plan: planResult.output, cancelled: false, failed: true, stages };
|
|
896
|
+
}
|
|
897
|
+
let plan = planResult.output;
|
|
898
|
+
if (interactive && plan) {
|
|
899
|
+
const edited = await ctx.ui.editor("Review implementation plan", plan);
|
|
900
|
+
if (edited != null && edited !== plan) {
|
|
901
|
+
plan = edited;
|
|
902
|
+
}
|
|
903
|
+
plan = await resolveQuestions(plan, ctx);
|
|
904
|
+
const action = await ctx.ui.select("Plan ready. What next?", ["Approve and implement", "Cancel"]);
|
|
905
|
+
if (action === "Cancel" || action == null) {
|
|
906
|
+
return { plan, cancelled: true, stages };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return { plan, cancelled: false, stages };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/pipelines/implement.ts
|
|
913
|
+
function result(text, stages, isError) {
|
|
914
|
+
return {
|
|
915
|
+
content: [{ type: "text", text }],
|
|
916
|
+
details: { pipeline: "implement", stages },
|
|
917
|
+
...isError ? { isError } : {}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
701
920
|
async function runImplement(cwd, issueArg, signal, onUpdate, ctx, flags = {
|
|
702
921
|
skipPlan: false,
|
|
703
922
|
skipReview: false
|
|
704
923
|
}) {
|
|
705
924
|
const interactive = ctx.hasUI && !flags.autonomous;
|
|
706
925
|
const resolved = await resolveIssue(cwd, issueArg || void 0);
|
|
707
|
-
if (typeof resolved === "string")
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
const
|
|
711
|
-
const issueLabel = isGitHub ? `#${resolved.number}: ${resolved.title}` : `${resolved.key}: ${resolved.title}`;
|
|
712
|
-
if (!flags.autonomous && (resolved.number || resolved.key)) {
|
|
713
|
-
const tag = isGitHub ? `#${resolved.number}` : resolved.key;
|
|
714
|
-
setForgeflowStatus(ctx, `${tag} ${resolved.title} \xB7 ${resolved.branch}`);
|
|
715
|
-
}
|
|
716
|
-
const issueContext = isGitHub ? `Issue #${resolved.number}: ${resolved.title}
|
|
926
|
+
if (typeof resolved === "string") return result(resolved, []);
|
|
927
|
+
const isGH = resolved.source === "github" && resolved.number > 0;
|
|
928
|
+
const issueLabel = isGH ? `#${resolved.number}: ${resolved.title}` : `${resolved.key}: ${resolved.title}`;
|
|
929
|
+
const issueContext = isGH ? `Issue #${resolved.number}: ${resolved.title}
|
|
717
930
|
|
|
718
931
|
${resolved.body}` : `Jira ${resolved.key}: ${resolved.title}
|
|
719
932
|
|
|
720
933
|
${resolved.body}`;
|
|
934
|
+
if (!flags.autonomous && (resolved.number || resolved.key))
|
|
935
|
+
setForgeflowStatus(ctx, `${isGH ? `#${resolved.number}` : resolved.key} ${resolved.title} \xB7 ${resolved.branch}`);
|
|
721
936
|
if (interactive && !flags.customPrompt) {
|
|
722
937
|
const extra = await ctx.ui.input("Additional instructions?", "Skip");
|
|
723
|
-
if (extra
|
|
724
|
-
flags.customPrompt = extra.trim();
|
|
725
|
-
}
|
|
938
|
+
if (extra?.trim()) flags.customPrompt = extra.trim();
|
|
726
939
|
}
|
|
727
|
-
const customPromptSection = flags.customPrompt ? `
|
|
728
|
-
|
|
729
|
-
ADDITIONAL INSTRUCTIONS FROM USER:
|
|
730
|
-
${flags.customPrompt}` : "";
|
|
731
940
|
if (resolved.existingPR) {
|
|
732
941
|
const stages2 = [];
|
|
733
|
-
if (!flags.skipReview)
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
return {
|
|
737
|
-
content: [{ type: "text", text: `Resumed ${issueLabel} \u2014 PR #${resolved.existingPR} already exists.` }],
|
|
738
|
-
details: { pipeline: "implement", stages: stages2 }
|
|
739
|
-
};
|
|
942
|
+
if (!flags.skipReview) await reviewAndFix(cwd, signal, onUpdate, ctx, stages2);
|
|
943
|
+
return result(`Resumed ${issueLabel} \u2014 PR #${resolved.existingPR} already exists.`, stages2);
|
|
740
944
|
}
|
|
741
945
|
if (resolved.branch) {
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
cwd
|
|
746
|
-
);
|
|
747
|
-
if (branchSetup.startsWith("RESUME:")) {
|
|
748
|
-
await exec(`git push -u origin ${branch}`, cwd);
|
|
749
|
-
const prBody = buildPrBody(cwd, resolved);
|
|
750
|
-
await createPr(cwd, resolved.title, prBody, branch);
|
|
946
|
+
const branchResult = await setupBranch(cwd, resolved.branch);
|
|
947
|
+
if (branchResult.status === "resumed") {
|
|
948
|
+
await ensurePr(cwd, resolved.title, buildPrBody(cwd, resolved), resolved.branch);
|
|
751
949
|
const stages2 = [];
|
|
752
950
|
await refactorAndReview(cwd, signal, onUpdate, ctx, stages2, flags.skipReview);
|
|
753
|
-
return {
|
|
754
|
-
content: [{ type: "text", text: `Resumed ${issueLabel} \u2014 pushed existing commits and created PR.` }],
|
|
755
|
-
details: { pipeline: "implement", stages: stages2 }
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
let afterBranch = await exec("git branch --show-current", cwd);
|
|
759
|
-
if (afterBranch !== branch) {
|
|
760
|
-
await exec(`git checkout ${branch} 2>/dev/null || git checkout -b ${branch}`, cwd);
|
|
761
|
-
afterBranch = await exec("git branch --show-current", cwd);
|
|
762
|
-
}
|
|
763
|
-
if (afterBranch !== branch) {
|
|
764
|
-
return {
|
|
765
|
-
content: [
|
|
766
|
-
{
|
|
767
|
-
type: "text",
|
|
768
|
-
text: `Failed to switch to ${branch} (on ${afterBranch}). Setup output: ${branchSetup || "(empty)"}`
|
|
769
|
-
}
|
|
770
|
-
],
|
|
771
|
-
details: { pipeline: "implement", stages: [] },
|
|
772
|
-
isError: true
|
|
773
|
-
};
|
|
951
|
+
return result(`Resumed ${issueLabel} \u2014 pushed existing commits and created PR.`, stages2);
|
|
774
952
|
}
|
|
953
|
+
if (branchResult.status === "failed")
|
|
954
|
+
return result(branchResult.error || `Failed to switch to ${resolved.branch}.`, [], true);
|
|
775
955
|
}
|
|
776
|
-
const
|
|
777
|
-
if (!flags.skipPlan)
|
|
778
|
-
|
|
779
|
-
stageList.push(emptyStage("refactorer"));
|
|
780
|
-
const stages = stageList;
|
|
781
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "implement", onUpdate };
|
|
956
|
+
const stages = [];
|
|
957
|
+
if (!flags.skipPlan) stages.push(emptyStage("planner"));
|
|
958
|
+
stages.push(emptyStage("implementor"), emptyStage("refactorer"));
|
|
782
959
|
let plan = "";
|
|
783
960
|
if (!flags.skipPlan) {
|
|
784
|
-
const planResult = await
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
);
|
|
791
|
-
if (planResult.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
details: { pipeline: "implement", stages },
|
|
795
|
-
isError: true
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
plan = planResult.output;
|
|
799
|
-
if (interactive && plan) {
|
|
800
|
-
const edited = await ctx.ui.editor(`Review implementation plan for ${issueLabel}`, plan);
|
|
801
|
-
if (edited != null && edited !== plan) {
|
|
802
|
-
plan = edited;
|
|
803
|
-
}
|
|
804
|
-
plan = await resolveQuestions(plan, ctx);
|
|
805
|
-
const action = await ctx.ui.select("Plan ready. What next?", ["Approve and implement", "Cancel"]);
|
|
806
|
-
if (action === "Cancel" || action == null) {
|
|
807
|
-
return {
|
|
808
|
-
content: [{ type: "text", text: "Implementation cancelled." }],
|
|
809
|
-
details: { pipeline: "implement", stages }
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
}
|
|
961
|
+
const planResult = await runPlanning(cwd, issueContext, flags.customPrompt, {
|
|
962
|
+
signal,
|
|
963
|
+
onUpdate,
|
|
964
|
+
ctx,
|
|
965
|
+
interactive,
|
|
966
|
+
stages
|
|
967
|
+
});
|
|
968
|
+
if (planResult.failed) return result(`Planner failed: ${planResult.plan}`, stages, true);
|
|
969
|
+
if (planResult.cancelled) return result("Implementation cancelled.", stages);
|
|
970
|
+
plan = planResult.plan;
|
|
813
971
|
}
|
|
814
972
|
cleanSignal(cwd, "blocked");
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
- You should be on branch: ${resolved.branch} \u2014 do NOT create or switch branches.` : "\n- Do NOT create or switch branches.";
|
|
821
|
-
const prNote = resolved.existingPR ? `
|
|
822
|
-
- PR #${resolved.existingPR} already exists for this branch.` : "";
|
|
823
|
-
const closeNote = isGitHub ? `
|
|
824
|
-
- The PR body MUST end with a blank line then 'Closes #${resolved.number}' on its own line (not inline with other text), so the issue auto-closes on merge.` : `
|
|
825
|
-
- The PR body should reference Jira issue ${resolved.key}.`;
|
|
826
|
-
const unresolvedNote = flags.autonomous ? `
|
|
827
|
-
- If the plan has unresolved questions, resolve them yourself using sensible defaults. Do NOT stop and wait.` : "";
|
|
828
|
-
await runAgent(
|
|
829
|
-
"implementor",
|
|
830
|
-
`Implement the following issue using strict TDD (red-green-refactor).
|
|
831
|
-
|
|
832
|
-
${issueContext}${planSection}${customPromptSection}
|
|
833
|
-
|
|
834
|
-
WORKFLOW:
|
|
835
|
-
1. Read the codebase.
|
|
836
|
-
2. TDD${plan ? " following the plan" : ""}.
|
|
837
|
-
3. Refactor after all tests pass.
|
|
838
|
-
4. Run check command, fix failures.
|
|
839
|
-
5. Commit, push, and create a PR.
|
|
840
|
-
|
|
841
|
-
CONSTRAINTS:${branchNote}${prNote}${closeNote}${unresolvedNote}
|
|
842
|
-
- If blocked, write BLOCKED.md with the reason and stop.`,
|
|
843
|
-
{ ...opts, tools: TOOLS_ALL }
|
|
844
|
-
);
|
|
845
|
-
if (signalExists(cwd, "blocked")) {
|
|
846
|
-
const reason = readSignal(cwd, "blocked") ?? "";
|
|
847
|
-
return {
|
|
848
|
-
content: [{ type: "text", text: `Implementor blocked:
|
|
849
|
-
${reason}` }],
|
|
850
|
-
details: { pipeline: "implement", stages },
|
|
851
|
-
isError: true
|
|
852
|
-
};
|
|
853
|
-
}
|
|
973
|
+
const prompt = buildImplementorPrompt(issueContext, plan, flags.customPrompt, resolved, flags.autonomous);
|
|
974
|
+
await runImplementor(cwd, prompt, signal, stages, onUpdate);
|
|
975
|
+
if (signalExists(cwd, "blocked"))
|
|
976
|
+
return result(`Implementor blocked:
|
|
977
|
+
${readSignal(cwd, "blocked") ?? ""}`, stages, true);
|
|
854
978
|
await refactorAndReview(cwd, signal, onUpdate, ctx, stages, flags.skipReview);
|
|
855
|
-
let prNumber =
|
|
979
|
+
let prNumber = 0;
|
|
856
980
|
if (resolved.branch) {
|
|
857
|
-
await
|
|
858
|
-
prNumber =
|
|
859
|
-
if (!prNumber || prNumber === "null") {
|
|
860
|
-
const prBody = buildPrBody(cwd, resolved);
|
|
861
|
-
await createPr(cwd, resolved.title, prBody, resolved.branch);
|
|
862
|
-
prNumber = await exec(`gh pr list --head "${resolved.branch}" --json number --jq '.[0].number'`, cwd);
|
|
863
|
-
}
|
|
981
|
+
const prResult = await ensurePr(cwd, resolved.title, buildPrBody(cwd, resolved), resolved.branch);
|
|
982
|
+
prNumber = prResult.number;
|
|
864
983
|
}
|
|
865
|
-
if (!flags.autonomous && prNumber
|
|
984
|
+
if (!flags.autonomous && prNumber > 0) {
|
|
866
985
|
const mergeStage = emptyStage("merge");
|
|
867
986
|
stages.push(mergeStage);
|
|
868
|
-
await
|
|
869
|
-
await
|
|
987
|
+
await mergePr(cwd, prNumber);
|
|
988
|
+
await returnToMain(cwd);
|
|
870
989
|
mergeStage.status = "done";
|
|
871
990
|
mergeStage.output = `Merged PR #${prNumber}`;
|
|
872
|
-
onUpdate?.({
|
|
873
|
-
content: [{ type: "text", text: "Pipeline complete" }],
|
|
874
|
-
details: { pipeline: "implement", stages }
|
|
875
|
-
});
|
|
991
|
+
onUpdate?.({ content: [{ type: "text", text: "Pipeline complete" }], details: { pipeline: "implement", stages } });
|
|
876
992
|
}
|
|
877
|
-
return {
|
|
878
|
-
content: [{ type: "text", text: `Implementation of ${issueLabel} complete.` }],
|
|
879
|
-
details: { pipeline: "implement", stages }
|
|
880
|
-
};
|
|
993
|
+
return result(`Implementation of ${issueLabel} complete.`, stages);
|
|
881
994
|
}
|
|
882
995
|
|
|
883
996
|
// src/pipelines/implement-all.ts
|
|
@@ -903,7 +1016,7 @@ async function runImplementAll(cwd, signal, onUpdate, ctx, flags) {
|
|
|
903
1016
|
const maxIterations = 50;
|
|
904
1017
|
while (iteration++ < maxIterations) {
|
|
905
1018
|
if (signal.aborted) break;
|
|
906
|
-
await
|
|
1019
|
+
await returnToMain(cwd, exec);
|
|
907
1020
|
const issuesJson = await exec(
|
|
908
1021
|
`gh issue list --state open --label "auto-generated" --json number,title,body --jq 'sort_by(.number)'`,
|
|
909
1022
|
cwd
|
|
@@ -970,25 +1083,20 @@ async function runImplementAll(cwd, signal, onUpdate, ctx, flags) {
|
|
|
970
1083
|
};
|
|
971
1084
|
}
|
|
972
1085
|
const branch = `feat/issue-${issueNum}`;
|
|
973
|
-
await
|
|
974
|
-
const prNum = await
|
|
975
|
-
if (prNum
|
|
976
|
-
|
|
977
|
-
|
|
1086
|
+
await returnToMain(cwd, exec);
|
|
1087
|
+
const prNum = await findPrNumber(cwd, branch, exec);
|
|
1088
|
+
if (prNum != null) {
|
|
1089
|
+
try {
|
|
1090
|
+
await mergePr(cwd, prNum, exec);
|
|
978
1091
|
completed.add(issueNum);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
content: [{ type: "text", text: `Failed to merge PR #${prNum} for issue #${issueNum}.` }],
|
|
988
|
-
details: { pipeline: "implement-all", stages: allStages },
|
|
989
|
-
isError: true
|
|
990
|
-
};
|
|
991
|
-
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
issueProgress.set(issueNum, { title: issueTitle, status: "failed" });
|
|
1094
|
+
updateProgressWidget(ctx, issueProgress, sumUsage(allStages).cost);
|
|
1095
|
+
return {
|
|
1096
|
+
content: [{ type: "text", text: `Failed to merge PR #${prNum} for issue #${issueNum}.` }],
|
|
1097
|
+
details: { pipeline: "implement-all", stages: allStages },
|
|
1098
|
+
isError: true
|
|
1099
|
+
};
|
|
992
1100
|
}
|
|
993
1101
|
} else {
|
|
994
1102
|
issueProgress.set(issueNum, { title: issueTitle, status: "failed" });
|
|
@@ -1012,6 +1120,139 @@ async function runImplementAll(cwd, signal, onUpdate, ctx, flags) {
|
|
|
1012
1120
|
};
|
|
1013
1121
|
}
|
|
1014
1122
|
|
|
1123
|
+
// src/pipelines/review-comments.ts
|
|
1124
|
+
function buildCommentProposalPrompt(findings, prNum, repo) {
|
|
1125
|
+
return `You have validated code review findings for PR #${prNum} in ${repo}.
|
|
1126
|
+
|
|
1127
|
+
FINDINGS:
|
|
1128
|
+
${findings}
|
|
1129
|
+
|
|
1130
|
+
Generate ready-to-run \`gh api\` commands to post each finding as a PR review comment. One command per finding.
|
|
1131
|
+
|
|
1132
|
+
Format each as:
|
|
1133
|
+
|
|
1134
|
+
**Finding N** \u2014 path/to/file.ts:LINE
|
|
1135
|
+
|
|
1136
|
+
\`\`\`bash
|
|
1137
|
+
gh api repos/${repo}/pulls/${prNum}/comments \\
|
|
1138
|
+
--method POST \\
|
|
1139
|
+
--field body="<comment>" \\
|
|
1140
|
+
--field commit_id="$(gh pr view ${prNum} --repo ${repo} --json headRefOid -q .headRefOid)" \\
|
|
1141
|
+
--field path="path/to/file.ts" \\
|
|
1142
|
+
--field line=LINE \\
|
|
1143
|
+
--field side="RIGHT"
|
|
1144
|
+
\`\`\`
|
|
1145
|
+
|
|
1146
|
+
Comment tone rules:
|
|
1147
|
+
- Write like a teammate, not an auditor. Casual, brief, direct.
|
|
1148
|
+
- 1-2 short sentences max. Lead with the suggestion, not the problem.
|
|
1149
|
+
- Use "might be worth..." / "could we..." / "what about..." / "small thing:"
|
|
1150
|
+
- No em dashes, no "Consider...", no "Note that...", no hedging filler.
|
|
1151
|
+
- Use GitHub \`\`\`suggestion\`\`\` blocks when proposing code changes.
|
|
1152
|
+
- Only generate commands for findings with a specific file + line.
|
|
1153
|
+
|
|
1154
|
+
After the comments, add the review decision command:
|
|
1155
|
+
|
|
1156
|
+
\`\`\`bash
|
|
1157
|
+
gh pr review ${prNum} --request-changes --body "Left a few comments" --repo ${repo}
|
|
1158
|
+
\`\`\`
|
|
1159
|
+
|
|
1160
|
+
Output ONLY the commands, no other text.`;
|
|
1161
|
+
}
|
|
1162
|
+
function extractGhCommands(text) {
|
|
1163
|
+
const blocks = text.match(/```bash\n([\s\S]*?)```/g) || [];
|
|
1164
|
+
const commands = [];
|
|
1165
|
+
for (const block of blocks) {
|
|
1166
|
+
const cmd = block.replace(/```bash\n/, "").replace(/```$/, "").trim();
|
|
1167
|
+
if (cmd.startsWith("gh ")) {
|
|
1168
|
+
commands.push(cmd);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return commands;
|
|
1172
|
+
}
|
|
1173
|
+
async function proposeAndPostComments(findings, pr, opts) {
|
|
1174
|
+
const { cwd, signal, stages, ctx, pipeline = "review", onUpdate } = opts;
|
|
1175
|
+
const execFn = opts.execFn ?? exec;
|
|
1176
|
+
const runAgentFn = await resolveRunAgent(opts.runAgentFn);
|
|
1177
|
+
const proposalPrompt = buildCommentProposalPrompt(findings, pr.number, pr.repo);
|
|
1178
|
+
stages.push(emptyStage("propose-comments"));
|
|
1179
|
+
await runAgentFn("review-judge", proposalPrompt, {
|
|
1180
|
+
agentsDir: AGENTS_DIR,
|
|
1181
|
+
cwd,
|
|
1182
|
+
signal,
|
|
1183
|
+
stages,
|
|
1184
|
+
pipeline,
|
|
1185
|
+
onUpdate,
|
|
1186
|
+
tools: TOOLS_READONLY
|
|
1187
|
+
});
|
|
1188
|
+
const commentStage = stages.find((s) => s.name === "propose-comments");
|
|
1189
|
+
const proposedCommands = commentStage?.output || "";
|
|
1190
|
+
if (!proposedCommands || !ctx.hasUI) return;
|
|
1191
|
+
const reviewed = await ctx.ui.editor(
|
|
1192
|
+
`Review PR comments for PR #${pr.number} (edit or close to skip)`,
|
|
1193
|
+
`${findings}
|
|
1194
|
+
|
|
1195
|
+
---
|
|
1196
|
+
|
|
1197
|
+
Proposed commands (run these to post):
|
|
1198
|
+
|
|
1199
|
+
${proposedCommands}`
|
|
1200
|
+
);
|
|
1201
|
+
if (reviewed == null) return;
|
|
1202
|
+
const action = await ctx.ui.select("Post these review comments?", ["Post comments", "Skip"]);
|
|
1203
|
+
if (action !== "Post comments") return;
|
|
1204
|
+
const commands = extractGhCommands(reviewed);
|
|
1205
|
+
for (const cmd of commands) {
|
|
1206
|
+
await execFn(cmd, cwd);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/pipelines/review-diff.ts
|
|
1211
|
+
async function resolveDiffTarget(cwd, target, execFn = execSafe) {
|
|
1212
|
+
if (target.match(/^\d+$/)) {
|
|
1213
|
+
return { diffCmd: `gh pr diff ${target}`, prNumber: target };
|
|
1214
|
+
}
|
|
1215
|
+
if (target.startsWith("--branch")) {
|
|
1216
|
+
const branch = target.replace("--branch", "").trim() || "HEAD";
|
|
1217
|
+
return { diffCmd: `git diff main...${branch}` };
|
|
1218
|
+
}
|
|
1219
|
+
let prNumber;
|
|
1220
|
+
const pr = await execFn("gh pr view --json number --jq .number", cwd);
|
|
1221
|
+
if (pr && pr !== "") {
|
|
1222
|
+
prNumber = pr;
|
|
1223
|
+
}
|
|
1224
|
+
return { diffCmd: "git diff main...HEAD", prNumber };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/pipelines/review.ts
|
|
1228
|
+
var reviewResult = (text, stages, isError) => ({
|
|
1229
|
+
content: [{ type: "text", text }],
|
|
1230
|
+
...isError ? { isError } : {},
|
|
1231
|
+
details: { pipeline: "review", stages }
|
|
1232
|
+
});
|
|
1233
|
+
async function runReview(cwd, target, signal, onUpdate, ctx, customPrompt) {
|
|
1234
|
+
const stages = [];
|
|
1235
|
+
const { diffCmd, prNumber } = await resolveDiffTarget(cwd, target);
|
|
1236
|
+
if (ctx.hasUI && !customPrompt) {
|
|
1237
|
+
const extra = await ctx.ui.input("Additional instructions?", "Skip");
|
|
1238
|
+
if (extra?.trim()) customPrompt = extra.trim();
|
|
1239
|
+
}
|
|
1240
|
+
const diff = await exec(diffCmd, cwd);
|
|
1241
|
+
if (!diff) return reviewResult("No changes to review.", stages);
|
|
1242
|
+
const result2 = await runReviewPipeline(diff, { cwd, signal, stages, pipeline: "review", onUpdate, customPrompt });
|
|
1243
|
+
if (result2.passed) return reviewResult("Review passed \u2014 no actionable findings.", stages);
|
|
1244
|
+
const findings = result2.findings ?? "";
|
|
1245
|
+
if (ctx.hasUI && prNumber) {
|
|
1246
|
+
const repo = await exec("gh repo view --json nameWithOwner --jq .nameWithOwner", cwd);
|
|
1247
|
+
await proposeAndPostComments(
|
|
1248
|
+
findings,
|
|
1249
|
+
{ number: prNumber, repo },
|
|
1250
|
+
{ cwd, signal, stages, ctx, pipeline: "review", onUpdate }
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
return reviewResult(findings, stages, true);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1015
1256
|
// src/index.ts
|
|
1016
1257
|
function parseImplFlags(args) {
|
|
1017
1258
|
const skipPlan = args.includes("--skip-plan");
|
|
@@ -1042,47 +1283,6 @@ function parseReviewArgs(args) {
|
|
|
1042
1283
|
customPrompt: trimmed.slice(firstSpace + 1).trim().replace(/^"(.*)"$/, "$1")
|
|
1043
1284
|
};
|
|
1044
1285
|
}
|
|
1045
|
-
function getDisplayItems(messages) {
|
|
1046
|
-
const items = [];
|
|
1047
|
-
for (const msg of messages) {
|
|
1048
|
-
if (msg.role === "assistant") {
|
|
1049
|
-
for (const part of msg.content) {
|
|
1050
|
-
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
1051
|
-
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
return items;
|
|
1056
|
-
}
|
|
1057
|
-
function formatToolCallShort(name, args, fg) {
|
|
1058
|
-
switch (name) {
|
|
1059
|
-
case "bash": {
|
|
1060
|
-
const cmd = args.command || "...";
|
|
1061
|
-
return fg("muted", "$ ") + fg("toolOutput", cmd.length > 60 ? `${cmd.slice(0, 60)}...` : cmd);
|
|
1062
|
-
}
|
|
1063
|
-
case "read":
|
|
1064
|
-
return fg("muted", "read ") + fg("accent", args.file_path || args.path || "...");
|
|
1065
|
-
case "write":
|
|
1066
|
-
return fg("muted", "write ") + fg("accent", args.file_path || args.path || "...");
|
|
1067
|
-
case "edit":
|
|
1068
|
-
return fg("muted", "edit ") + fg("accent", args.file_path || args.path || "...");
|
|
1069
|
-
case "grep":
|
|
1070
|
-
return fg("muted", "grep ") + fg("accent", `/${args.pattern || ""}/`);
|
|
1071
|
-
case "find":
|
|
1072
|
-
return fg("muted", "find ") + fg("accent", args.pattern || "*");
|
|
1073
|
-
default:
|
|
1074
|
-
return fg("accent", name);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
function formatUsage(usage, model) {
|
|
1078
|
-
const parts = [];
|
|
1079
|
-
if (usage.turns) parts.push(`${usage.turns}t`);
|
|
1080
|
-
if (usage.input) parts.push(`\u2191${usage.input < 1e3 ? usage.input : `${Math.round(usage.input / 1e3)}k`}`);
|
|
1081
|
-
if (usage.output) parts.push(`\u2193${usage.output < 1e3 ? usage.output : `${Math.round(usage.output / 1e3)}k`}`);
|
|
1082
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
1083
|
-
if (model) parts.push(model);
|
|
1084
|
-
return parts.join(" ");
|
|
1085
|
-
}
|
|
1086
1286
|
var ForgeflowDevParams = Type.Object({
|
|
1087
1287
|
pipeline: Type.String({
|
|
1088
1288
|
description: 'Which pipeline to run: "implement", "implement-all", "review", "architecture", or "discover-skills"'
|
|
@@ -1161,85 +1361,13 @@ function registerForgeflowDevTool(pi) {
|
|
|
1161
1361
|
text += theme.fg("dim", `${prefix}${args.issue}`);
|
|
1162
1362
|
}
|
|
1163
1363
|
if (args.target) text += theme.fg("dim", ` ${args.target}`);
|
|
1164
|
-
return new
|
|
1364
|
+
return new Text2(text, 0, 0);
|
|
1165
1365
|
},
|
|
1166
|
-
renderResult(
|
|
1167
|
-
|
|
1168
|
-
if (!details || details.stages.length === 0) {
|
|
1169
|
-
const text = result.content[0];
|
|
1170
|
-
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1171
|
-
}
|
|
1172
|
-
if (expanded) {
|
|
1173
|
-
return renderExpanded(details, theme);
|
|
1174
|
-
}
|
|
1175
|
-
return renderCollapsed(details, theme);
|
|
1366
|
+
renderResult(result2, { expanded }, theme) {
|
|
1367
|
+
return renderResult(result2, expanded, theme, "forgeflow-dev");
|
|
1176
1368
|
}
|
|
1177
1369
|
});
|
|
1178
1370
|
}
|
|
1179
|
-
function renderExpanded(details, theme) {
|
|
1180
|
-
const container = new Container();
|
|
1181
|
-
container.addChild(
|
|
1182
|
-
new Text(theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", details.pipeline), 0, 0)
|
|
1183
|
-
);
|
|
1184
|
-
container.addChild(new Spacer(1));
|
|
1185
|
-
for (const stage of details.stages) {
|
|
1186
|
-
const icon = stageIcon(stage, theme);
|
|
1187
|
-
container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(stage.name))}`, 0, 0));
|
|
1188
|
-
const items = getDisplayItems(stage.messages);
|
|
1189
|
-
for (const item of items) {
|
|
1190
|
-
if (item.type === "toolCall") {
|
|
1191
|
-
container.addChild(
|
|
1192
|
-
new Text(
|
|
1193
|
-
` ${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`,
|
|
1194
|
-
0,
|
|
1195
|
-
0
|
|
1196
|
-
)
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
const output = getFinalOutput(stage.messages);
|
|
1201
|
-
if (output) {
|
|
1202
|
-
container.addChild(new Spacer(1));
|
|
1203
|
-
try {
|
|
1204
|
-
const { getMarkdownTheme } = __require("@mariozechner/pi-coding-agent");
|
|
1205
|
-
container.addChild(new Markdown(output.trim(), 0, 0, getMarkdownTheme()));
|
|
1206
|
-
} catch {
|
|
1207
|
-
container.addChild(new Text(theme.fg("toolOutput", output.slice(0, 500)), 0, 0));
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
const usageStr = formatUsage(stage.usage, stage.model);
|
|
1211
|
-
if (usageStr) container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
1212
|
-
container.addChild(new Spacer(1));
|
|
1213
|
-
}
|
|
1214
|
-
return container;
|
|
1215
|
-
}
|
|
1216
|
-
function renderCollapsed(details, theme) {
|
|
1217
|
-
let text = theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", details.pipeline);
|
|
1218
|
-
for (const stage of details.stages) {
|
|
1219
|
-
const icon = stageIcon(stage, theme);
|
|
1220
|
-
text += `
|
|
1221
|
-
${icon} ${theme.fg("toolTitle", stage.name)}`;
|
|
1222
|
-
if (stage.status === "running") {
|
|
1223
|
-
const items = getDisplayItems(stage.messages);
|
|
1224
|
-
const last = items.filter((i) => i.type === "toolCall").slice(-3);
|
|
1225
|
-
for (const item of last) {
|
|
1226
|
-
if (item.type === "toolCall") {
|
|
1227
|
-
text += `
|
|
1228
|
-
${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
} else if (stage.status === "done" || stage.status === "failed") {
|
|
1232
|
-
const preview = stage.output.split("\n")[0]?.slice(0, 80) || "(no output)";
|
|
1233
|
-
text += theme.fg("dim", ` ${preview}`);
|
|
1234
|
-
const usageStr = formatUsage(stage.usage, stage.model);
|
|
1235
|
-
if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
return new Text(text, 0, 0);
|
|
1239
|
-
}
|
|
1240
|
-
function stageIcon(stage, theme) {
|
|
1241
|
-
return stage.status === "done" ? theme.fg("success", "\u2713") : stage.status === "running" ? theme.fg("warning", "\u27F3") : stage.status === "failed" ? theme.fg("error", "\u2717") : theme.fg("muted", "\u25CB");
|
|
1242
|
-
}
|
|
1243
1371
|
var extension = (pi) => {
|
|
1244
1372
|
registerForgeflowDevTool(pi);
|
|
1245
1373
|
pi.registerCommand("implement", {
|