@callumvass/forgeflow-pm 0.1.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/agents/{issue-creator.md → gh-issue-creator.md} +1 -1
- package/agents/{single-issue-creator.md → gh-single-issue-creator.md} +1 -1
- package/agents/investigator.md +32 -0
- package/agents/jira-issue-creator.md +43 -0
- package/extensions/index.js +251 -25
- package/package.json +9 -3
- package/skills/writing-style/SKILL.md +33 -0
- package/src/index.ts +0 -280
- package/src/pipelines/continue.ts +0 -138
- package/src/pipelines/create-issues.ts +0 -45
- package/src/pipelines/prd-qa.ts +0 -88
- package/src/resolve.ts +0 -6
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- package/tsup.config.ts +0 -15
package/src/index.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { type AnyCtx, getFinalOutput, type PipelineDetails, type StageResult } from "@callumvass/forgeflow-shared";
|
|
2
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
4
|
-
import { Type } from "@sinclair/typebox";
|
|
5
|
-
import { runContinue } from "./pipelines/continue.js";
|
|
6
|
-
import { runCreateIssue, runCreateIssues } from "./pipelines/create-issues.js";
|
|
7
|
-
import { runPrdQa } from "./pipelines/prd-qa.js";
|
|
8
|
-
|
|
9
|
-
// ─── Display helpers ──────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
12
|
-
|
|
13
|
-
interface ForgeflowPmInput {
|
|
14
|
-
pipeline: string;
|
|
15
|
-
maxIterations?: number;
|
|
16
|
-
issue?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function getDisplayItems(messages: AnyCtx[]): DisplayItem[] {
|
|
20
|
-
const items: DisplayItem[] = [];
|
|
21
|
-
for (const msg of messages) {
|
|
22
|
-
if (msg.role === "assistant") {
|
|
23
|
-
for (const part of msg.content) {
|
|
24
|
-
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
25
|
-
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return items;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatToolCallShort(
|
|
33
|
-
name: string,
|
|
34
|
-
args: Record<string, unknown>,
|
|
35
|
-
fg: (c: string, t: string) => string,
|
|
36
|
-
): string {
|
|
37
|
-
switch (name) {
|
|
38
|
-
case "bash": {
|
|
39
|
-
const cmd = (args.command as string) || "...";
|
|
40
|
-
return fg("muted", "$ ") + fg("toolOutput", cmd.length > 60 ? `${cmd.slice(0, 60)}...` : cmd);
|
|
41
|
-
}
|
|
42
|
-
case "read":
|
|
43
|
-
return fg("muted", "read ") + fg("accent", (args.file_path || args.path || "...") as string);
|
|
44
|
-
case "write":
|
|
45
|
-
return fg("muted", "write ") + fg("accent", (args.file_path || args.path || "...") as string);
|
|
46
|
-
case "edit":
|
|
47
|
-
return fg("muted", "edit ") + fg("accent", (args.file_path || args.path || "...") as string);
|
|
48
|
-
case "grep":
|
|
49
|
-
return fg("muted", "grep ") + fg("accent", `/${args.pattern || ""}/`);
|
|
50
|
-
case "find":
|
|
51
|
-
return fg("muted", "find ") + fg("accent", (args.pattern || "*") as string);
|
|
52
|
-
default:
|
|
53
|
-
return fg("accent", name);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function formatUsage(usage: { input: number; output: number; cost: number; turns: number }, model?: string): string {
|
|
58
|
-
const parts: string[] = [];
|
|
59
|
-
if (usage.turns) parts.push(`${usage.turns}t`);
|
|
60
|
-
if (usage.input) parts.push(`↑${usage.input < 1000 ? usage.input : `${Math.round(usage.input / 1000)}k`}`);
|
|
61
|
-
if (usage.output) parts.push(`↓${usage.output < 1000 ? usage.output : `${Math.round(usage.output / 1000)}k`}`);
|
|
62
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
63
|
-
if (model) parts.push(model);
|
|
64
|
-
return parts.join(" ");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ─── Tool registration ────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
const ForgeflowPmParams = Type.Object({
|
|
70
|
-
pipeline: Type.String({
|
|
71
|
-
description: 'Which pipeline to run: "continue", "prd-qa", "create-issues", or "create-issue"',
|
|
72
|
-
}),
|
|
73
|
-
maxIterations: Type.Optional(Type.Number({ description: "Max iterations for prd-qa (default 10)" })),
|
|
74
|
-
issue: Type.Optional(Type.String({ description: "Feature idea for create-issue, or description for continue" })),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
function registerForgeflowPmTool(pi: ExtensionAPI) {
|
|
78
|
-
pi.registerTool({
|
|
79
|
-
name: "forgeflow-pm",
|
|
80
|
-
label: "Forgeflow PM",
|
|
81
|
-
description: [
|
|
82
|
-
"Run forgeflow PM pipelines: continue (update PRD Done/Next→QA→create issues for next phase),",
|
|
83
|
-
"prd-qa (refine PRD), create-issues (decompose PRD into GitHub issues),",
|
|
84
|
-
"create-issue (single issue from a feature idea).",
|
|
85
|
-
"Each pipeline spawns specialized sub-agents with isolated context.",
|
|
86
|
-
].join(" "),
|
|
87
|
-
parameters: ForgeflowPmParams as AnyCtx,
|
|
88
|
-
|
|
89
|
-
async execute(
|
|
90
|
-
_toolCallId: string,
|
|
91
|
-
_params: unknown,
|
|
92
|
-
signal: AbortSignal | undefined,
|
|
93
|
-
onUpdate: AnyCtx,
|
|
94
|
-
ctx: AnyCtx,
|
|
95
|
-
) {
|
|
96
|
-
const params = _params as ForgeflowPmInput;
|
|
97
|
-
const cwd = ctx.cwd as string;
|
|
98
|
-
const sig = signal ?? new AbortController().signal;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
switch (params.pipeline) {
|
|
102
|
-
case "continue":
|
|
103
|
-
return await runContinue(cwd, params.issue ?? "", params.maxIterations ?? 10, sig, onUpdate, ctx);
|
|
104
|
-
case "prd-qa":
|
|
105
|
-
return await runPrdQa(cwd, params.maxIterations ?? 10, sig, onUpdate, ctx);
|
|
106
|
-
case "create-issues":
|
|
107
|
-
return await runCreateIssues(cwd, sig, onUpdate, ctx);
|
|
108
|
-
case "create-issue":
|
|
109
|
-
return await runCreateIssue(cwd, params.issue ?? "", sig, onUpdate, ctx);
|
|
110
|
-
default:
|
|
111
|
-
return {
|
|
112
|
-
content: [
|
|
113
|
-
{
|
|
114
|
-
type: "text",
|
|
115
|
-
text: `Unknown pipeline: ${params.pipeline}. Use: continue, prd-qa, create-issues, create-issue`,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
details: { pipeline: params.pipeline, stages: [] } as PipelineDetails,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
} finally {
|
|
122
|
-
if (ctx.hasUI) {
|
|
123
|
-
ctx.ui.setStatus("forgeflow-pm", undefined);
|
|
124
|
-
ctx.ui.setWidget("forgeflow-pm", undefined);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
renderCall(_args: unknown, theme: AnyCtx) {
|
|
130
|
-
const args = _args as ForgeflowPmInput;
|
|
131
|
-
const pipeline = args.pipeline || "?";
|
|
132
|
-
let text = theme.fg("toolTitle", theme.bold("forgeflow-pm ")) + theme.fg("accent", pipeline);
|
|
133
|
-
if (args.issue) text += theme.fg("dim", ` "${args.issue}"`);
|
|
134
|
-
if (args.maxIterations) text += theme.fg("muted", ` (max ${args.maxIterations})`);
|
|
135
|
-
return new Text(text, 0, 0);
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
renderResult(result: AnyCtx, { expanded }: { expanded: boolean }, theme: AnyCtx) {
|
|
139
|
-
const details = result.details as PipelineDetails | undefined;
|
|
140
|
-
if (!details || details.stages.length === 0) {
|
|
141
|
-
const text = result.content[0];
|
|
142
|
-
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (expanded) {
|
|
146
|
-
return renderExpanded(details, theme);
|
|
147
|
-
}
|
|
148
|
-
return renderCollapsed(details, theme);
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── Rendering ────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
function renderExpanded(details: PipelineDetails, theme: AnyCtx) {
|
|
156
|
-
const container = new Container();
|
|
157
|
-
container.addChild(
|
|
158
|
-
new Text(theme.fg("toolTitle", theme.bold("forgeflow-pm ")) + theme.fg("accent", details.pipeline), 0, 0),
|
|
159
|
-
);
|
|
160
|
-
container.addChild(new Spacer(1));
|
|
161
|
-
|
|
162
|
-
for (const stage of details.stages) {
|
|
163
|
-
const icon = stageIcon(stage, theme);
|
|
164
|
-
container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(stage.name))}`, 0, 0));
|
|
165
|
-
|
|
166
|
-
const items = getDisplayItems(stage.messages);
|
|
167
|
-
for (const item of items) {
|
|
168
|
-
if (item.type === "toolCall") {
|
|
169
|
-
container.addChild(
|
|
170
|
-
new Text(
|
|
171
|
-
` ${theme.fg("muted", "→ ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`,
|
|
172
|
-
0,
|
|
173
|
-
0,
|
|
174
|
-
),
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const output = getFinalOutput(stage.messages);
|
|
180
|
-
if (output) {
|
|
181
|
-
container.addChild(new Spacer(1));
|
|
182
|
-
try {
|
|
183
|
-
const { getMarkdownTheme } = require("@mariozechner/pi-coding-agent");
|
|
184
|
-
container.addChild(new Markdown(output.trim(), 0, 0, getMarkdownTheme()));
|
|
185
|
-
} catch {
|
|
186
|
-
container.addChild(new Text(theme.fg("toolOutput", output.slice(0, 500)), 0, 0));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const usageStr = formatUsage(stage.usage, stage.model);
|
|
191
|
-
if (usageStr) container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
192
|
-
container.addChild(new Spacer(1));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return container;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function renderCollapsed(details: PipelineDetails, theme: AnyCtx) {
|
|
199
|
-
let text = theme.fg("toolTitle", theme.bold("forgeflow-pm ")) + theme.fg("accent", details.pipeline);
|
|
200
|
-
for (const stage of details.stages) {
|
|
201
|
-
const icon = stageIcon(stage, theme);
|
|
202
|
-
text += `\n ${icon} ${theme.fg("toolTitle", stage.name)}`;
|
|
203
|
-
|
|
204
|
-
if (stage.status === "running") {
|
|
205
|
-
const items = getDisplayItems(stage.messages);
|
|
206
|
-
const last = items.filter((i) => i.type === "toolCall").slice(-3);
|
|
207
|
-
for (const item of last) {
|
|
208
|
-
if (item.type === "toolCall") {
|
|
209
|
-
text += `\n ${theme.fg("muted", "→ ")}${formatToolCallShort(item.name, item.args, theme.fg.bind(theme))}`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} else if (stage.status === "done" || stage.status === "failed") {
|
|
213
|
-
const preview = stage.output.split("\n")[0]?.slice(0, 80) || "(no output)";
|
|
214
|
-
text += theme.fg("dim", ` ${preview}`);
|
|
215
|
-
const usageStr = formatUsage(stage.usage, stage.model);
|
|
216
|
-
if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return new Text(text, 0, 0);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function stageIcon(stage: StageResult, theme: AnyCtx): string {
|
|
223
|
-
return stage.status === "done"
|
|
224
|
-
? theme.fg("success", "✓")
|
|
225
|
-
: stage.status === "running"
|
|
226
|
-
? theme.fg("warning", "⟳")
|
|
227
|
-
: stage.status === "failed"
|
|
228
|
-
? theme.fg("error", "✗")
|
|
229
|
-
: theme.fg("muted", "○");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ─── Extension entry point ────────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
const extension: (pi: ExtensionAPI) => void = (pi) => {
|
|
235
|
-
registerForgeflowPmTool(pi);
|
|
236
|
-
|
|
237
|
-
pi.registerCommand("continue", {
|
|
238
|
-
description:
|
|
239
|
-
'Update PRD with Done/Next based on codebase state, QA the Next section, then create issues. Usage: /continue ["description of next phase"]',
|
|
240
|
-
handler: async (args) => {
|
|
241
|
-
const trimmed = args.trim().replace(/^"(.*)"$/, "$1");
|
|
242
|
-
const descPart = trimmed ? `, issue="${trimmed}"` : "";
|
|
243
|
-
pi.sendUserMessage(
|
|
244
|
-
`Call the forgeflow-pm tool now with these exact parameters: pipeline="continue"${descPart}. Do not interpret the description — pass it as-is.`,
|
|
245
|
-
);
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
pi.registerCommand("prd-qa", {
|
|
250
|
-
description: "Refine PRD.md via critic → architect → integrator loop",
|
|
251
|
-
handler: async (args) => {
|
|
252
|
-
const maxIter = parseInt(args, 10) || 10;
|
|
253
|
-
pi.sendUserMessage(
|
|
254
|
-
`Call the forgeflow-pm tool now with these exact parameters: pipeline="prd-qa", maxIterations=${maxIter}.`,
|
|
255
|
-
);
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
pi.registerCommand("create-issues", {
|
|
260
|
-
description: "Decompose PRD.md into vertical-slice GitHub issues",
|
|
261
|
-
handler: async () => {
|
|
262
|
-
pi.sendUserMessage(`Call the forgeflow-pm tool now with these exact parameters: pipeline="create-issues".`);
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
pi.registerCommand("create-issue", {
|
|
267
|
-
description: "Create a single GitHub issue from a feature idea",
|
|
268
|
-
handler: async (args) => {
|
|
269
|
-
if (!args.trim()) {
|
|
270
|
-
pi.sendUserMessage('I need a feature idea. Usage: /create-issue "Add user authentication"');
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
pi.sendUserMessage(
|
|
274
|
-
`Call the forgeflow-pm tool now with these exact parameters: pipeline="create-issue", issue="${args.trim()}". Do not interpret the issue text — pass it as-is.`,
|
|
275
|
-
);
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
export default extension;
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
type AnyCtx,
|
|
4
|
-
emptyStage,
|
|
5
|
-
runAgent,
|
|
6
|
-
type StageResult,
|
|
7
|
-
signalExists,
|
|
8
|
-
TOOLS_ALL,
|
|
9
|
-
TOOLS_NO_EDIT,
|
|
10
|
-
} from "@callumvass/forgeflow-shared";
|
|
11
|
-
import { AGENTS_DIR } from "../resolve.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Continue pipeline: update PRD with Done/Next, run QA loop, create issues.
|
|
15
|
-
*/
|
|
16
|
-
export async function runContinue(
|
|
17
|
-
cwd: string,
|
|
18
|
-
description: string,
|
|
19
|
-
maxIterations: number,
|
|
20
|
-
signal: AbortSignal,
|
|
21
|
-
onUpdate: AnyCtx,
|
|
22
|
-
ctx: AnyCtx,
|
|
23
|
-
) {
|
|
24
|
-
if (!fs.existsSync(`${cwd}/PRD.md`)) {
|
|
25
|
-
return {
|
|
26
|
-
content: [{ type: "text" as const, text: "PRD.md not found." }],
|
|
27
|
-
details: { pipeline: "continue", stages: [] },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const stages: StageResult[] = [];
|
|
32
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "continue", onUpdate };
|
|
33
|
-
|
|
34
|
-
// Phase 1: Update PRD with Done/Next structure
|
|
35
|
-
stages.push(emptyStage("prd-architect"));
|
|
36
|
-
const updatePrompt = `You are updating a PRD for the next phase of work on an existing project.
|
|
37
|
-
|
|
38
|
-
1. Read PRD.md to understand the product spec.
|
|
39
|
-
2. Explore the codebase thoroughly — file structure, existing features, git log, tests, what's actually built.
|
|
40
|
-
3. Compare what the PRD describes vs what exists in code.
|
|
41
|
-
4. Rewrite PRD.md with this structure:
|
|
42
|
-
- Keep the Problem Statement, Goals, Tech Stack, and other top-level sections
|
|
43
|
-
- 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 — bullet points or short paragraphs describing completed user-facing capabilities.
|
|
44
|
-
- Add or update a \`## Next\` section: the upcoming work.${description ? ` The user wants the next phase to focus on: ${description}` : ""}
|
|
45
|
-
- The \`## Next\` section should follow all PRD quality standards — user stories, functional requirements, edge cases, scope boundaries.
|
|
46
|
-
- Remove any phase markers like 'Phase 1 (Complete)' — use Done/Next instead.
|
|
47
|
-
|
|
48
|
-
5. Keep the total PRD under 200 lines. The Done section should be especially concise — it's context, not spec.
|
|
49
|
-
|
|
50
|
-
CRITICAL RULES:
|
|
51
|
-
- Do NOT include code blocks, type definitions, or implementation detail.
|
|
52
|
-
- The Done section summarizes capabilities ('users can create runs and see streaming output'), not architecture ('Hono server with SSE endpoints').
|
|
53
|
-
- The Next section must be specific enough to create vertical-slice issues from.
|
|
54
|
-
- If no description was provided for Next, infer it from the existing PRD's roadmap, scope boundaries, or TODO items.`;
|
|
55
|
-
|
|
56
|
-
const archResult = await runAgent("prd-architect", updatePrompt, { ...opts, tools: TOOLS_ALL });
|
|
57
|
-
if (archResult.status === "failed") {
|
|
58
|
-
return {
|
|
59
|
-
content: [{ type: "text" as const, text: `PRD update failed.\nStderr: ${archResult.stderr.slice(0, 300)}` }],
|
|
60
|
-
details: { pipeline: "continue", stages },
|
|
61
|
-
isError: true,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Approval gate after PRD update
|
|
66
|
-
if (ctx.hasUI) {
|
|
67
|
-
const prdContent = fs.readFileSync(`${cwd}/PRD.md`, "utf-8");
|
|
68
|
-
const edited = await ctx.ui.editor("Review updated PRD (Done/Next structure)", prdContent);
|
|
69
|
-
if (edited != null && edited !== prdContent) {
|
|
70
|
-
fs.writeFileSync(`${cwd}/PRD.md`, edited, "utf-8");
|
|
71
|
-
}
|
|
72
|
-
const action = await ctx.ui.select("PRD updated with Done/Next. What next?", ["Continue to QA", "Stop here"]);
|
|
73
|
-
if (action === "Stop here" || action == null) {
|
|
74
|
-
return {
|
|
75
|
-
content: [{ type: "text" as const, text: "PRD updated. Stopped before QA." }],
|
|
76
|
-
details: { pipeline: "continue", stages },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Phase 2: PRD QA loop on the Next section
|
|
82
|
-
for (let i = 1; i <= maxIterations; i++) {
|
|
83
|
-
stages.push(emptyStage("prd-critic"));
|
|
84
|
-
const criticResult = await runAgent(
|
|
85
|
-
"prd-critic",
|
|
86
|
-
"Review PRD.md for completeness — focus on the ## Next section. If it needs refinement, create QUESTIONS.md. If it's complete, do NOT create QUESTIONS.md.",
|
|
87
|
-
{ ...opts, tools: TOOLS_NO_EDIT },
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (!signalExists(cwd, "questions")) {
|
|
91
|
-
if (criticResult.status === "failed") {
|
|
92
|
-
return {
|
|
93
|
-
content: [{ type: "text" as const, text: `Critic failed.\nStderr: ${criticResult.stderr.slice(0, 300)}` }],
|
|
94
|
-
details: { pipeline: "continue", stages },
|
|
95
|
-
isError: true,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
stages.push(emptyStage("prd-architect"));
|
|
102
|
-
await runAgent(
|
|
103
|
-
"prd-architect",
|
|
104
|
-
"Read PRD.md and answer all questions in QUESTIONS.md. Write answers inline in QUESTIONS.md.",
|
|
105
|
-
{ ...opts, tools: TOOLS_ALL },
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
stages.push(emptyStage("prd-integrator"));
|
|
109
|
-
await runAgent(
|
|
110
|
-
"prd-integrator",
|
|
111
|
-
"Incorporate answers from QUESTIONS.md into PRD.md, then delete QUESTIONS.md.",
|
|
112
|
-
opts,
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
if (ctx.hasUI) {
|
|
116
|
-
const prdContent = fs.readFileSync(`${cwd}/PRD.md`, "utf-8");
|
|
117
|
-
const edited = await ctx.ui.editor(`QA iteration ${i} — Review PRD`, prdContent);
|
|
118
|
-
if (edited != null && edited !== prdContent) {
|
|
119
|
-
fs.writeFileSync(`${cwd}/PRD.md`, edited, "utf-8");
|
|
120
|
-
}
|
|
121
|
-
const action = await ctx.ui.select("PRD updated. What next?", ["Continue refining", "Accept PRD"]);
|
|
122
|
-
if (action === "Accept PRD" || action == null) break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Phase 3: Create issues from the Next section
|
|
127
|
-
stages.push(emptyStage("issue-creator"));
|
|
128
|
-
await runAgent(
|
|
129
|
-
"issue-creator",
|
|
130
|
-
"Decompose PRD.md into vertical-slice GitHub issues. Focus on the ## Next section — the ## Done section is context only. Read the issue-template skill for the standard format.",
|
|
131
|
-
{ ...opts, tools: TOOLS_NO_EDIT },
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
content: [{ type: "text" as const, text: "Continue pipeline complete. PRD updated, QA'd, and issues created." }],
|
|
136
|
-
details: { pipeline: "continue", stages },
|
|
137
|
-
};
|
|
138
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { type AnyCtx, emptyStage, runAgent, TOOLS_NO_EDIT } from "@callumvass/forgeflow-shared";
|
|
3
|
-
import { AGENTS_DIR } from "../resolve.js";
|
|
4
|
-
|
|
5
|
-
export async function runCreateIssue(cwd: string, idea: string, signal: AbortSignal, onUpdate: AnyCtx, _ctx: AnyCtx) {
|
|
6
|
-
if (!idea) {
|
|
7
|
-
return {
|
|
8
|
-
content: [{ type: "text" as const, text: "No feature idea provided." }],
|
|
9
|
-
details: { pipeline: "create-issue", stages: [] },
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const stages = [emptyStage("single-issue-creator")];
|
|
14
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "create-issue", onUpdate };
|
|
15
|
-
|
|
16
|
-
await runAgent("single-issue-creator", idea, { ...opts, tools: TOOLS_NO_EDIT });
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
content: [{ type: "text" as const, text: "Issue created." }],
|
|
20
|
-
details: { pipeline: "create-issue", stages },
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function runCreateIssues(cwd: string, signal: AbortSignal, onUpdate: AnyCtx, _ctx: AnyCtx) {
|
|
25
|
-
if (!fs.existsSync(`${cwd}/PRD.md`)) {
|
|
26
|
-
return {
|
|
27
|
-
content: [{ type: "text" as const, text: "PRD.md not found." }],
|
|
28
|
-
details: { pipeline: "create-issues", stages: [] },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const stages = [emptyStage("issue-creator")];
|
|
33
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "create-issues", onUpdate };
|
|
34
|
-
|
|
35
|
-
await runAgent(
|
|
36
|
-
"issue-creator",
|
|
37
|
-
"Decompose PRD.md into vertical-slice GitHub issues. Read the issue-template skill for the standard format.",
|
|
38
|
-
{ ...opts, tools: TOOLS_NO_EDIT },
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: "text" as const, text: "Issue creation complete." }],
|
|
43
|
-
details: { pipeline: "create-issues", stages },
|
|
44
|
-
};
|
|
45
|
-
}
|
package/src/pipelines/prd-qa.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
type AnyCtx,
|
|
4
|
-
emptyStage,
|
|
5
|
-
runAgent,
|
|
6
|
-
type StageResult,
|
|
7
|
-
signalExists,
|
|
8
|
-
TOOLS_ALL,
|
|
9
|
-
TOOLS_NO_EDIT,
|
|
10
|
-
} from "@callumvass/forgeflow-shared";
|
|
11
|
-
import { AGENTS_DIR } from "../resolve.js";
|
|
12
|
-
|
|
13
|
-
export async function runPrdQa(cwd: string, maxIterations: number, signal: AbortSignal, onUpdate: AnyCtx, ctx: AnyCtx) {
|
|
14
|
-
if (!fs.existsSync(`${cwd}/PRD.md`)) {
|
|
15
|
-
return {
|
|
16
|
-
content: [{ type: "text" as const, text: "PRD.md not found." }],
|
|
17
|
-
details: { pipeline: "prd-qa", stages: [] },
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const stages: StageResult[] = [];
|
|
22
|
-
const opts = { agentsDir: AGENTS_DIR, cwd, signal, stages, pipeline: "prd-qa", onUpdate };
|
|
23
|
-
|
|
24
|
-
for (let i = 1; i <= maxIterations; i++) {
|
|
25
|
-
// Critic
|
|
26
|
-
stages.push(emptyStage("prd-critic"));
|
|
27
|
-
const criticResult = await runAgent(
|
|
28
|
-
"prd-critic",
|
|
29
|
-
"Review PRD.md for completeness. If it needs refinement, create QUESTIONS.md. If it's complete, do NOT create QUESTIONS.md.",
|
|
30
|
-
{ ...opts, tools: TOOLS_NO_EDIT },
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// No QUESTIONS.md = critic considers PRD complete
|
|
34
|
-
if (!signalExists(cwd, "questions")) {
|
|
35
|
-
if (criticResult.status === "failed") {
|
|
36
|
-
return {
|
|
37
|
-
content: [{ type: "text" as const, text: `Critic failed.\nStderr: ${criticResult.stderr.slice(0, 300)}` }],
|
|
38
|
-
details: { pipeline: "prd-qa", stages },
|
|
39
|
-
isError: true,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
content: [{ type: "text" as const, text: "PRD refinement complete. Ready for /create-issues." }],
|
|
44
|
-
details: { pipeline: "prd-qa", stages },
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Architect
|
|
49
|
-
stages.push(emptyStage("prd-architect"));
|
|
50
|
-
await runAgent(
|
|
51
|
-
"prd-architect",
|
|
52
|
-
"Read PRD.md and answer all questions in QUESTIONS.md. Write answers inline in QUESTIONS.md.",
|
|
53
|
-
{ ...opts, tools: TOOLS_ALL },
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
// Integrator — incorporate answers into PRD before approval gate
|
|
57
|
-
stages.push(emptyStage("prd-integrator"));
|
|
58
|
-
await runAgent(
|
|
59
|
-
"prd-integrator",
|
|
60
|
-
"Incorporate answers from QUESTIONS.md into PRD.md, then delete QUESTIONS.md.",
|
|
61
|
-
opts,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// Approval gate — show PRD in editor, user can review/edit then decide
|
|
65
|
-
if (ctx.hasUI) {
|
|
66
|
-
const prdContent = fs.readFileSync(`${cwd}/PRD.md`, "utf-8");
|
|
67
|
-
const edited = await ctx.ui.editor(`Iteration ${i} — Review PRD (edit or close to continue)`, prdContent);
|
|
68
|
-
|
|
69
|
-
// If user edited, write changes back
|
|
70
|
-
if (edited != null && edited !== prdContent) {
|
|
71
|
-
fs.writeFileSync(`${cwd}/PRD.md`, edited, "utf-8");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const action = await ctx.ui.select("PRD updated. What next?", ["Continue refining", "Accept PRD"]);
|
|
75
|
-
if (action === "Accept PRD" || action == null) {
|
|
76
|
-
return {
|
|
77
|
-
content: [{ type: "text" as const, text: "PRD accepted." }],
|
|
78
|
-
details: { pipeline: "prd-qa", stages },
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
content: [{ type: "text" as const, text: `PRD refinement did not complete after ${maxIterations} iterations.` }],
|
|
86
|
-
details: { pipeline: "prd-qa", stages },
|
|
87
|
-
};
|
|
88
|
-
}
|
package/src/resolve.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
|
|
4
|
-
// After bundling: extensions/index.js → up one level → agents/
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
export const AGENTS_DIR = path.resolve(__dirname, "..", "agents");
|