@callumvass/forgeflow-dev 0.1.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/agents/architecture-reviewer.md +67 -0
- package/agents/code-reviewer.md +44 -0
- package/agents/implementor.md +98 -0
- package/agents/planner.md +88 -0
- package/agents/refactorer.md +39 -0
- package/agents/review-judge.md +44 -0
- package/agents/skill-discoverer.md +110 -0
- package/extensions/index.js +1279 -0
- package/package.json +42 -0
- package/skills/code-review/SKILL.md +119 -0
- package/skills/plugins/SKILL.md +58 -0
- package/skills/stitch/SKILL.md +46 -0
- package/skills/tdd/SKILL.md +115 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +86 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +98 -0
- package/src/index.ts +380 -0
- package/src/pipelines/architecture.ts +67 -0
- package/src/pipelines/discover-skills.ts +33 -0
- package/src/pipelines/implement-all.ts +181 -0
- package/src/pipelines/implement.ts +305 -0
- package/src/pipelines/review.ts +183 -0
- package/src/resolve.ts +6 -0
- package/src/utils/exec.ts +13 -0
- package/src/utils/git.ts +132 -0
- package/src/utils/ui.ts +29 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// ../shared/dist/constants.js
|
|
9
|
+
var TOOLS_ALL = ["read", "write", "edit", "bash", "grep", "find"];
|
|
10
|
+
var TOOLS_READONLY = ["read", "bash", "grep", "find"];
|
|
11
|
+
var TOOLS_NO_EDIT = ["read", "write", "bash", "grep", "find"];
|
|
12
|
+
var SIGNALS = {
|
|
13
|
+
questions: "QUESTIONS.md",
|
|
14
|
+
findings: "FINDINGS.md",
|
|
15
|
+
blocked: "BLOCKED.md"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ../shared/dist/run-agent.js
|
|
19
|
+
import { spawn } from "child_process";
|
|
20
|
+
import * as fs from "fs";
|
|
21
|
+
import * as os from "os";
|
|
22
|
+
import * as path from "path";
|
|
23
|
+
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
|
24
|
+
|
|
25
|
+
// ../shared/dist/types.js
|
|
26
|
+
function emptyUsage() {
|
|
27
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
28
|
+
}
|
|
29
|
+
function emptyStage(name) {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
status: "pending",
|
|
33
|
+
messages: [],
|
|
34
|
+
exitCode: -1,
|
|
35
|
+
stderr: "",
|
|
36
|
+
output: "",
|
|
37
|
+
usage: emptyUsage()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function getFinalOutput(messages) {
|
|
41
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
42
|
+
const msg = messages[i];
|
|
43
|
+
if (msg.role === "assistant") {
|
|
44
|
+
for (const part of msg.content) {
|
|
45
|
+
if (typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
|
|
46
|
+
return part.text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
function sumUsage(stages) {
|
|
54
|
+
const total = emptyUsage();
|
|
55
|
+
for (const s of stages) {
|
|
56
|
+
total.input += s.usage.input;
|
|
57
|
+
total.output += s.usage.output;
|
|
58
|
+
total.cacheRead += s.usage.cacheRead;
|
|
59
|
+
total.cacheWrite += s.usage.cacheWrite;
|
|
60
|
+
total.cost += s.usage.cost;
|
|
61
|
+
total.turns += s.usage.turns;
|
|
62
|
+
}
|
|
63
|
+
return total;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ../shared/dist/run-agent.js
|
|
67
|
+
function getPiInvocation(args) {
|
|
68
|
+
const currentScript = process.argv[1];
|
|
69
|
+
if (currentScript && fs.existsSync(currentScript)) {
|
|
70
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
71
|
+
}
|
|
72
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
73
|
+
if (!/^(node|bun)(\.exe)?$/.test(execName)) {
|
|
74
|
+
return { command: process.execPath, args };
|
|
75
|
+
}
|
|
76
|
+
return { command: "pi", args };
|
|
77
|
+
}
|
|
78
|
+
async function writePromptToTempFile(name, prompt) {
|
|
79
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "forgeflow-"));
|
|
80
|
+
const filePath = path.join(tmpDir, `prompt-${name}.md`);
|
|
81
|
+
await withFileMutationQueue(filePath, async () => {
|
|
82
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 384 });
|
|
83
|
+
});
|
|
84
|
+
return { dir: tmpDir, filePath };
|
|
85
|
+
}
|
|
86
|
+
async function runAgent(agentName, task, options) {
|
|
87
|
+
const agentPath = path.join(options.agentsDir, `${agentName}.md`);
|
|
88
|
+
const lookupName = options.stageName ?? agentName;
|
|
89
|
+
const stage = options.stages.find((s) => s.name === lookupName && s.status === "pending") ?? options.stages.find((s) => s.name === lookupName);
|
|
90
|
+
if (!stage) {
|
|
91
|
+
const s = emptyStage(agentName);
|
|
92
|
+
s.status = "failed";
|
|
93
|
+
s.output = "Stage not found in pipeline";
|
|
94
|
+
return s;
|
|
95
|
+
}
|
|
96
|
+
stage.status = "running";
|
|
97
|
+
emitUpdate(options);
|
|
98
|
+
const tools = options.tools ?? ["read", "write", "edit", "bash", "grep", "find"];
|
|
99
|
+
const args = ["--mode", "json", "-p", "--no-session", "--tools", tools.join(",")];
|
|
100
|
+
let tmpDir = null;
|
|
101
|
+
let tmpFile = null;
|
|
102
|
+
try {
|
|
103
|
+
const systemPrompt = fs.readFileSync(agentPath, "utf-8");
|
|
104
|
+
const tmp = await writePromptToTempFile(agentName, systemPrompt);
|
|
105
|
+
tmpDir = tmp.dir;
|
|
106
|
+
tmpFile = tmp.filePath;
|
|
107
|
+
args.push("--append-system-prompt", tmpFile);
|
|
108
|
+
args.push(`Task: ${task}`);
|
|
109
|
+
const exitCode = await new Promise((resolve2) => {
|
|
110
|
+
const invocation = getPiInvocation(args);
|
|
111
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
112
|
+
cwd: options.cwd,
|
|
113
|
+
shell: false,
|
|
114
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
115
|
+
});
|
|
116
|
+
let buffer = "";
|
|
117
|
+
const processLine = (line) => {
|
|
118
|
+
if (!line.trim())
|
|
119
|
+
return;
|
|
120
|
+
let event;
|
|
121
|
+
try {
|
|
122
|
+
event = JSON.parse(line);
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (event.type === "message_end" && event.message) {
|
|
127
|
+
const msg = event.message;
|
|
128
|
+
stage.messages.push(msg);
|
|
129
|
+
if (msg.role === "assistant") {
|
|
130
|
+
stage.usage.turns++;
|
|
131
|
+
const usage = msg.usage;
|
|
132
|
+
if (usage) {
|
|
133
|
+
stage.usage.input += usage.input || 0;
|
|
134
|
+
stage.usage.output += usage.output || 0;
|
|
135
|
+
stage.usage.cacheRead += usage.cacheRead || 0;
|
|
136
|
+
stage.usage.cacheWrite += usage.cacheWrite || 0;
|
|
137
|
+
stage.usage.cost += usage.cost?.total || 0;
|
|
138
|
+
}
|
|
139
|
+
if (!stage.model && msg.model)
|
|
140
|
+
stage.model = msg.model;
|
|
141
|
+
}
|
|
142
|
+
emitUpdate(options);
|
|
143
|
+
}
|
|
144
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
145
|
+
stage.messages.push(event.message);
|
|
146
|
+
emitUpdate(options);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
proc.stdout.on("data", (data) => {
|
|
150
|
+
buffer += data.toString();
|
|
151
|
+
const lines = buffer.split("\n");
|
|
152
|
+
buffer = lines.pop() || "";
|
|
153
|
+
for (const line of lines)
|
|
154
|
+
processLine(line);
|
|
155
|
+
});
|
|
156
|
+
proc.stderr.on("data", (data) => {
|
|
157
|
+
stage.stderr += data.toString();
|
|
158
|
+
});
|
|
159
|
+
proc.on("close", (code) => {
|
|
160
|
+
if (buffer.trim())
|
|
161
|
+
processLine(buffer);
|
|
162
|
+
resolve2(code ?? 0);
|
|
163
|
+
});
|
|
164
|
+
proc.on("error", () => resolve2(1));
|
|
165
|
+
if (options.signal) {
|
|
166
|
+
const kill = () => {
|
|
167
|
+
proc.kill("SIGTERM");
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
if (!proc.killed)
|
|
170
|
+
proc.kill("SIGKILL");
|
|
171
|
+
}, 5e3);
|
|
172
|
+
};
|
|
173
|
+
if (options.signal.aborted)
|
|
174
|
+
kill();
|
|
175
|
+
else
|
|
176
|
+
options.signal.addEventListener("abort", kill, { once: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
stage.exitCode = exitCode;
|
|
180
|
+
stage.status = exitCode === 0 ? "done" : "failed";
|
|
181
|
+
for (let i = stage.messages.length - 1; i >= 0; i--) {
|
|
182
|
+
const msg = stage.messages[i];
|
|
183
|
+
if (msg.role === "assistant") {
|
|
184
|
+
for (const part of msg.content) {
|
|
185
|
+
if (typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
|
|
186
|
+
stage.output = part.text;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (stage.output)
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
emitUpdate(options);
|
|
195
|
+
return stage;
|
|
196
|
+
} finally {
|
|
197
|
+
if (tmpFile)
|
|
198
|
+
try {
|
|
199
|
+
fs.unlinkSync(tmpFile);
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
if (tmpDir)
|
|
203
|
+
try {
|
|
204
|
+
fs.rmdirSync(tmpDir);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function getLastToolCall(messages) {
|
|
210
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
211
|
+
const msg = messages[i];
|
|
212
|
+
if (msg.role === "assistant") {
|
|
213
|
+
const parts = msg.content;
|
|
214
|
+
for (let j = parts.length - 1; j >= 0; j--) {
|
|
215
|
+
const part = parts[j];
|
|
216
|
+
if (part?.type === "toolCall") {
|
|
217
|
+
const name = part.name;
|
|
218
|
+
const args = part.arguments ?? {};
|
|
219
|
+
switch (name) {
|
|
220
|
+
case "bash": {
|
|
221
|
+
const cmd = (args.command || "").slice(0, 60);
|
|
222
|
+
return cmd ? `$ ${cmd}` : name;
|
|
223
|
+
}
|
|
224
|
+
case "read":
|
|
225
|
+
case "write":
|
|
226
|
+
case "edit":
|
|
227
|
+
return `${name} ${args.file_path ?? args.path ?? ""}`;
|
|
228
|
+
case "grep":
|
|
229
|
+
return `grep /${args.pattern ?? ""}/`;
|
|
230
|
+
case "find":
|
|
231
|
+
return `find ${args.pattern ?? ""}`;
|
|
232
|
+
default:
|
|
233
|
+
return name;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return "";
|
|
240
|
+
}
|
|
241
|
+
function emitUpdate(options) {
|
|
242
|
+
if (!options.onUpdate)
|
|
243
|
+
return;
|
|
244
|
+
const running = options.stages.find((s) => s.status === "running");
|
|
245
|
+
let text;
|
|
246
|
+
if (running) {
|
|
247
|
+
const lastTool = getLastToolCall(running.messages);
|
|
248
|
+
text = lastTool ? `[${running.name}] ${lastTool}` : `[${running.name}] running...`;
|
|
249
|
+
} else {
|
|
250
|
+
text = options.stages.every((s) => s.status === "done") ? "Pipeline complete" : "Processing...";
|
|
251
|
+
}
|
|
252
|
+
options.onUpdate({
|
|
253
|
+
content: [{ type: "text", text }],
|
|
254
|
+
details: { pipeline: options.pipeline, stages: options.stages }
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ../shared/dist/signals.js
|
|
259
|
+
import * as fs2 from "fs";
|
|
260
|
+
import * as path2 from "path";
|
|
261
|
+
function signalPath(cwd, signal) {
|
|
262
|
+
return path2.join(cwd, SIGNALS[signal]);
|
|
263
|
+
}
|
|
264
|
+
function signalExists(cwd, signal) {
|
|
265
|
+
return fs2.existsSync(signalPath(cwd, signal));
|
|
266
|
+
}
|
|
267
|
+
function readSignal(cwd, signal) {
|
|
268
|
+
const p = signalPath(cwd, signal);
|
|
269
|
+
try {
|
|
270
|
+
return fs2.readFileSync(p, "utf-8");
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function cleanSignal(cwd, signal) {
|
|
276
|
+
try {
|
|
277
|
+
fs2.unlinkSync(signalPath(cwd, signal));
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/index.ts
|
|
283
|
+
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
284
|
+
import { Type } from "@sinclair/typebox";
|
|
285
|
+
|
|
286
|
+
// src/resolve.ts
|
|
287
|
+
import * as path3 from "path";
|
|
288
|
+
import { fileURLToPath } from "url";
|
|
289
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
290
|
+
var AGENTS_DIR = path3.resolve(__dirname, "..", "agents");
|
|
291
|
+
|
|
292
|
+
// src/pipelines/architecture.ts
|
|
293
|
+
async function runArchitecture(cwd, signal, onUpdate, ctx) {
|
|
294
|
+
const stages = [emptyStage("architecture-reviewer")];
|
|
295
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "architecture", onUpdate };
|
|
296
|
+
const exploreResult = await runAgent(
|
|
297
|
+
"architecture-reviewer",
|
|
298
|
+
"Explore this codebase and identify architectural friction. Present numbered candidates ranked by severity.",
|
|
299
|
+
{ ...opts, tools: TOOLS_READONLY }
|
|
300
|
+
);
|
|
301
|
+
if (exploreResult.status === "failed") {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: "text", text: `Exploration failed: ${exploreResult.output}` }],
|
|
304
|
+
details: { pipeline: "architecture", stages },
|
|
305
|
+
isError: true
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (!ctx.hasUI) {
|
|
309
|
+
return {
|
|
310
|
+
content: [{ type: "text", text: exploreResult.output }],
|
|
311
|
+
details: { pipeline: "architecture", stages }
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const edited = await ctx.ui.editor(
|
|
315
|
+
"Review architecture candidates (edit to highlight your pick)",
|
|
316
|
+
exploreResult.output
|
|
317
|
+
);
|
|
318
|
+
const action = await ctx.ui.select("Create RFC issue for a candidate?", ["Yes \u2014 generate RFC", "Skip"]);
|
|
319
|
+
if (action === "Skip" || action == null) {
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: "text", text: "Architecture review complete. No RFC created." }],
|
|
322
|
+
details: { pipeline: "architecture", stages }
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
stages.push(emptyStage("architecture-rfc"));
|
|
326
|
+
const candidateContext = edited ?? exploreResult.output;
|
|
327
|
+
const rfcResult = await runAgent(
|
|
328
|
+
"architecture-reviewer",
|
|
329
|
+
`Based on the following architectural analysis, generate a detailed RFC and create a GitHub issue (with label "architecture") for the highest-priority candidate \u2014 or the one the user highlighted/edited.
|
|
330
|
+
|
|
331
|
+
ANALYSIS:
|
|
332
|
+
${candidateContext}`,
|
|
333
|
+
{ ...opts, tools: TOOLS_READONLY }
|
|
334
|
+
);
|
|
335
|
+
const issueMatch = rfcResult.output?.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
|
|
336
|
+
const issueNum = issueMatch?.[1];
|
|
337
|
+
const issueUrl = issueMatch?.[0];
|
|
338
|
+
const summary = issueUrl ? `Architecture RFC issue created: ${issueUrl}
|
|
339
|
+
|
|
340
|
+
Run \`/implement ${issueNum}\` to implement it.` : "Architecture RFC issue created.";
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: summary }],
|
|
343
|
+
details: { pipeline: "architecture", stages }
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/pipelines/discover-skills.ts
|
|
348
|
+
async function runDiscoverSkills(cwd, query, signal, onUpdate, _ctx) {
|
|
349
|
+
const isInstall = query.includes(",") || query.includes("/");
|
|
350
|
+
const stages = [emptyStage("skill-discoverer")];
|
|
351
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "discover-skills", onUpdate };
|
|
352
|
+
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.";
|
|
353
|
+
const tools = isInstall ? TOOLS_ALL : TOOLS_NO_EDIT;
|
|
354
|
+
const result = await runAgent("skill-discoverer", task, { ...opts, tools });
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: "text", text: result.output || "No skills found." }],
|
|
357
|
+
details: { pipeline: "discover-skills", stages }
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/utils/exec.ts
|
|
362
|
+
import { spawn as spawn2 } from "child_process";
|
|
363
|
+
function exec(cmd, cwd) {
|
|
364
|
+
return new Promise((resolve2) => {
|
|
365
|
+
const proc = spawn2("bash", ["-c", cmd], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
366
|
+
let out = "";
|
|
367
|
+
proc.stdout.on("data", (d) => {
|
|
368
|
+
out += d.toString();
|
|
369
|
+
});
|
|
370
|
+
proc.on("close", () => resolve2(out.trim()));
|
|
371
|
+
proc.on("error", () => resolve2(""));
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/utils/git.ts
|
|
376
|
+
function slugify(text, maxLen = 40) {
|
|
377
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, maxLen).replace(/-$/, "");
|
|
378
|
+
}
|
|
379
|
+
var JIRA_KEY_RE = /^[A-Z]+-\d+$/;
|
|
380
|
+
var JIRA_BRANCH_RE = /feat\/([A-Z]+-\d+)/;
|
|
381
|
+
async function ensureBranch(cwd, branch) {
|
|
382
|
+
const currentBranch = await exec("git branch --show-current", cwd);
|
|
383
|
+
if (currentBranch === branch) return;
|
|
384
|
+
const exists = await exec(`git rev-parse --verify ${branch} 2>/dev/null && echo yes || echo no`, cwd);
|
|
385
|
+
if (exists === "yes") {
|
|
386
|
+
await exec(`git checkout ${branch}`, cwd);
|
|
387
|
+
} else {
|
|
388
|
+
await exec(`git checkout -b ${branch}`, cwd);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function resolveIssue(cwd, issueArg) {
|
|
392
|
+
if (issueArg && JIRA_KEY_RE.test(issueArg)) {
|
|
393
|
+
return resolveJiraIssue(cwd, issueArg);
|
|
394
|
+
}
|
|
395
|
+
if (issueArg && /^\d+$/.test(issueArg)) {
|
|
396
|
+
return resolveGitHubIssue(cwd, parseInt(issueArg, 10));
|
|
397
|
+
}
|
|
398
|
+
if (issueArg) {
|
|
399
|
+
return { source: "github", key: "", number: 0, title: issueArg, body: issueArg, branch: "" };
|
|
400
|
+
}
|
|
401
|
+
const branch = await exec("git branch --show-current", cwd);
|
|
402
|
+
const jiraMatch = branch.match(JIRA_BRANCH_RE);
|
|
403
|
+
if (jiraMatch) {
|
|
404
|
+
return resolveJiraIssue(cwd, jiraMatch[1], branch);
|
|
405
|
+
}
|
|
406
|
+
const ghMatch = branch.match(/(?:feat\/)?issue-(\d+)/);
|
|
407
|
+
if (ghMatch) {
|
|
408
|
+
return resolveGitHubIssue(cwd, parseInt(ghMatch[1], 10));
|
|
409
|
+
}
|
|
410
|
+
return `On branch "${branch}" \u2014 can't detect issue. Use /implement <issue#> or /implement <JIRA-KEY>.`;
|
|
411
|
+
}
|
|
412
|
+
async function resolveGitHubIssue(cwd, issueNum) {
|
|
413
|
+
const issueJson = await exec(`gh issue view ${issueNum} --json number,title,body`, cwd);
|
|
414
|
+
if (!issueJson) return `Could not fetch issue #${issueNum}.`;
|
|
415
|
+
let issue;
|
|
416
|
+
try {
|
|
417
|
+
issue = JSON.parse(issueJson);
|
|
418
|
+
} catch {
|
|
419
|
+
return `Could not parse issue #${issueNum}.`;
|
|
420
|
+
}
|
|
421
|
+
const branch = `feat/issue-${issueNum}`;
|
|
422
|
+
const prJson = await exec(`gh pr list --head "${branch}" --json number --jq '.[0].number'`, cwd);
|
|
423
|
+
const existingPR = prJson && prJson !== "null" ? parseInt(prJson, 10) : void 0;
|
|
424
|
+
return { source: "github", key: String(issueNum), ...issue, branch, existingPR };
|
|
425
|
+
}
|
|
426
|
+
async function resolveJiraIssue(cwd, jiraKey, existingBranch) {
|
|
427
|
+
const raw = await exec(`jira issue view ${jiraKey} --raw`, cwd);
|
|
428
|
+
if (!raw) return `Could not fetch Jira issue ${jiraKey}.`;
|
|
429
|
+
let data;
|
|
430
|
+
try {
|
|
431
|
+
data = JSON.parse(raw);
|
|
432
|
+
} catch {
|
|
433
|
+
return `Could not parse Jira issue ${jiraKey}.`;
|
|
434
|
+
}
|
|
435
|
+
const fields = data.fields ?? {};
|
|
436
|
+
const title = fields.summary ?? jiraKey;
|
|
437
|
+
const bodyParts = [];
|
|
438
|
+
if (fields.description) bodyParts.push(fields.description);
|
|
439
|
+
if (fields.acceptance_criteria) bodyParts.push(`## Acceptance Criteria
|
|
440
|
+
${fields.acceptance_criteria}`);
|
|
441
|
+
if (fields.status?.name) bodyParts.push(`**Status:** ${fields.status.name}`);
|
|
442
|
+
if (fields.priority?.name) bodyParts.push(`**Priority:** ${fields.priority.name}`);
|
|
443
|
+
if (fields.story_points != null) bodyParts.push(`**Story Points:** ${fields.story_points}`);
|
|
444
|
+
if (fields.sprint?.name) bodyParts.push(`**Sprint:** ${fields.sprint.name}`);
|
|
445
|
+
const body = bodyParts.join("\n\n");
|
|
446
|
+
const branch = existingBranch ?? `feat/${jiraKey}-${slugify(title)}`;
|
|
447
|
+
const prJson = await exec(`gh pr list --head "${branch}" --json number --jq '.[0].number'`, cwd);
|
|
448
|
+
const existingPR = prJson && prJson !== "null" ? parseInt(prJson, 10) : void 0;
|
|
449
|
+
return { source: "jira", key: jiraKey, number: 0, title, body, branch, existingPR };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/utils/ui.ts
|
|
453
|
+
function setForgeflowStatus(ctx, text) {
|
|
454
|
+
if (ctx.hasUI) ctx.ui.setStatus("forgeflow-dev", text);
|
|
455
|
+
}
|
|
456
|
+
function setForgeflowWidget(ctx, lines) {
|
|
457
|
+
if (ctx.hasUI) ctx.ui.setWidget("forgeflow-dev", lines);
|
|
458
|
+
}
|
|
459
|
+
function updateProgressWidget(ctx, progress, totalCost) {
|
|
460
|
+
let done = 0;
|
|
461
|
+
for (const [, info] of progress) {
|
|
462
|
+
if (info.status === "done") done++;
|
|
463
|
+
}
|
|
464
|
+
let header = `implement-all \xB7 ${done}/${progress.size}`;
|
|
465
|
+
if (totalCost > 0) header += ` \xB7 $${totalCost.toFixed(2)}`;
|
|
466
|
+
const lines = [header];
|
|
467
|
+
for (const [num, info] of progress) {
|
|
468
|
+
const icon = info.status === "done" ? "\u2713" : info.status === "running" ? "\u27F3" : info.status === "failed" ? "\u2717" : "\u25CB";
|
|
469
|
+
const title = info.title.length > 50 ? `${info.title.slice(0, 50)}...` : info.title;
|
|
470
|
+
lines.push(` ${icon} #${num} ${title}`);
|
|
471
|
+
}
|
|
472
|
+
setForgeflowWidget(ctx, lines);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/pipelines/review.ts
|
|
476
|
+
async function runReviewInline(cwd, signal, onUpdate, ctx, stages, diffCmd = "git diff main...HEAD", pipeline = "review", options = {}) {
|
|
477
|
+
const diff = await exec(diffCmd, cwd);
|
|
478
|
+
if (!diff) {
|
|
479
|
+
return { content: [{ type: "text", text: "No changes to review." }] };
|
|
480
|
+
}
|
|
481
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline, onUpdate };
|
|
482
|
+
const extraInstructions = options.customPrompt ? `
|
|
483
|
+
|
|
484
|
+
ADDITIONAL INSTRUCTIONS FROM USER:
|
|
485
|
+
${options.customPrompt}` : "";
|
|
486
|
+
cleanSignal(cwd, "findings");
|
|
487
|
+
stages.push(emptyStage("code-reviewer"));
|
|
488
|
+
await runAgent("code-reviewer", `Review the following diff:
|
|
489
|
+
|
|
490
|
+
${diff}${extraInstructions}`, {
|
|
491
|
+
...opts,
|
|
492
|
+
tools: TOOLS_NO_EDIT
|
|
493
|
+
});
|
|
494
|
+
if (!signalExists(cwd, "findings")) {
|
|
495
|
+
return { content: [{ type: "text", text: "Review passed \u2014 no actionable findings." }] };
|
|
496
|
+
}
|
|
497
|
+
stages.push(emptyStage("review-judge"));
|
|
498
|
+
const findings = readSignal(cwd, "findings") ?? "";
|
|
499
|
+
await runAgent(
|
|
500
|
+
"review-judge",
|
|
501
|
+
`Validate the following code review findings against the actual code:
|
|
502
|
+
|
|
503
|
+
${findings}`,
|
|
504
|
+
{ ...opts, tools: TOOLS_NO_EDIT }
|
|
505
|
+
);
|
|
506
|
+
if (!signalExists(cwd, "findings")) {
|
|
507
|
+
return { content: [{ type: "text", text: "Review passed \u2014 judge filtered all findings." }] };
|
|
508
|
+
}
|
|
509
|
+
const validatedFindings = readSignal(cwd, "findings") ?? "";
|
|
510
|
+
if (options.interactive && options.prNumber) {
|
|
511
|
+
const repo = await exec("gh repo view --json nameWithOwner --jq .nameWithOwner", cwd);
|
|
512
|
+
const prNum = options.prNumber;
|
|
513
|
+
const proposalPrompt = `You have validated code review findings for PR #${prNum} in ${repo}.
|
|
514
|
+
|
|
515
|
+
FINDINGS:
|
|
516
|
+
${validatedFindings}
|
|
517
|
+
|
|
518
|
+
Generate ready-to-run \`gh api\` commands to post each finding as a PR review comment. One command per finding.
|
|
519
|
+
|
|
520
|
+
Format each as:
|
|
521
|
+
|
|
522
|
+
**Finding N** \u2014 path/to/file.ts:LINE
|
|
523
|
+
|
|
524
|
+
\`\`\`bash
|
|
525
|
+
gh api repos/${repo}/pulls/${prNum}/comments \\
|
|
526
|
+
--method POST \\
|
|
527
|
+
--field body="<comment>" \\
|
|
528
|
+
--field commit_id="$(gh pr view ${prNum} --repo ${repo} --json headRefOid -q .headRefOid)" \\
|
|
529
|
+
--field path="path/to/file.ts" \\
|
|
530
|
+
--field line=LINE \\
|
|
531
|
+
--field side="RIGHT"
|
|
532
|
+
\`\`\`
|
|
533
|
+
|
|
534
|
+
Comment tone rules:
|
|
535
|
+
- Write like a teammate, not an auditor. Casual, brief, direct.
|
|
536
|
+
- 1-2 short sentences max. Lead with the suggestion, not the problem.
|
|
537
|
+
- Use "might be worth..." / "could we..." / "what about..." / "small thing:"
|
|
538
|
+
- No em dashes, no "Consider...", no "Note that...", no hedging filler.
|
|
539
|
+
- Use GitHub \`\`\`suggestion\`\`\` blocks when proposing code changes.
|
|
540
|
+
- Only generate commands for findings with a specific file + line.
|
|
541
|
+
|
|
542
|
+
After the comments, add the review decision command:
|
|
543
|
+
|
|
544
|
+
\`\`\`bash
|
|
545
|
+
gh pr review ${prNum} --request-changes --body "Left a few comments" --repo ${repo}
|
|
546
|
+
\`\`\`
|
|
547
|
+
|
|
548
|
+
Output ONLY the commands, no other text.`;
|
|
549
|
+
stages.push(emptyStage("propose-comments"));
|
|
550
|
+
await runAgent("review-judge", proposalPrompt, {
|
|
551
|
+
agentsDir: AGENTS_DIR,
|
|
552
|
+
cwd,
|
|
553
|
+
signal,
|
|
554
|
+
stages,
|
|
555
|
+
pipeline,
|
|
556
|
+
onUpdate,
|
|
557
|
+
tools: TOOLS_READONLY
|
|
558
|
+
});
|
|
559
|
+
const commentStage = stages.find((s) => s.name === "propose-comments");
|
|
560
|
+
const proposedCommands = commentStage?.output || "";
|
|
561
|
+
if (proposedCommands && ctx.hasUI) {
|
|
562
|
+
const reviewed = await ctx.ui.editor(
|
|
563
|
+
`Review PR comments for PR #${prNum} (edit or close to skip)`,
|
|
564
|
+
`${validatedFindings}
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
Proposed commands (run these to post):
|
|
569
|
+
|
|
570
|
+
${proposedCommands}`
|
|
571
|
+
);
|
|
572
|
+
if (reviewed != null) {
|
|
573
|
+
const action = await ctx.ui.select("Post these review comments?", ["Post comments", "Skip"]);
|
|
574
|
+
if (action === "Post comments") {
|
|
575
|
+
const commands = reviewed.match(/```bash\n([\s\S]*?)```/g) || [];
|
|
576
|
+
for (const block of commands) {
|
|
577
|
+
const cmd = block.replace(/```bash\n/, "").replace(/```$/, "").trim();
|
|
578
|
+
if (cmd.startsWith("gh ")) {
|
|
579
|
+
await exec(cmd, cwd);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return { content: [{ type: "text", text: validatedFindings }], isError: true };
|
|
587
|
+
}
|
|
588
|
+
async function runReview(cwd, target, signal, onUpdate, ctx, customPrompt) {
|
|
589
|
+
const stages = [];
|
|
590
|
+
let diffCmd = "git diff main...HEAD";
|
|
591
|
+
let prNumber;
|
|
592
|
+
if (target.match(/^\d+$/)) {
|
|
593
|
+
diffCmd = `gh pr diff ${target}`;
|
|
594
|
+
prNumber = target;
|
|
595
|
+
} else if (target.startsWith("--branch")) {
|
|
596
|
+
const branch = target.replace("--branch", "").trim() || "HEAD";
|
|
597
|
+
diffCmd = `git diff main...${branch}`;
|
|
598
|
+
} else {
|
|
599
|
+
const pr = await exec("gh pr view --json number --jq .number 2>/dev/null", cwd);
|
|
600
|
+
if (pr && pr !== "") prNumber = pr;
|
|
601
|
+
}
|
|
602
|
+
const result = await runReviewInline(cwd, signal, onUpdate, ctx, stages, diffCmd, "review", {
|
|
603
|
+
prNumber,
|
|
604
|
+
interactive: ctx.hasUI,
|
|
605
|
+
customPrompt
|
|
606
|
+
});
|
|
607
|
+
return { ...result, details: { pipeline: "review", stages } };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/pipelines/implement.ts
|
|
611
|
+
async function reviewAndFix(cwd, signal, onUpdate, ctx, stages, pipeline = "implement") {
|
|
612
|
+
const reviewResult = await runReviewInline(cwd, signal, onUpdate, ctx, stages);
|
|
613
|
+
if (reviewResult.isError) {
|
|
614
|
+
const findings = reviewResult.content[0]?.type === "text" ? reviewResult.content[0].text : "";
|
|
615
|
+
stages.push(emptyStage("fix-findings"));
|
|
616
|
+
await runAgent(
|
|
617
|
+
"implementor",
|
|
618
|
+
`Fix the following code review findings:
|
|
619
|
+
|
|
620
|
+
${findings}
|
|
621
|
+
|
|
622
|
+
RULES:
|
|
623
|
+
- Fix only the cited issues. Do not refactor or improve unrelated code.
|
|
624
|
+
- Run the check command after fixes.
|
|
625
|
+
- Commit and push the fixes.`,
|
|
626
|
+
{ agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline, onUpdate, tools: TOOLS_ALL, stageName: "fix-findings" }
|
|
627
|
+
);
|
|
628
|
+
cleanSignal(cwd, "findings");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function refactorAndReview(cwd, signal, onUpdate, ctx, stages, skipReview, pipeline = "implement") {
|
|
632
|
+
if (!stages.some((s) => s.name === "refactorer")) stages.push(emptyStage("refactorer"));
|
|
633
|
+
await runAgent(
|
|
634
|
+
"refactorer",
|
|
635
|
+
"Review code added in this branch (git diff main...HEAD). Refactor if clear wins exist. Run checks after changes. Commit and push if changed.",
|
|
636
|
+
{ agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline, onUpdate, tools: TOOLS_ALL }
|
|
637
|
+
);
|
|
638
|
+
if (!skipReview) {
|
|
639
|
+
await reviewAndFix(cwd, signal, onUpdate, ctx, stages, pipeline);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function resolveQuestions(plan, ctx) {
|
|
643
|
+
const sectionMatch = plan.match(/### Unresolved Questions\n([\s\S]*?)(?=\n###|$)/);
|
|
644
|
+
if (!sectionMatch) return plan;
|
|
645
|
+
const section = sectionMatch[1] ?? "";
|
|
646
|
+
const questions = [];
|
|
647
|
+
for (const m of section.matchAll(/^- (.+)$/gm)) {
|
|
648
|
+
if (m[1]) questions.push(m[1]);
|
|
649
|
+
}
|
|
650
|
+
if (questions.length === 0) return plan;
|
|
651
|
+
let updatedSection = section;
|
|
652
|
+
for (const q of questions) {
|
|
653
|
+
const answer = await ctx.ui.input(`${q}`, "Skip to use defaults");
|
|
654
|
+
if (answer != null && answer.trim() !== "") {
|
|
655
|
+
updatedSection = updatedSection.replace(`- ${q}`, `- ${q}
|
|
656
|
+
**Answer:** ${answer.trim()}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return plan.replace(`### Unresolved Questions
|
|
660
|
+
${section}`, `### Unresolved Questions
|
|
661
|
+
${updatedSection}`);
|
|
662
|
+
}
|
|
663
|
+
async function runImplement(cwd, issueArg, signal, onUpdate, ctx, flags = {
|
|
664
|
+
skipPlan: false,
|
|
665
|
+
skipReview: false
|
|
666
|
+
}) {
|
|
667
|
+
const interactive = ctx.hasUI && !flags.autonomous;
|
|
668
|
+
const resolved = await resolveIssue(cwd, issueArg || void 0);
|
|
669
|
+
if (typeof resolved === "string") {
|
|
670
|
+
return { content: [{ type: "text", text: resolved }], details: { pipeline: "implement", stages: [] } };
|
|
671
|
+
}
|
|
672
|
+
const isGitHub = resolved.source === "github" && resolved.number > 0;
|
|
673
|
+
const issueLabel = isGitHub ? `#${resolved.number}: ${resolved.title}` : `${resolved.key}: ${resolved.title}`;
|
|
674
|
+
if (!flags.autonomous && (resolved.number || resolved.key)) {
|
|
675
|
+
const tag = isGitHub ? `#${resolved.number}` : resolved.key;
|
|
676
|
+
setForgeflowStatus(ctx, `${tag} ${resolved.title} \xB7 ${resolved.branch}`);
|
|
677
|
+
}
|
|
678
|
+
const issueContext = isGitHub ? `Issue #${resolved.number}: ${resolved.title}
|
|
679
|
+
|
|
680
|
+
${resolved.body}` : `Jira ${resolved.key}: ${resolved.title}
|
|
681
|
+
|
|
682
|
+
${resolved.body}`;
|
|
683
|
+
const customPromptSection = flags.customPrompt ? `
|
|
684
|
+
|
|
685
|
+
ADDITIONAL INSTRUCTIONS FROM USER:
|
|
686
|
+
${flags.customPrompt}` : "";
|
|
687
|
+
if (resolved.existingPR) {
|
|
688
|
+
const stages2 = [];
|
|
689
|
+
if (!flags.skipReview) {
|
|
690
|
+
await reviewAndFix(cwd, signal, onUpdate, ctx, stages2);
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
content: [{ type: "text", text: `Resumed ${issueLabel} \u2014 PR #${resolved.existingPR} already exists.` }],
|
|
694
|
+
details: { pipeline: "implement", stages: stages2 }
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (resolved.branch) {
|
|
698
|
+
const branchExists = await exec(
|
|
699
|
+
`git rev-parse --verify ${resolved.branch} 2>/dev/null && echo yes || echo no`,
|
|
700
|
+
cwd
|
|
701
|
+
);
|
|
702
|
+
if (branchExists === "yes") {
|
|
703
|
+
await ensureBranch(cwd, resolved.branch);
|
|
704
|
+
const ahead = await exec(`git rev-list main..${resolved.branch} --count`, cwd);
|
|
705
|
+
if (parseInt(ahead, 10) > 0) {
|
|
706
|
+
await exec(`git push -u origin ${resolved.branch}`, cwd);
|
|
707
|
+
const prBody = isGitHub ? `Closes #${resolved.number}` : `Jira: ${resolved.key}`;
|
|
708
|
+
await exec(`gh pr create --title "${resolved.title}" --body "${prBody}" --head ${resolved.branch}`, cwd);
|
|
709
|
+
const stages2 = [];
|
|
710
|
+
await refactorAndReview(cwd, signal, onUpdate, ctx, stages2, flags.skipReview);
|
|
711
|
+
return {
|
|
712
|
+
content: [{ type: "text", text: `Resumed ${issueLabel} \u2014 pushed existing commits and created PR.` }],
|
|
713
|
+
details: { pipeline: "implement", stages: stages2 }
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const stageList = [];
|
|
719
|
+
if (!flags.skipPlan) stageList.push(emptyStage("planner"));
|
|
720
|
+
stageList.push(emptyStage("implementor"));
|
|
721
|
+
stageList.push(emptyStage("refactorer"));
|
|
722
|
+
const stages = stageList;
|
|
723
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "implement", onUpdate };
|
|
724
|
+
let plan = "";
|
|
725
|
+
if (!flags.skipPlan) {
|
|
726
|
+
const planResult = await runAgent(
|
|
727
|
+
"planner",
|
|
728
|
+
`Plan the implementation for this issue by producing a sequenced list of test cases.
|
|
729
|
+
|
|
730
|
+
${issueContext}${customPromptSection}`,
|
|
731
|
+
{ ...opts, tools: TOOLS_READONLY }
|
|
732
|
+
);
|
|
733
|
+
if (planResult.status === "failed") {
|
|
734
|
+
return {
|
|
735
|
+
content: [{ type: "text", text: `Planner failed: ${planResult.output}` }],
|
|
736
|
+
details: { pipeline: "implement", stages },
|
|
737
|
+
isError: true
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
plan = planResult.output;
|
|
741
|
+
if (interactive && plan) {
|
|
742
|
+
const edited = await ctx.ui.editor(`Review implementation plan for ${issueLabel}`, plan);
|
|
743
|
+
if (edited != null && edited !== plan) {
|
|
744
|
+
plan = edited;
|
|
745
|
+
}
|
|
746
|
+
plan = await resolveQuestions(plan, ctx);
|
|
747
|
+
const action = await ctx.ui.select("Plan ready. What next?", ["Approve and implement", "Cancel"]);
|
|
748
|
+
if (action === "Cancel" || action == null) {
|
|
749
|
+
return {
|
|
750
|
+
content: [{ type: "text", text: "Implementation cancelled." }],
|
|
751
|
+
details: { pipeline: "implement", stages }
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (resolved.branch) {
|
|
757
|
+
const currentBranch = await exec("git branch --show-current", cwd);
|
|
758
|
+
if (currentBranch === "main" || currentBranch === "master") {
|
|
759
|
+
const dirty = await exec("git status --porcelain", cwd);
|
|
760
|
+
if (dirty) {
|
|
761
|
+
return {
|
|
762
|
+
content: [
|
|
763
|
+
{
|
|
764
|
+
type: "text",
|
|
765
|
+
text: `Cannot switch to ${resolved.branch} \u2014 working tree is dirty. Please commit or stash your changes first.`
|
|
766
|
+
}
|
|
767
|
+
],
|
|
768
|
+
details: { pipeline: "implement", stages: [] },
|
|
769
|
+
isError: true
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
await ensureBranch(cwd, resolved.branch);
|
|
773
|
+
const afterBranch = await exec("git branch --show-current", cwd);
|
|
774
|
+
if (afterBranch !== resolved.branch) {
|
|
775
|
+
return {
|
|
776
|
+
content: [
|
|
777
|
+
{
|
|
778
|
+
type: "text",
|
|
779
|
+
text: `Failed to switch to ${resolved.branch} (still on ${afterBranch}). Check git state and retry.`
|
|
780
|
+
}
|
|
781
|
+
],
|
|
782
|
+
details: { pipeline: "implement", stages: [] },
|
|
783
|
+
isError: true
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
cleanSignal(cwd, "blocked");
|
|
789
|
+
const planSection = plan ? `
|
|
790
|
+
|
|
791
|
+
IMPLEMENTATION PLAN:
|
|
792
|
+
${plan}` : "";
|
|
793
|
+
const branchNote = resolved.branch ? `
|
|
794
|
+
- You should be on branch: ${resolved.branch} \u2014 do NOT create or switch branches.` : "\n- Do NOT create or switch branches.";
|
|
795
|
+
const prNote = resolved.existingPR ? `
|
|
796
|
+
- PR #${resolved.existingPR} already exists for this branch.` : "";
|
|
797
|
+
const closeNote = isGitHub ? `
|
|
798
|
+
- The PR body MUST include 'Closes #${resolved.number}' so the issue auto-closes on merge.` : `
|
|
799
|
+
- The PR body should reference Jira issue ${resolved.key}.`;
|
|
800
|
+
const unresolvedNote = flags.autonomous ? `
|
|
801
|
+
- If the plan has unresolved questions, resolve them yourself using sensible defaults. Do NOT stop and wait.` : "";
|
|
802
|
+
await runAgent(
|
|
803
|
+
"implementor",
|
|
804
|
+
`Implement the following issue using strict TDD (red-green-refactor).
|
|
805
|
+
|
|
806
|
+
${issueContext}${planSection}${customPromptSection}
|
|
807
|
+
|
|
808
|
+
WORKFLOW:
|
|
809
|
+
1. Read the codebase.
|
|
810
|
+
2. TDD${plan ? " following the plan" : ""}.
|
|
811
|
+
3. Refactor after all tests pass.
|
|
812
|
+
4. Run check command, fix failures.
|
|
813
|
+
5. Commit, push, and create a PR.
|
|
814
|
+
|
|
815
|
+
CONSTRAINTS:${branchNote}${prNote}${closeNote}${unresolvedNote}
|
|
816
|
+
- If blocked, write BLOCKED.md with the reason and stop.`,
|
|
817
|
+
{ ...opts, tools: TOOLS_ALL }
|
|
818
|
+
);
|
|
819
|
+
if (signalExists(cwd, "blocked")) {
|
|
820
|
+
const reason = readSignal(cwd, "blocked") ?? "";
|
|
821
|
+
return {
|
|
822
|
+
content: [{ type: "text", text: `Implementor blocked:
|
|
823
|
+
${reason}` }],
|
|
824
|
+
details: { pipeline: "implement", stages },
|
|
825
|
+
isError: true
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
await refactorAndReview(cwd, signal, onUpdate, ctx, stages, flags.skipReview);
|
|
829
|
+
let prNumber = "";
|
|
830
|
+
if (resolved.branch) {
|
|
831
|
+
await exec(`git push -u origin ${resolved.branch}`, cwd);
|
|
832
|
+
prNumber = await exec(`gh pr list --head "${resolved.branch}" --json number --jq '.[0].number'`, cwd);
|
|
833
|
+
if (!prNumber || prNumber === "null") {
|
|
834
|
+
const prBody = isGitHub ? `Closes #${resolved.number}` : `Jira: ${resolved.key}`;
|
|
835
|
+
await exec(`gh pr create --title "${resolved.title}" --body "${prBody}" --head ${resolved.branch}`, cwd);
|
|
836
|
+
prNumber = await exec(`gh pr list --head "${resolved.branch}" --json number --jq '.[0].number'`, cwd);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!flags.autonomous && prNumber && prNumber !== "null") {
|
|
840
|
+
const mergeStage = emptyStage("merge");
|
|
841
|
+
stages.push(mergeStage);
|
|
842
|
+
await exec(`gh pr merge ${prNumber} --squash --delete-branch`, cwd);
|
|
843
|
+
await exec("git checkout main && git pull", cwd);
|
|
844
|
+
mergeStage.status = "done";
|
|
845
|
+
mergeStage.output = `Merged PR #${prNumber}`;
|
|
846
|
+
onUpdate?.({
|
|
847
|
+
content: [{ type: "text", text: "Pipeline complete" }],
|
|
848
|
+
details: { pipeline: "implement", stages }
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
content: [{ type: "text", text: `Implementation of ${issueLabel} complete.` }],
|
|
853
|
+
details: { pipeline: "implement", stages }
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/pipelines/implement-all.ts
|
|
858
|
+
function getReadyIssues(issues, completed) {
|
|
859
|
+
return issues.filter((issue) => {
|
|
860
|
+
if (completed.has(issue.number)) return false;
|
|
861
|
+
const parts = issue.body.split("## Dependencies");
|
|
862
|
+
if (parts.length < 2) return true;
|
|
863
|
+
const depSection = parts[1]?.split("\n## ")[0] ?? "";
|
|
864
|
+
const deps = [...depSection.matchAll(/#(\d+)/g)].map((m) => parseInt(m[1] ?? "0", 10));
|
|
865
|
+
return deps.every((d) => completed.has(d));
|
|
866
|
+
}).map((i) => i.number);
|
|
867
|
+
}
|
|
868
|
+
async function runImplementAll(cwd, signal, onUpdate, ctx, flags) {
|
|
869
|
+
const allStages = [];
|
|
870
|
+
const issueProgress = /* @__PURE__ */ new Map();
|
|
871
|
+
const closedJson = await exec(
|
|
872
|
+
`gh issue list --state closed --label "auto-generated" --json number --jq '.[].number'`,
|
|
873
|
+
cwd
|
|
874
|
+
);
|
|
875
|
+
const completed = new Set(closedJson ? closedJson.split("\n").filter(Boolean).map(Number) : []);
|
|
876
|
+
let iteration = 0;
|
|
877
|
+
const maxIterations = 50;
|
|
878
|
+
while (iteration++ < maxIterations) {
|
|
879
|
+
if (signal.aborted) break;
|
|
880
|
+
await exec("git checkout main && git pull --rebase", cwd);
|
|
881
|
+
const issuesJson = await exec(
|
|
882
|
+
`gh issue list --state open --label "auto-generated" --json number,title,body --jq 'sort_by(.number)'`,
|
|
883
|
+
cwd
|
|
884
|
+
);
|
|
885
|
+
let issues;
|
|
886
|
+
try {
|
|
887
|
+
issues = JSON.parse(issuesJson || "[]");
|
|
888
|
+
} catch {
|
|
889
|
+
issues = [];
|
|
890
|
+
}
|
|
891
|
+
if (issues.length === 0) {
|
|
892
|
+
return {
|
|
893
|
+
content: [{ type: "text", text: "All issues implemented." }],
|
|
894
|
+
details: { pipeline: "implement-all", stages: allStages }
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
for (const issue of issues) {
|
|
898
|
+
if (!issueProgress.has(issue.number)) {
|
|
899
|
+
issueProgress.set(issue.number, { title: issue.title, status: "pending" });
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const ready = getReadyIssues(issues, completed);
|
|
903
|
+
if (ready.length === 0) {
|
|
904
|
+
return {
|
|
905
|
+
content: [
|
|
906
|
+
{ type: "text", text: `${issues.length} issues remain but all have unresolved dependencies.` }
|
|
907
|
+
],
|
|
908
|
+
details: { pipeline: "implement-all", stages: allStages },
|
|
909
|
+
isError: true
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
const issueNum = ready[0];
|
|
913
|
+
const issueTitle = issues.find((i) => i.number === issueNum)?.title ?? `#${issueNum}`;
|
|
914
|
+
issueProgress.set(issueNum, { title: issueTitle, status: "running" });
|
|
915
|
+
setForgeflowStatus(
|
|
916
|
+
ctx,
|
|
917
|
+
`implement-all \xB7 ${completed.size}/${completed.size + issues.length} \xB7 #${issueNum} ${issueTitle}`
|
|
918
|
+
);
|
|
919
|
+
updateProgressWidget(ctx, issueProgress, sumUsage(allStages).cost);
|
|
920
|
+
allStages.push(emptyStage(`implement-${issueNum}`));
|
|
921
|
+
const implResult = await runImplement(cwd, String(issueNum), signal, onUpdate, ctx, {
|
|
922
|
+
...flags,
|
|
923
|
+
autonomous: true
|
|
924
|
+
});
|
|
925
|
+
const implStage = allStages.find((s) => s.name === `implement-${issueNum}`);
|
|
926
|
+
if (implStage) {
|
|
927
|
+
implStage.status = implResult.isError ? "failed" : "done";
|
|
928
|
+
implStage.output = implResult.content[0]?.type === "text" ? implResult.content[0].text : "";
|
|
929
|
+
const detailedStages = implResult.details?.stages;
|
|
930
|
+
if (detailedStages) implStage.usage = sumUsage(detailedStages);
|
|
931
|
+
}
|
|
932
|
+
if (implResult.isError) {
|
|
933
|
+
issueProgress.set(issueNum, { title: issueTitle, status: "failed" });
|
|
934
|
+
updateProgressWidget(ctx, issueProgress, sumUsage(allStages).cost);
|
|
935
|
+
return {
|
|
936
|
+
content: [
|
|
937
|
+
{
|
|
938
|
+
type: "text",
|
|
939
|
+
text: `Failed on issue #${issueNum}: ${implResult.content[0]?.type === "text" ? implResult.content[0].text : "unknown error"}`
|
|
940
|
+
}
|
|
941
|
+
],
|
|
942
|
+
details: { pipeline: "implement-all", stages: allStages },
|
|
943
|
+
isError: true
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const branch = `feat/issue-${issueNum}`;
|
|
947
|
+
await exec("git checkout main && git pull --rebase", cwd);
|
|
948
|
+
const prNum = await exec(`gh pr list --head "${branch}" --json number --jq '.[0].number'`, cwd);
|
|
949
|
+
if (prNum && prNum !== "null") {
|
|
950
|
+
const mergeResult = await exec(`gh pr merge ${prNum} --squash --delete-branch`, cwd);
|
|
951
|
+
if (mergeResult.includes("Merged") || mergeResult === "") {
|
|
952
|
+
completed.add(issueNum);
|
|
953
|
+
} else {
|
|
954
|
+
const prState = await exec(`gh pr view ${prNum} --json state --jq '.state'`, cwd);
|
|
955
|
+
if (prState === "MERGED") {
|
|
956
|
+
completed.add(issueNum);
|
|
957
|
+
} else {
|
|
958
|
+
issueProgress.set(issueNum, { title: issueTitle, status: "failed" });
|
|
959
|
+
updateProgressWidget(ctx, issueProgress, sumUsage(allStages).cost);
|
|
960
|
+
return {
|
|
961
|
+
content: [{ type: "text", text: `Failed to merge PR #${prNum} for issue #${issueNum}.` }],
|
|
962
|
+
details: { pipeline: "implement-all", stages: allStages },
|
|
963
|
+
isError: true
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
issueProgress.set(issueNum, { title: issueTitle, status: "failed" });
|
|
969
|
+
updateProgressWidget(ctx, issueProgress, sumUsage(allStages).cost);
|
|
970
|
+
return {
|
|
971
|
+
content: [{ type: "text", text: `No PR found for issue #${issueNum} after implementation.` }],
|
|
972
|
+
details: { pipeline: "implement-all", stages: allStages },
|
|
973
|
+
isError: true
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
issueProgress.set(issueNum, { title: issueTitle, status: "done" });
|
|
977
|
+
setForgeflowStatus(
|
|
978
|
+
ctx,
|
|
979
|
+
`implement-all \xB7 ${completed.size}/${completed.size + issues.length - 1} \xB7 $${sumUsage(allStages).cost.toFixed(2)}`
|
|
980
|
+
);
|
|
981
|
+
updateProgressWidget(ctx, issueProgress, sumUsage(allStages).cost);
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
content: [{ type: "text", text: `Reached max iterations (${maxIterations}).` }],
|
|
985
|
+
details: { pipeline: "implement-all", stages: allStages }
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/index.ts
|
|
990
|
+
function parseImplFlags(args) {
|
|
991
|
+
const skipPlan = args.includes("--skip-plan");
|
|
992
|
+
const skipReview = args.includes("--skip-review");
|
|
993
|
+
const rest = args.replace(/--skip-plan/g, "").replace(/--skip-review/g, "").trim();
|
|
994
|
+
const firstSpace = rest.indexOf(" ");
|
|
995
|
+
const issue = firstSpace === -1 ? rest : rest.slice(0, firstSpace);
|
|
996
|
+
const customPrompt = firstSpace === -1 ? "" : rest.slice(firstSpace + 1).trim().replace(/^"(.*)"$/, "$1");
|
|
997
|
+
const flags = [skipPlan ? ", skipPlan: true" : "", skipReview ? ", skipReview: true" : ""].join("");
|
|
998
|
+
return { issue, customPrompt, flags };
|
|
999
|
+
}
|
|
1000
|
+
function parseReviewArgs(args) {
|
|
1001
|
+
const trimmed = args.trim();
|
|
1002
|
+
if (!trimmed) return { target: "", customPrompt: "" };
|
|
1003
|
+
if (trimmed.startsWith("--branch")) {
|
|
1004
|
+
const afterFlag = trimmed.replace(/^--branch\s*/, "").trim();
|
|
1005
|
+
const firstSpace2 = afterFlag.indexOf(" ");
|
|
1006
|
+
if (firstSpace2 === -1) return { target: `--branch ${afterFlag}`, customPrompt: "" };
|
|
1007
|
+
return {
|
|
1008
|
+
target: `--branch ${afterFlag.slice(0, firstSpace2)}`,
|
|
1009
|
+
customPrompt: afterFlag.slice(firstSpace2 + 1).trim().replace(/^"(.*)"$/, "$1")
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
1013
|
+
if (firstSpace === -1) return { target: trimmed, customPrompt: "" };
|
|
1014
|
+
return {
|
|
1015
|
+
target: trimmed.slice(0, firstSpace),
|
|
1016
|
+
customPrompt: trimmed.slice(firstSpace + 1).trim().replace(/^"(.*)"$/, "$1")
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function getDisplayItems(messages) {
|
|
1020
|
+
const items = [];
|
|
1021
|
+
for (const msg of messages) {
|
|
1022
|
+
if (msg.role === "assistant") {
|
|
1023
|
+
for (const part of msg.content) {
|
|
1024
|
+
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
1025
|
+
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return items;
|
|
1030
|
+
}
|
|
1031
|
+
function formatToolCallShort(name, args, fg) {
|
|
1032
|
+
switch (name) {
|
|
1033
|
+
case "bash": {
|
|
1034
|
+
const cmd = args.command || "...";
|
|
1035
|
+
return fg("muted", "$ ") + fg("toolOutput", cmd.length > 60 ? `${cmd.slice(0, 60)}...` : cmd);
|
|
1036
|
+
}
|
|
1037
|
+
case "read":
|
|
1038
|
+
return fg("muted", "read ") + fg("accent", args.file_path || args.path || "...");
|
|
1039
|
+
case "write":
|
|
1040
|
+
return fg("muted", "write ") + fg("accent", args.file_path || args.path || "...");
|
|
1041
|
+
case "edit":
|
|
1042
|
+
return fg("muted", "edit ") + fg("accent", args.file_path || args.path || "...");
|
|
1043
|
+
case "grep":
|
|
1044
|
+
return fg("muted", "grep ") + fg("accent", `/${args.pattern || ""}/`);
|
|
1045
|
+
case "find":
|
|
1046
|
+
return fg("muted", "find ") + fg("accent", args.pattern || "*");
|
|
1047
|
+
default:
|
|
1048
|
+
return fg("accent", name);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function formatUsage(usage, model) {
|
|
1052
|
+
const parts = [];
|
|
1053
|
+
if (usage.turns) parts.push(`${usage.turns}t`);
|
|
1054
|
+
if (usage.input) parts.push(`\u2191${usage.input < 1e3 ? usage.input : `${Math.round(usage.input / 1e3)}k`}`);
|
|
1055
|
+
if (usage.output) parts.push(`\u2193${usage.output < 1e3 ? usage.output : `${Math.round(usage.output / 1e3)}k`}`);
|
|
1056
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
1057
|
+
if (model) parts.push(model);
|
|
1058
|
+
return parts.join(" ");
|
|
1059
|
+
}
|
|
1060
|
+
var ForgeflowDevParams = Type.Object({
|
|
1061
|
+
pipeline: Type.String({
|
|
1062
|
+
description: 'Which pipeline to run: "implement", "implement-all", "review", "architecture", or "discover-skills"'
|
|
1063
|
+
}),
|
|
1064
|
+
issue: Type.Optional(
|
|
1065
|
+
Type.String({
|
|
1066
|
+
description: "Issue number or description for implement pipeline"
|
|
1067
|
+
})
|
|
1068
|
+
),
|
|
1069
|
+
target: Type.Optional(Type.String({ description: "PR number or --branch for review pipeline" })),
|
|
1070
|
+
skipPlan: Type.Optional(Type.Boolean({ description: "Skip planner, implement directly (default false)" })),
|
|
1071
|
+
skipReview: Type.Optional(Type.Boolean({ description: "Skip code review after implementation (default false)" })),
|
|
1072
|
+
customPrompt: Type.Optional(
|
|
1073
|
+
Type.String({ description: "Additional user instructions passed to agents (e.g. 'check the openapi spec')" })
|
|
1074
|
+
)
|
|
1075
|
+
});
|
|
1076
|
+
function registerForgeflowDevTool(pi) {
|
|
1077
|
+
pi.registerTool({
|
|
1078
|
+
name: "forgeflow-dev",
|
|
1079
|
+
label: "Forgeflow Dev",
|
|
1080
|
+
description: [
|
|
1081
|
+
"Run forgeflow dev pipelines: implement (plan\u2192TDD\u2192refactor a single issue),",
|
|
1082
|
+
"implement-all (loop through all open issues autonomously), review (deterministic checks\u2192code review\u2192judge),",
|
|
1083
|
+
"architecture (analyze codebase for structural friction\u2192create RFC issues),",
|
|
1084
|
+
"discover-skills (find and install domain-specific plugins).",
|
|
1085
|
+
"Each pipeline spawns specialized sub-agents with isolated context."
|
|
1086
|
+
].join(" "),
|
|
1087
|
+
parameters: ForgeflowDevParams,
|
|
1088
|
+
async execute(_toolCallId, _params, signal, onUpdate, ctx) {
|
|
1089
|
+
const params = _params;
|
|
1090
|
+
const cwd = ctx.cwd;
|
|
1091
|
+
const sig = signal ?? new AbortController().signal;
|
|
1092
|
+
try {
|
|
1093
|
+
switch (params.pipeline) {
|
|
1094
|
+
case "implement":
|
|
1095
|
+
return await runImplement(cwd, params.issue ?? "", sig, onUpdate, ctx, {
|
|
1096
|
+
skipPlan: params.skipPlan ?? false,
|
|
1097
|
+
skipReview: params.skipReview ?? false,
|
|
1098
|
+
customPrompt: params.customPrompt
|
|
1099
|
+
});
|
|
1100
|
+
case "implement-all":
|
|
1101
|
+
return await runImplementAll(cwd, sig, onUpdate, ctx, {
|
|
1102
|
+
skipPlan: params.skipPlan ?? false,
|
|
1103
|
+
skipReview: params.skipReview ?? false
|
|
1104
|
+
});
|
|
1105
|
+
case "review":
|
|
1106
|
+
return await runReview(cwd, params.target ?? "", sig, onUpdate, ctx, params.customPrompt);
|
|
1107
|
+
case "architecture":
|
|
1108
|
+
return await runArchitecture(cwd, sig, onUpdate, ctx);
|
|
1109
|
+
case "discover-skills":
|
|
1110
|
+
return await runDiscoverSkills(cwd, params.issue ?? "", sig, onUpdate, ctx);
|
|
1111
|
+
default:
|
|
1112
|
+
return {
|
|
1113
|
+
content: [
|
|
1114
|
+
{
|
|
1115
|
+
type: "text",
|
|
1116
|
+
text: `Unknown pipeline: ${params.pipeline}. Use: implement, implement-all, review, architecture, discover-skills`
|
|
1117
|
+
}
|
|
1118
|
+
],
|
|
1119
|
+
details: { pipeline: params.pipeline, stages: [] }
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
} finally {
|
|
1123
|
+
if (ctx.hasUI) {
|
|
1124
|
+
ctx.ui.setStatus("forgeflow-dev", void 0);
|
|
1125
|
+
ctx.ui.setWidget("forgeflow-dev", void 0);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
renderCall(_args, theme) {
|
|
1130
|
+
const args = _args;
|
|
1131
|
+
const pipeline = args.pipeline || "?";
|
|
1132
|
+
let text = theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", pipeline);
|
|
1133
|
+
if (args.issue) {
|
|
1134
|
+
const prefix = /^[A-Z]+-\d+$/.test(args.issue) ? " " : " #";
|
|
1135
|
+
text += theme.fg("dim", `${prefix}${args.issue}`);
|
|
1136
|
+
}
|
|
1137
|
+
if (args.target) text += theme.fg("dim", ` ${args.target}`);
|
|
1138
|
+
return new Text(text, 0, 0);
|
|
1139
|
+
},
|
|
1140
|
+
renderResult(result, { expanded }, theme) {
|
|
1141
|
+
const details = result.details;
|
|
1142
|
+
if (!details || details.stages.length === 0) {
|
|
1143
|
+
const text = result.content[0];
|
|
1144
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1145
|
+
}
|
|
1146
|
+
if (expanded) {
|
|
1147
|
+
return renderExpanded(details, theme);
|
|
1148
|
+
}
|
|
1149
|
+
return renderCollapsed(details, theme);
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
function renderExpanded(details, theme) {
|
|
1154
|
+
const container = new Container();
|
|
1155
|
+
container.addChild(
|
|
1156
|
+
new Text(theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", details.pipeline), 0, 0)
|
|
1157
|
+
);
|
|
1158
|
+
container.addChild(new Spacer(1));
|
|
1159
|
+
for (const stage of details.stages) {
|
|
1160
|
+
const icon = stageIcon(stage, theme);
|
|
1161
|
+
container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(stage.name))}`, 0, 0));
|
|
1162
|
+
const items = getDisplayItems(stage.messages);
|
|
1163
|
+
for (const item of items) {
|
|
1164
|
+
if (item.type === "toolCall") {
|
|
1165
|
+
container.addChild(
|
|
1166
|
+
new Text(
|
|
1167
|
+
` ${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`,
|
|
1168
|
+
0,
|
|
1169
|
+
0
|
|
1170
|
+
)
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const output = getFinalOutput(stage.messages);
|
|
1175
|
+
if (output) {
|
|
1176
|
+
container.addChild(new Spacer(1));
|
|
1177
|
+
try {
|
|
1178
|
+
const { getMarkdownTheme } = __require("@mariozechner/pi-coding-agent");
|
|
1179
|
+
container.addChild(new Markdown(output.trim(), 0, 0, getMarkdownTheme()));
|
|
1180
|
+
} catch {
|
|
1181
|
+
container.addChild(new Text(theme.fg("toolOutput", output.slice(0, 500)), 0, 0));
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const usageStr = formatUsage(stage.usage, stage.model);
|
|
1185
|
+
if (usageStr) container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
1186
|
+
container.addChild(new Spacer(1));
|
|
1187
|
+
}
|
|
1188
|
+
return container;
|
|
1189
|
+
}
|
|
1190
|
+
function renderCollapsed(details, theme) {
|
|
1191
|
+
let text = theme.fg("toolTitle", theme.bold("forgeflow-dev ")) + theme.fg("accent", details.pipeline);
|
|
1192
|
+
for (const stage of details.stages) {
|
|
1193
|
+
const icon = stageIcon(stage, theme);
|
|
1194
|
+
text += `
|
|
1195
|
+
${icon} ${theme.fg("toolTitle", stage.name)}`;
|
|
1196
|
+
if (stage.status === "running") {
|
|
1197
|
+
const items = getDisplayItems(stage.messages);
|
|
1198
|
+
const last = items.filter((i) => i.type === "toolCall").slice(-3);
|
|
1199
|
+
for (const item of last) {
|
|
1200
|
+
if (item.type === "toolCall") {
|
|
1201
|
+
text += `
|
|
1202
|
+
${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
} else if (stage.status === "done" || stage.status === "failed") {
|
|
1206
|
+
const preview = stage.output.split("\n")[0]?.slice(0, 80) || "(no output)";
|
|
1207
|
+
text += theme.fg("dim", ` ${preview}`);
|
|
1208
|
+
const usageStr = formatUsage(stage.usage, stage.model);
|
|
1209
|
+
if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return new Text(text, 0, 0);
|
|
1213
|
+
}
|
|
1214
|
+
function stageIcon(stage, theme) {
|
|
1215
|
+
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");
|
|
1216
|
+
}
|
|
1217
|
+
var extension = (pi) => {
|
|
1218
|
+
registerForgeflowDevTool(pi);
|
|
1219
|
+
pi.registerCommand("implement", {
|
|
1220
|
+
description: "Implement a single issue using TDD. Usage: /implement <issue#|JIRA-KEY> [custom prompt] [--skip-plan] [--skip-review]",
|
|
1221
|
+
handler: async (args) => {
|
|
1222
|
+
const { issue, customPrompt, flags } = parseImplFlags(args);
|
|
1223
|
+
const promptPart = customPrompt ? `, customPrompt: "${customPrompt}"` : "";
|
|
1224
|
+
if (issue) {
|
|
1225
|
+
pi.sendUserMessage(
|
|
1226
|
+
`Call the forgeflow-dev tool now with these exact parameters: pipeline="implement", issue="${issue}"${promptPart}${flags}. Do not interpret the issue number \u2014 pass it as-is.`
|
|
1227
|
+
);
|
|
1228
|
+
} else {
|
|
1229
|
+
pi.sendUserMessage(
|
|
1230
|
+
`Call the forgeflow-dev tool now with these exact parameters: pipeline="implement"${promptPart}${flags}. No issue number provided \u2014 the tool will detect it from the current branch. Do NOT ask for an issue number.`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
pi.registerCommand("implement-all", {
|
|
1236
|
+
description: "Loop through all open auto-generated issues: implement, review, merge. Flags: --skip-plan, --skip-review",
|
|
1237
|
+
handler: async (args) => {
|
|
1238
|
+
const { flags } = parseImplFlags(args);
|
|
1239
|
+
pi.sendUserMessage(
|
|
1240
|
+
`Call the forgeflow-dev tool now with these exact parameters: pipeline="implement-all"${flags}. Do NOT ask for confirmation \u2014 run autonomously.`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
pi.registerCommand("review", {
|
|
1245
|
+
description: "Run code review: deterministic checks \u2192 reviewer \u2192 judge. Usage: /review [target] [custom prompt]",
|
|
1246
|
+
handler: async (args) => {
|
|
1247
|
+
const { target, customPrompt } = parseReviewArgs(args);
|
|
1248
|
+
const promptPart = customPrompt ? `, customPrompt: "${customPrompt}"` : "";
|
|
1249
|
+
pi.sendUserMessage(
|
|
1250
|
+
`Call the forgeflow-dev tool now with these exact parameters: pipeline="review"${target ? `, target="${target}"` : ""}${promptPart}. Do not interpret the target \u2014 pass it as-is.`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
pi.registerCommand("architecture", {
|
|
1255
|
+
description: "Analyze codebase for architectural friction and create RFC issues",
|
|
1256
|
+
handler: async () => {
|
|
1257
|
+
pi.sendUserMessage(`Call the forgeflow-dev tool now with these exact parameters: pipeline="architecture".`);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
pi.registerCommand("discover-skills", {
|
|
1261
|
+
description: "Find and install domain-specific plugins from skills.sh for this project's tech stack",
|
|
1262
|
+
handler: async (args) => {
|
|
1263
|
+
const query = args.trim();
|
|
1264
|
+
if (query) {
|
|
1265
|
+
pi.sendUserMessage(
|
|
1266
|
+
`Call the forgeflow-dev tool now with these exact parameters: pipeline="discover-skills", issue="${query}". Present the tool's output verbatim \u2014 do not summarize or reformat it.`
|
|
1267
|
+
);
|
|
1268
|
+
} else {
|
|
1269
|
+
pi.sendUserMessage(
|
|
1270
|
+
`Call the forgeflow-dev tool now with these exact parameters: pipeline="discover-skills". Present the tool's output verbatim \u2014 do not summarize or reformat it.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
};
|
|
1276
|
+
var index_default = extension;
|
|
1277
|
+
export {
|
|
1278
|
+
index_default as default
|
|
1279
|
+
};
|