@callumvass/forgeflow-pm 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/issue-creator.md +76 -0
- package/agents/prd-architect.md +66 -0
- package/agents/prd-critic.md +61 -0
- package/agents/prd-integrator.md +31 -0
- package/agents/single-issue-creator.md +23 -0
- package/extensions/index.js +695 -0
- package/package.json +42 -0
- package/skills/issue-template/SKILL.md +50 -0
- package/skills/prd-quality/SKILL.md +69 -0
- package/src/index.ts +280 -0
- package/src/pipelines/continue.ts +138 -0
- package/src/pipelines/create-issues.ts +45 -0
- package/src/pipelines/prd-qa.ts +88 -0
- package/src/resolve.ts +6 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,695 @@
|
|
|
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_NO_EDIT = ["read", "write", "bash", "grep", "find"];
|
|
11
|
+
var SIGNALS = {
|
|
12
|
+
questions: "QUESTIONS.md",
|
|
13
|
+
findings: "FINDINGS.md",
|
|
14
|
+
blocked: "BLOCKED.md"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ../shared/dist/run-agent.js
|
|
18
|
+
import { spawn } from "child_process";
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as os from "os";
|
|
21
|
+
import * as path from "path";
|
|
22
|
+
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
|
23
|
+
|
|
24
|
+
// ../shared/dist/types.js
|
|
25
|
+
function emptyUsage() {
|
|
26
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
27
|
+
}
|
|
28
|
+
function emptyStage(name) {
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
status: "pending",
|
|
32
|
+
messages: [],
|
|
33
|
+
exitCode: -1,
|
|
34
|
+
stderr: "",
|
|
35
|
+
output: "",
|
|
36
|
+
usage: emptyUsage()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function getFinalOutput(messages) {
|
|
40
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
41
|
+
const msg = messages[i];
|
|
42
|
+
if (msg.role === "assistant") {
|
|
43
|
+
for (const part of msg.content) {
|
|
44
|
+
if (typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
|
|
45
|
+
return part.text;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ../shared/dist/run-agent.js
|
|
54
|
+
function getPiInvocation(args) {
|
|
55
|
+
const currentScript = process.argv[1];
|
|
56
|
+
if (currentScript && fs.existsSync(currentScript)) {
|
|
57
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
58
|
+
}
|
|
59
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
60
|
+
if (!/^(node|bun)(\.exe)?$/.test(execName)) {
|
|
61
|
+
return { command: process.execPath, args };
|
|
62
|
+
}
|
|
63
|
+
return { command: "pi", args };
|
|
64
|
+
}
|
|
65
|
+
async function writePromptToTempFile(name, prompt) {
|
|
66
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "forgeflow-"));
|
|
67
|
+
const filePath = path.join(tmpDir, `prompt-${name}.md`);
|
|
68
|
+
await withFileMutationQueue(filePath, async () => {
|
|
69
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 384 });
|
|
70
|
+
});
|
|
71
|
+
return { dir: tmpDir, filePath };
|
|
72
|
+
}
|
|
73
|
+
async function runAgent(agentName, task, options) {
|
|
74
|
+
const agentPath = path.join(options.agentsDir, `${agentName}.md`);
|
|
75
|
+
const lookupName = options.stageName ?? agentName;
|
|
76
|
+
const stage = options.stages.find((s) => s.name === lookupName && s.status === "pending") ?? options.stages.find((s) => s.name === lookupName);
|
|
77
|
+
if (!stage) {
|
|
78
|
+
const s = emptyStage(agentName);
|
|
79
|
+
s.status = "failed";
|
|
80
|
+
s.output = "Stage not found in pipeline";
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
stage.status = "running";
|
|
84
|
+
emitUpdate(options);
|
|
85
|
+
const tools = options.tools ?? ["read", "write", "edit", "bash", "grep", "find"];
|
|
86
|
+
const args = ["--mode", "json", "-p", "--no-session", "--tools", tools.join(",")];
|
|
87
|
+
let tmpDir = null;
|
|
88
|
+
let tmpFile = null;
|
|
89
|
+
try {
|
|
90
|
+
const systemPrompt = fs.readFileSync(agentPath, "utf-8");
|
|
91
|
+
const tmp = await writePromptToTempFile(agentName, systemPrompt);
|
|
92
|
+
tmpDir = tmp.dir;
|
|
93
|
+
tmpFile = tmp.filePath;
|
|
94
|
+
args.push("--append-system-prompt", tmpFile);
|
|
95
|
+
args.push(`Task: ${task}`);
|
|
96
|
+
const exitCode = await new Promise((resolve2) => {
|
|
97
|
+
const invocation = getPiInvocation(args);
|
|
98
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
99
|
+
cwd: options.cwd,
|
|
100
|
+
shell: false,
|
|
101
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
102
|
+
});
|
|
103
|
+
let buffer = "";
|
|
104
|
+
const processLine = (line) => {
|
|
105
|
+
if (!line.trim())
|
|
106
|
+
return;
|
|
107
|
+
let event;
|
|
108
|
+
try {
|
|
109
|
+
event = JSON.parse(line);
|
|
110
|
+
} catch {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (event.type === "message_end" && event.message) {
|
|
114
|
+
const msg = event.message;
|
|
115
|
+
stage.messages.push(msg);
|
|
116
|
+
if (msg.role === "assistant") {
|
|
117
|
+
stage.usage.turns++;
|
|
118
|
+
const usage = msg.usage;
|
|
119
|
+
if (usage) {
|
|
120
|
+
stage.usage.input += usage.input || 0;
|
|
121
|
+
stage.usage.output += usage.output || 0;
|
|
122
|
+
stage.usage.cacheRead += usage.cacheRead || 0;
|
|
123
|
+
stage.usage.cacheWrite += usage.cacheWrite || 0;
|
|
124
|
+
stage.usage.cost += usage.cost?.total || 0;
|
|
125
|
+
}
|
|
126
|
+
if (!stage.model && msg.model)
|
|
127
|
+
stage.model = msg.model;
|
|
128
|
+
}
|
|
129
|
+
emitUpdate(options);
|
|
130
|
+
}
|
|
131
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
132
|
+
stage.messages.push(event.message);
|
|
133
|
+
emitUpdate(options);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
proc.stdout.on("data", (data) => {
|
|
137
|
+
buffer += data.toString();
|
|
138
|
+
const lines = buffer.split("\n");
|
|
139
|
+
buffer = lines.pop() || "";
|
|
140
|
+
for (const line of lines)
|
|
141
|
+
processLine(line);
|
|
142
|
+
});
|
|
143
|
+
proc.stderr.on("data", (data) => {
|
|
144
|
+
stage.stderr += data.toString();
|
|
145
|
+
});
|
|
146
|
+
proc.on("close", (code) => {
|
|
147
|
+
if (buffer.trim())
|
|
148
|
+
processLine(buffer);
|
|
149
|
+
resolve2(code ?? 0);
|
|
150
|
+
});
|
|
151
|
+
proc.on("error", () => resolve2(1));
|
|
152
|
+
if (options.signal) {
|
|
153
|
+
const kill = () => {
|
|
154
|
+
proc.kill("SIGTERM");
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
if (!proc.killed)
|
|
157
|
+
proc.kill("SIGKILL");
|
|
158
|
+
}, 5e3);
|
|
159
|
+
};
|
|
160
|
+
if (options.signal.aborted)
|
|
161
|
+
kill();
|
|
162
|
+
else
|
|
163
|
+
options.signal.addEventListener("abort", kill, { once: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
stage.exitCode = exitCode;
|
|
167
|
+
stage.status = exitCode === 0 ? "done" : "failed";
|
|
168
|
+
for (let i = stage.messages.length - 1; i >= 0; i--) {
|
|
169
|
+
const msg = stage.messages[i];
|
|
170
|
+
if (msg.role === "assistant") {
|
|
171
|
+
for (const part of msg.content) {
|
|
172
|
+
if (typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
|
|
173
|
+
stage.output = part.text;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (stage.output)
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
emitUpdate(options);
|
|
182
|
+
return stage;
|
|
183
|
+
} finally {
|
|
184
|
+
if (tmpFile)
|
|
185
|
+
try {
|
|
186
|
+
fs.unlinkSync(tmpFile);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
if (tmpDir)
|
|
190
|
+
try {
|
|
191
|
+
fs.rmdirSync(tmpDir);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function getLastToolCall(messages) {
|
|
197
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
198
|
+
const msg = messages[i];
|
|
199
|
+
if (msg.role === "assistant") {
|
|
200
|
+
const parts = msg.content;
|
|
201
|
+
for (let j = parts.length - 1; j >= 0; j--) {
|
|
202
|
+
const part = parts[j];
|
|
203
|
+
if (part?.type === "toolCall") {
|
|
204
|
+
const name = part.name;
|
|
205
|
+
const args = part.arguments ?? {};
|
|
206
|
+
switch (name) {
|
|
207
|
+
case "bash": {
|
|
208
|
+
const cmd = (args.command || "").slice(0, 60);
|
|
209
|
+
return cmd ? `$ ${cmd}` : name;
|
|
210
|
+
}
|
|
211
|
+
case "read":
|
|
212
|
+
case "write":
|
|
213
|
+
case "edit":
|
|
214
|
+
return `${name} ${args.file_path ?? args.path ?? ""}`;
|
|
215
|
+
case "grep":
|
|
216
|
+
return `grep /${args.pattern ?? ""}/`;
|
|
217
|
+
case "find":
|
|
218
|
+
return `find ${args.pattern ?? ""}`;
|
|
219
|
+
default:
|
|
220
|
+
return name;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return "";
|
|
227
|
+
}
|
|
228
|
+
function emitUpdate(options) {
|
|
229
|
+
if (!options.onUpdate)
|
|
230
|
+
return;
|
|
231
|
+
const running = options.stages.find((s) => s.status === "running");
|
|
232
|
+
let text;
|
|
233
|
+
if (running) {
|
|
234
|
+
const lastTool = getLastToolCall(running.messages);
|
|
235
|
+
text = lastTool ? `[${running.name}] ${lastTool}` : `[${running.name}] running...`;
|
|
236
|
+
} else {
|
|
237
|
+
text = options.stages.every((s) => s.status === "done") ? "Pipeline complete" : "Processing...";
|
|
238
|
+
}
|
|
239
|
+
options.onUpdate({
|
|
240
|
+
content: [{ type: "text", text }],
|
|
241
|
+
details: { pipeline: options.pipeline, stages: options.stages }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ../shared/dist/signals.js
|
|
246
|
+
import * as fs2 from "fs";
|
|
247
|
+
import * as path2 from "path";
|
|
248
|
+
function signalPath(cwd, signal) {
|
|
249
|
+
return path2.join(cwd, SIGNALS[signal]);
|
|
250
|
+
}
|
|
251
|
+
function signalExists(cwd, signal) {
|
|
252
|
+
return fs2.existsSync(signalPath(cwd, signal));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/index.ts
|
|
256
|
+
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
257
|
+
import { Type } from "@sinclair/typebox";
|
|
258
|
+
|
|
259
|
+
// src/pipelines/continue.ts
|
|
260
|
+
import * as fs3 from "fs";
|
|
261
|
+
|
|
262
|
+
// src/resolve.ts
|
|
263
|
+
import * as path3 from "path";
|
|
264
|
+
import { fileURLToPath } from "url";
|
|
265
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
266
|
+
var AGENTS_DIR = path3.resolve(__dirname, "..", "agents");
|
|
267
|
+
|
|
268
|
+
// src/pipelines/continue.ts
|
|
269
|
+
async function runContinue(cwd, description, maxIterations, signal, onUpdate, ctx) {
|
|
270
|
+
if (!fs3.existsSync(`${cwd}/PRD.md`)) {
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: "text", text: "PRD.md not found." }],
|
|
273
|
+
details: { pipeline: "continue", stages: [] }
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const stages = [];
|
|
277
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "continue", onUpdate };
|
|
278
|
+
stages.push(emptyStage("prd-architect"));
|
|
279
|
+
const updatePrompt = `You are updating a PRD for the next phase of work on an existing project.
|
|
280
|
+
|
|
281
|
+
1. Read PRD.md to understand the product spec.
|
|
282
|
+
2. Explore the codebase thoroughly \u2014 file structure, existing features, git log, tests, what's actually built.
|
|
283
|
+
3. Compare what the PRD describes vs what exists in code.
|
|
284
|
+
4. Rewrite PRD.md with this structure:
|
|
285
|
+
- Keep the Problem Statement, Goals, Tech Stack, and other top-level sections
|
|
286
|
+
- Add or update a \`## Done\` section: a concise summary of what's already built (based on your codebase exploration, not just what the PRD previously said). Keep it brief \u2014 bullet points or short paragraphs describing completed user-facing capabilities.
|
|
287
|
+
- Add or update a \`## Next\` section: the upcoming work.${description ? ` The user wants the next phase to focus on: ${description}` : ""}
|
|
288
|
+
- The \`## Next\` section should follow all PRD quality standards \u2014 user stories, functional requirements, edge cases, scope boundaries.
|
|
289
|
+
- Remove any phase markers like 'Phase 1 (Complete)' \u2014 use Done/Next instead.
|
|
290
|
+
|
|
291
|
+
5. Keep the total PRD under 200 lines. The Done section should be especially concise \u2014 it's context, not spec.
|
|
292
|
+
|
|
293
|
+
CRITICAL RULES:
|
|
294
|
+
- Do NOT include code blocks, type definitions, or implementation detail.
|
|
295
|
+
- The Done section summarizes capabilities ('users can create runs and see streaming output'), not architecture ('Hono server with SSE endpoints').
|
|
296
|
+
- The Next section must be specific enough to create vertical-slice issues from.
|
|
297
|
+
- If no description was provided for Next, infer it from the existing PRD's roadmap, scope boundaries, or TODO items.`;
|
|
298
|
+
const archResult = await runAgent("prd-architect", updatePrompt, { ...opts, tools: TOOLS_ALL });
|
|
299
|
+
if (archResult.status === "failed") {
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text", text: `PRD update failed.
|
|
302
|
+
Stderr: ${archResult.stderr.slice(0, 300)}` }],
|
|
303
|
+
details: { pipeline: "continue", stages },
|
|
304
|
+
isError: true
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (ctx.hasUI) {
|
|
308
|
+
const prdContent = fs3.readFileSync(`${cwd}/PRD.md`, "utf-8");
|
|
309
|
+
const edited = await ctx.ui.editor("Review updated PRD (Done/Next structure)", prdContent);
|
|
310
|
+
if (edited != null && edited !== prdContent) {
|
|
311
|
+
fs3.writeFileSync(`${cwd}/PRD.md`, edited, "utf-8");
|
|
312
|
+
}
|
|
313
|
+
const action = await ctx.ui.select("PRD updated with Done/Next. What next?", ["Continue to QA", "Stop here"]);
|
|
314
|
+
if (action === "Stop here" || action == null) {
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: "text", text: "PRD updated. Stopped before QA." }],
|
|
317
|
+
details: { pipeline: "continue", stages }
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (let i = 1; i <= maxIterations; i++) {
|
|
322
|
+
stages.push(emptyStage("prd-critic"));
|
|
323
|
+
const criticResult = await runAgent(
|
|
324
|
+
"prd-critic",
|
|
325
|
+
"Review PRD.md for completeness \u2014 focus on the ## Next section. If it needs refinement, create QUESTIONS.md. If it's complete, do NOT create QUESTIONS.md.",
|
|
326
|
+
{ ...opts, tools: TOOLS_NO_EDIT }
|
|
327
|
+
);
|
|
328
|
+
if (!signalExists(cwd, "questions")) {
|
|
329
|
+
if (criticResult.status === "failed") {
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text", text: `Critic failed.
|
|
332
|
+
Stderr: ${criticResult.stderr.slice(0, 300)}` }],
|
|
333
|
+
details: { pipeline: "continue", stages },
|
|
334
|
+
isError: true
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
stages.push(emptyStage("prd-architect"));
|
|
340
|
+
await runAgent(
|
|
341
|
+
"prd-architect",
|
|
342
|
+
"Read PRD.md and answer all questions in QUESTIONS.md. Write answers inline in QUESTIONS.md.",
|
|
343
|
+
{ ...opts, tools: TOOLS_ALL }
|
|
344
|
+
);
|
|
345
|
+
stages.push(emptyStage("prd-integrator"));
|
|
346
|
+
await runAgent(
|
|
347
|
+
"prd-integrator",
|
|
348
|
+
"Incorporate answers from QUESTIONS.md into PRD.md, then delete QUESTIONS.md.",
|
|
349
|
+
opts
|
|
350
|
+
);
|
|
351
|
+
if (ctx.hasUI) {
|
|
352
|
+
const prdContent = fs3.readFileSync(`${cwd}/PRD.md`, "utf-8");
|
|
353
|
+
const edited = await ctx.ui.editor(`QA iteration ${i} \u2014 Review PRD`, prdContent);
|
|
354
|
+
if (edited != null && edited !== prdContent) {
|
|
355
|
+
fs3.writeFileSync(`${cwd}/PRD.md`, edited, "utf-8");
|
|
356
|
+
}
|
|
357
|
+
const action = await ctx.ui.select("PRD updated. What next?", ["Continue refining", "Accept PRD"]);
|
|
358
|
+
if (action === "Accept PRD" || action == null) break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
stages.push(emptyStage("issue-creator"));
|
|
362
|
+
await runAgent(
|
|
363
|
+
"issue-creator",
|
|
364
|
+
"Decompose PRD.md into vertical-slice GitHub issues. Focus on the ## Next section \u2014 the ## Done section is context only. Read the issue-template skill for the standard format.",
|
|
365
|
+
{ ...opts, tools: TOOLS_NO_EDIT }
|
|
366
|
+
);
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: "text", text: "Continue pipeline complete. PRD updated, QA'd, and issues created." }],
|
|
369
|
+
details: { pipeline: "continue", stages }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/pipelines/create-issues.ts
|
|
374
|
+
import * as fs4 from "fs";
|
|
375
|
+
async function runCreateIssue(cwd, idea, signal, onUpdate, _ctx) {
|
|
376
|
+
if (!idea) {
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: "No feature idea provided." }],
|
|
379
|
+
details: { pipeline: "create-issue", stages: [] }
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const stages = [emptyStage("single-issue-creator")];
|
|
383
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "create-issue", onUpdate };
|
|
384
|
+
await runAgent("single-issue-creator", idea, { ...opts, tools: TOOLS_NO_EDIT });
|
|
385
|
+
return {
|
|
386
|
+
content: [{ type: "text", text: "Issue created." }],
|
|
387
|
+
details: { pipeline: "create-issue", stages }
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async function runCreateIssues(cwd, signal, onUpdate, _ctx) {
|
|
391
|
+
if (!fs4.existsSync(`${cwd}/PRD.md`)) {
|
|
392
|
+
return {
|
|
393
|
+
content: [{ type: "text", text: "PRD.md not found." }],
|
|
394
|
+
details: { pipeline: "create-issues", stages: [] }
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const stages = [emptyStage("issue-creator")];
|
|
398
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "create-issues", onUpdate };
|
|
399
|
+
await runAgent(
|
|
400
|
+
"issue-creator",
|
|
401
|
+
"Decompose PRD.md into vertical-slice GitHub issues. Read the issue-template skill for the standard format.",
|
|
402
|
+
{ ...opts, tools: TOOLS_NO_EDIT }
|
|
403
|
+
);
|
|
404
|
+
return {
|
|
405
|
+
content: [{ type: "text", text: "Issue creation complete." }],
|
|
406
|
+
details: { pipeline: "create-issues", stages }
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/pipelines/prd-qa.ts
|
|
411
|
+
import * as fs5 from "fs";
|
|
412
|
+
async function runPrdQa(cwd, maxIterations, signal, onUpdate, ctx) {
|
|
413
|
+
if (!fs5.existsSync(`${cwd}/PRD.md`)) {
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: "text", text: "PRD.md not found." }],
|
|
416
|
+
details: { pipeline: "prd-qa", stages: [] }
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const stages = [];
|
|
420
|
+
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "prd-qa", onUpdate };
|
|
421
|
+
for (let i = 1; i <= maxIterations; i++) {
|
|
422
|
+
stages.push(emptyStage("prd-critic"));
|
|
423
|
+
const criticResult = await runAgent(
|
|
424
|
+
"prd-critic",
|
|
425
|
+
"Review PRD.md for completeness. If it needs refinement, create QUESTIONS.md. If it's complete, do NOT create QUESTIONS.md.",
|
|
426
|
+
{ ...opts, tools: TOOLS_NO_EDIT }
|
|
427
|
+
);
|
|
428
|
+
if (!signalExists(cwd, "questions")) {
|
|
429
|
+
if (criticResult.status === "failed") {
|
|
430
|
+
return {
|
|
431
|
+
content: [{ type: "text", text: `Critic failed.
|
|
432
|
+
Stderr: ${criticResult.stderr.slice(0, 300)}` }],
|
|
433
|
+
details: { pipeline: "prd-qa", stages },
|
|
434
|
+
isError: true
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: "text", text: "PRD refinement complete. Ready for /create-issues." }],
|
|
439
|
+
details: { pipeline: "prd-qa", stages }
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
stages.push(emptyStage("prd-architect"));
|
|
443
|
+
await runAgent(
|
|
444
|
+
"prd-architect",
|
|
445
|
+
"Read PRD.md and answer all questions in QUESTIONS.md. Write answers inline in QUESTIONS.md.",
|
|
446
|
+
{ ...opts, tools: TOOLS_ALL }
|
|
447
|
+
);
|
|
448
|
+
stages.push(emptyStage("prd-integrator"));
|
|
449
|
+
await runAgent(
|
|
450
|
+
"prd-integrator",
|
|
451
|
+
"Incorporate answers from QUESTIONS.md into PRD.md, then delete QUESTIONS.md.",
|
|
452
|
+
opts
|
|
453
|
+
);
|
|
454
|
+
if (ctx.hasUI) {
|
|
455
|
+
const prdContent = fs5.readFileSync(`${cwd}/PRD.md`, "utf-8");
|
|
456
|
+
const edited = await ctx.ui.editor(`Iteration ${i} \u2014 Review PRD (edit or close to continue)`, prdContent);
|
|
457
|
+
if (edited != null && edited !== prdContent) {
|
|
458
|
+
fs5.writeFileSync(`${cwd}/PRD.md`, edited, "utf-8");
|
|
459
|
+
}
|
|
460
|
+
const action = await ctx.ui.select("PRD updated. What next?", ["Continue refining", "Accept PRD"]);
|
|
461
|
+
if (action === "Accept PRD" || action == null) {
|
|
462
|
+
return {
|
|
463
|
+
content: [{ type: "text", text: "PRD accepted." }],
|
|
464
|
+
details: { pipeline: "prd-qa", stages }
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text: `PRD refinement did not complete after ${maxIterations} iterations.` }],
|
|
471
|
+
details: { pipeline: "prd-qa", stages }
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/index.ts
|
|
476
|
+
function getDisplayItems(messages) {
|
|
477
|
+
const items = [];
|
|
478
|
+
for (const msg of messages) {
|
|
479
|
+
if (msg.role === "assistant") {
|
|
480
|
+
for (const part of msg.content) {
|
|
481
|
+
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
482
|
+
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return items;
|
|
487
|
+
}
|
|
488
|
+
function formatToolCallShort(name, args, fg) {
|
|
489
|
+
switch (name) {
|
|
490
|
+
case "bash": {
|
|
491
|
+
const cmd = args.command || "...";
|
|
492
|
+
return fg("muted", "$ ") + fg("toolOutput", cmd.length > 60 ? `${cmd.slice(0, 60)}...` : cmd);
|
|
493
|
+
}
|
|
494
|
+
case "read":
|
|
495
|
+
return fg("muted", "read ") + fg("accent", args.file_path || args.path || "...");
|
|
496
|
+
case "write":
|
|
497
|
+
return fg("muted", "write ") + fg("accent", args.file_path || args.path || "...");
|
|
498
|
+
case "edit":
|
|
499
|
+
return fg("muted", "edit ") + fg("accent", args.file_path || args.path || "...");
|
|
500
|
+
case "grep":
|
|
501
|
+
return fg("muted", "grep ") + fg("accent", `/${args.pattern || ""}/`);
|
|
502
|
+
case "find":
|
|
503
|
+
return fg("muted", "find ") + fg("accent", args.pattern || "*");
|
|
504
|
+
default:
|
|
505
|
+
return fg("accent", name);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function formatUsage(usage, model) {
|
|
509
|
+
const parts = [];
|
|
510
|
+
if (usage.turns) parts.push(`${usage.turns}t`);
|
|
511
|
+
if (usage.input) parts.push(`\u2191${usage.input < 1e3 ? usage.input : `${Math.round(usage.input / 1e3)}k`}`);
|
|
512
|
+
if (usage.output) parts.push(`\u2193${usage.output < 1e3 ? usage.output : `${Math.round(usage.output / 1e3)}k`}`);
|
|
513
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
514
|
+
if (model) parts.push(model);
|
|
515
|
+
return parts.join(" ");
|
|
516
|
+
}
|
|
517
|
+
var ForgeflowPmParams = Type.Object({
|
|
518
|
+
pipeline: Type.String({
|
|
519
|
+
description: 'Which pipeline to run: "continue", "prd-qa", "create-issues", or "create-issue"'
|
|
520
|
+
}),
|
|
521
|
+
maxIterations: Type.Optional(Type.Number({ description: "Max iterations for prd-qa (default 10)" })),
|
|
522
|
+
issue: Type.Optional(Type.String({ description: "Feature idea for create-issue, or description for continue" }))
|
|
523
|
+
});
|
|
524
|
+
function registerForgeflowPmTool(pi) {
|
|
525
|
+
pi.registerTool({
|
|
526
|
+
name: "forgeflow-pm",
|
|
527
|
+
label: "Forgeflow PM",
|
|
528
|
+
description: [
|
|
529
|
+
"Run forgeflow PM pipelines: continue (update PRD Done/Next\u2192QA\u2192create issues for next phase),",
|
|
530
|
+
"prd-qa (refine PRD), create-issues (decompose PRD into GitHub issues),",
|
|
531
|
+
"create-issue (single issue from a feature idea).",
|
|
532
|
+
"Each pipeline spawns specialized sub-agents with isolated context."
|
|
533
|
+
].join(" "),
|
|
534
|
+
parameters: ForgeflowPmParams,
|
|
535
|
+
async execute(_toolCallId, _params, signal, onUpdate, ctx) {
|
|
536
|
+
const params = _params;
|
|
537
|
+
const cwd = ctx.cwd;
|
|
538
|
+
const sig = signal ?? new AbortController().signal;
|
|
539
|
+
try {
|
|
540
|
+
switch (params.pipeline) {
|
|
541
|
+
case "continue":
|
|
542
|
+
return await runContinue(cwd, params.issue ?? "", params.maxIterations ?? 10, sig, onUpdate, ctx);
|
|
543
|
+
case "prd-qa":
|
|
544
|
+
return await runPrdQa(cwd, params.maxIterations ?? 10, sig, onUpdate, ctx);
|
|
545
|
+
case "create-issues":
|
|
546
|
+
return await runCreateIssues(cwd, sig, onUpdate, ctx);
|
|
547
|
+
case "create-issue":
|
|
548
|
+
return await runCreateIssue(cwd, params.issue ?? "", sig, onUpdate, ctx);
|
|
549
|
+
default:
|
|
550
|
+
return {
|
|
551
|
+
content: [
|
|
552
|
+
{
|
|
553
|
+
type: "text",
|
|
554
|
+
text: `Unknown pipeline: ${params.pipeline}. Use: continue, prd-qa, create-issues, create-issue`
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
details: { pipeline: params.pipeline, stages: [] }
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
} finally {
|
|
561
|
+
if (ctx.hasUI) {
|
|
562
|
+
ctx.ui.setStatus("forgeflow-pm", void 0);
|
|
563
|
+
ctx.ui.setWidget("forgeflow-pm", void 0);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
renderCall(_args, theme) {
|
|
568
|
+
const args = _args;
|
|
569
|
+
const pipeline = args.pipeline || "?";
|
|
570
|
+
let text = theme.fg("toolTitle", theme.bold("forgeflow-pm ")) + theme.fg("accent", pipeline);
|
|
571
|
+
if (args.issue) text += theme.fg("dim", ` "${args.issue}"`);
|
|
572
|
+
if (args.maxIterations) text += theme.fg("muted", ` (max ${args.maxIterations})`);
|
|
573
|
+
return new Text(text, 0, 0);
|
|
574
|
+
},
|
|
575
|
+
renderResult(result, { expanded }, theme) {
|
|
576
|
+
const details = result.details;
|
|
577
|
+
if (!details || details.stages.length === 0) {
|
|
578
|
+
const text = result.content[0];
|
|
579
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
580
|
+
}
|
|
581
|
+
if (expanded) {
|
|
582
|
+
return renderExpanded(details, theme);
|
|
583
|
+
}
|
|
584
|
+
return renderCollapsed(details, theme);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function renderExpanded(details, theme) {
|
|
589
|
+
const container = new Container();
|
|
590
|
+
container.addChild(
|
|
591
|
+
new Text(theme.fg("toolTitle", theme.bold("forgeflow-pm ")) + theme.fg("accent", details.pipeline), 0, 0)
|
|
592
|
+
);
|
|
593
|
+
container.addChild(new Spacer(1));
|
|
594
|
+
for (const stage of details.stages) {
|
|
595
|
+
const icon = stageIcon(stage, theme);
|
|
596
|
+
container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(stage.name))}`, 0, 0));
|
|
597
|
+
const items = getDisplayItems(stage.messages);
|
|
598
|
+
for (const item of items) {
|
|
599
|
+
if (item.type === "toolCall") {
|
|
600
|
+
container.addChild(
|
|
601
|
+
new Text(
|
|
602
|
+
` ${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`,
|
|
603
|
+
0,
|
|
604
|
+
0
|
|
605
|
+
)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const output = getFinalOutput(stage.messages);
|
|
610
|
+
if (output) {
|
|
611
|
+
container.addChild(new Spacer(1));
|
|
612
|
+
try {
|
|
613
|
+
const { getMarkdownTheme } = __require("@mariozechner/pi-coding-agent");
|
|
614
|
+
container.addChild(new Markdown(output.trim(), 0, 0, getMarkdownTheme()));
|
|
615
|
+
} catch {
|
|
616
|
+
container.addChild(new Text(theme.fg("toolOutput", output.slice(0, 500)), 0, 0));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const usageStr = formatUsage(stage.usage, stage.model);
|
|
620
|
+
if (usageStr) container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
621
|
+
container.addChild(new Spacer(1));
|
|
622
|
+
}
|
|
623
|
+
return container;
|
|
624
|
+
}
|
|
625
|
+
function renderCollapsed(details, theme) {
|
|
626
|
+
let text = theme.fg("toolTitle", theme.bold("forgeflow-pm ")) + theme.fg("accent", details.pipeline);
|
|
627
|
+
for (const stage of details.stages) {
|
|
628
|
+
const icon = stageIcon(stage, theme);
|
|
629
|
+
text += `
|
|
630
|
+
${icon} ${theme.fg("toolTitle", stage.name)}`;
|
|
631
|
+
if (stage.status === "running") {
|
|
632
|
+
const items = getDisplayItems(stage.messages);
|
|
633
|
+
const last = items.filter((i) => i.type === "toolCall").slice(-3);
|
|
634
|
+
for (const item of last) {
|
|
635
|
+
if (item.type === "toolCall") {
|
|
636
|
+
text += `
|
|
637
|
+
${theme.fg("muted", "\u2192 ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
} else if (stage.status === "done" || stage.status === "failed") {
|
|
641
|
+
const preview = stage.output.split("\n")[0]?.slice(0, 80) || "(no output)";
|
|
642
|
+
text += theme.fg("dim", ` ${preview}`);
|
|
643
|
+
const usageStr = formatUsage(stage.usage, stage.model);
|
|
644
|
+
if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return new Text(text, 0, 0);
|
|
648
|
+
}
|
|
649
|
+
function stageIcon(stage, theme) {
|
|
650
|
+
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");
|
|
651
|
+
}
|
|
652
|
+
var extension = (pi) => {
|
|
653
|
+
registerForgeflowPmTool(pi);
|
|
654
|
+
pi.registerCommand("continue", {
|
|
655
|
+
description: 'Update PRD with Done/Next based on codebase state, QA the Next section, then create issues. Usage: /continue ["description of next phase"]',
|
|
656
|
+
handler: async (args) => {
|
|
657
|
+
const trimmed = args.trim().replace(/^"(.*)"$/, "$1");
|
|
658
|
+
const descPart = trimmed ? `, issue="${trimmed}"` : "";
|
|
659
|
+
pi.sendUserMessage(
|
|
660
|
+
`Call the forgeflow-pm tool now with these exact parameters: pipeline="continue"${descPart}. Do not interpret the description \u2014 pass it as-is.`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
pi.registerCommand("prd-qa", {
|
|
665
|
+
description: "Refine PRD.md via critic \u2192 architect \u2192 integrator loop",
|
|
666
|
+
handler: async (args) => {
|
|
667
|
+
const maxIter = parseInt(args, 10) || 10;
|
|
668
|
+
pi.sendUserMessage(
|
|
669
|
+
`Call the forgeflow-pm tool now with these exact parameters: pipeline="prd-qa", maxIterations=${maxIter}.`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
pi.registerCommand("create-issues", {
|
|
674
|
+
description: "Decompose PRD.md into vertical-slice GitHub issues",
|
|
675
|
+
handler: async () => {
|
|
676
|
+
pi.sendUserMessage(`Call the forgeflow-pm tool now with these exact parameters: pipeline="create-issues".`);
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
pi.registerCommand("create-issue", {
|
|
680
|
+
description: "Create a single GitHub issue from a feature idea",
|
|
681
|
+
handler: async (args) => {
|
|
682
|
+
if (!args.trim()) {
|
|
683
|
+
pi.sendUserMessage('I need a feature idea. Usage: /create-issue "Add user authentication"');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
pi.sendUserMessage(
|
|
687
|
+
`Call the forgeflow-pm tool now with these exact parameters: pipeline="create-issue", issue="${args.trim()}". Do not interpret the issue text \u2014 pass it as-is.`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
var index_default = extension;
|
|
693
|
+
export {
|
|
694
|
+
index_default as default
|
|
695
|
+
};
|