@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
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@callumvass/forgeflow-pm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "PM pipeline for Pi — PRD refinement, issue creation, and continue.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/callumvass/forgeflow.git",
|
|
13
|
+
"directory": "packages/pm"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"provenance": true
|
|
17
|
+
},
|
|
18
|
+
"pi": {
|
|
19
|
+
"extensions": [
|
|
20
|
+
"./extensions"
|
|
21
|
+
],
|
|
22
|
+
"skills": [
|
|
23
|
+
"./skills"
|
|
24
|
+
],
|
|
25
|
+
"agents": [
|
|
26
|
+
"./agents"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@callumvass/forgeflow-shared": "*"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@mariozechner/pi-ai": "*",
|
|
37
|
+
"@mariozechner/pi-agent-core": "*",
|
|
38
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
39
|
+
"@mariozechner/pi-tui": "*",
|
|
40
|
+
"@sinclair/typebox": "*"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: issue-template
|
|
3
|
+
description: Standard format and rules for creating GitHub issues for autonomous agent implementation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Issue Template Skill
|
|
7
|
+
|
|
8
|
+
## Issue Template
|
|
9
|
+
|
|
10
|
+
Every issue MUST follow this exact format:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Title: <short descriptive title>
|
|
14
|
+
|
|
15
|
+
Body:
|
|
16
|
+
## Context
|
|
17
|
+
<Provide enough detail for an agent to implement THIS slice. Include: user-observable behavior, relevant data model (conceptual), API contracts, technology choices, edge cases. Do NOT include: type definitions, internal state shapes, config blocks, file layout, or framework-specific patterns. Keep under ~60 lines.>
|
|
18
|
+
|
|
19
|
+
## Acceptance Criteria
|
|
20
|
+
<Bulleted checklist describing what the USER sees/experiences. Not implementation details.>
|
|
21
|
+
- [ ] User does X and sees Y
|
|
22
|
+
- [ ] ...
|
|
23
|
+
|
|
24
|
+
## Test Plan
|
|
25
|
+
<Specific tests that must pass. FIRST test must be a trigger test.>
|
|
26
|
+
- [ ] Trigger: <entry-point → observable output, proving the slice is wired end-to-end>
|
|
27
|
+
- [ ] Boundary: <describe test through real runtime or route-level render>
|
|
28
|
+
- [ ] Unit (only if pure algorithm): <describe algorithmic edge case test>
|
|
29
|
+
|
|
30
|
+
## Implementation Hints
|
|
31
|
+
<Concrete guidance: files to create/modify, key APIs, rough approach. Keep it actionable.>
|
|
32
|
+
|
|
33
|
+
## Dependencies
|
|
34
|
+
<Which previous issues must be complete first, if any>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Creating Issues
|
|
38
|
+
|
|
39
|
+
- Use `gh issue create` to create each issue.
|
|
40
|
+
- Add the `auto-generated` label to every issue (create the label first if it doesn't exist).
|
|
41
|
+
- After creating each issue, note its number so you can reference it in subsequent issues' Dependencies sections.
|
|
42
|
+
|
|
43
|
+
## Rules
|
|
44
|
+
|
|
45
|
+
- The Context section is CRITICAL — the agent works from this alone. Include behavioral requirements, data model concepts, API contracts, and edge cases inline. Do NOT say "see PRD.md".
|
|
46
|
+
- Acceptance criteria must describe user-observable behavior, not code structure.
|
|
47
|
+
- Each vertical slice must cross all necessary layers to deliver a working flow.
|
|
48
|
+
- **Design is per-slice, not a final pass.** If DESIGN.md exists, every slice that touches UI must implement its screens using the design system.
|
|
49
|
+
- **No standalone validation/edge-case issues.** Validation, error handling, and edge cases belong in the slice that introduces the behavior.
|
|
50
|
+
- **No standalone polish issues.** Accessibility, responsive layout, and design compliance belong in each slice.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prd-quality
|
|
3
|
+
description: PRD completeness and quality criteria for evaluating product requirements documents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This skill defines the quality criteria for evaluating whether a PRD is complete and implementation-ready.
|
|
7
|
+
|
|
8
|
+
## Completeness Criteria
|
|
9
|
+
|
|
10
|
+
A PRD is ready for implementation when it satisfies ALL of the following:
|
|
11
|
+
|
|
12
|
+
### 1. Problem Statement & Goals
|
|
13
|
+
- Clear description of the problem being solved
|
|
14
|
+
- Measurable success criteria or goals
|
|
15
|
+
- Why this matters (user pain, business value)
|
|
16
|
+
|
|
17
|
+
### 2. User Stories / Use Cases
|
|
18
|
+
- Clear actors (who does what)
|
|
19
|
+
- Each story follows a complete flow from trigger to outcome
|
|
20
|
+
- Covers primary happy paths and key alternative paths
|
|
21
|
+
|
|
22
|
+
### 3. Functional Requirements
|
|
23
|
+
- Detailed enough that a developer can implement without further clarification
|
|
24
|
+
- Covers inputs, outputs, and transformations for each feature
|
|
25
|
+
- API contracts as prose: "POST /api/rooms creates a room and returns a room code" — NOT code blocks
|
|
26
|
+
- Data model as concepts: "a room tracks voters, their votes, and the current poll" — NOT type definitions
|
|
27
|
+
|
|
28
|
+
### 4. Non-Functional Requirements
|
|
29
|
+
- Performance expectations (latency, throughput)
|
|
30
|
+
- Security considerations (auth model, data protection, input validation)
|
|
31
|
+
- Scalability constraints or targets
|
|
32
|
+
- Reliability/availability requirements if relevant
|
|
33
|
+
|
|
34
|
+
### 5. Edge Cases & Error Handling
|
|
35
|
+
- Each flow identifies what can go wrong
|
|
36
|
+
- Error states have defined user-facing behavior
|
|
37
|
+
- Boundary conditions are addressed (empty states, limits, concurrent access)
|
|
38
|
+
|
|
39
|
+
### 6. Scope Boundaries
|
|
40
|
+
- Explicit "in scope" and "out of scope" sections
|
|
41
|
+
- No ambiguity about what will and won't be built
|
|
42
|
+
|
|
43
|
+
### 6a. Phase Structure (for multi-phase projects)
|
|
44
|
+
- If the project has prior work, the PRD should contain a `## Done` section summarizing completed work
|
|
45
|
+
- The `## Next` section describes what's being built now
|
|
46
|
+
- `## Done` is treated as accepted context — not re-evaluated for completeness
|
|
47
|
+
- Only `## Next` is evaluated against these criteria
|
|
48
|
+
|
|
49
|
+
### 7. Vertical-Slice Readiness
|
|
50
|
+
- Requirements are structured around user-observable flows, not technical layers
|
|
51
|
+
- Each feature can be decomposed into end-to-end slices
|
|
52
|
+
- No requirement depends on a fully-built layer that doesn't yet exist
|
|
53
|
+
|
|
54
|
+
### 8. Implementation Clarity
|
|
55
|
+
- Enough technical detail to know WHAT to build and which technologies to use — not HOW to structure code
|
|
56
|
+
- Technology choices specified where they matter, with brief rationale
|
|
57
|
+
- Key library/dependency choices include version or API generation details when relevant
|
|
58
|
+
- Integration points with existing systems documented
|
|
59
|
+
- Do NOT include: type definitions, internal state shapes, file/directory layout, framework-specific patterns, config file contents, or specific hex colors/pixel values
|
|
60
|
+
|
|
61
|
+
### 9. Specification Minimalism
|
|
62
|
+
- PRD describes behavior (what the system does), not implementation (how code is structured)
|
|
63
|
+
- ZERO code blocks allowed in the PRD
|
|
64
|
+
- "Swap test": if a detail would change when swapping to an equivalent library but the behavior stays the same, it's implementation detail — remove it
|
|
65
|
+
- Technology choices name the tool and justify why — they do not prescribe usage patterns
|
|
66
|
+
- Data models describe entities and relationships conceptually, not as language-level types
|
|
67
|
+
- API contracts define endpoints and response descriptions in prose, not code blocks
|
|
68
|
+
- Target ~150-200 lines for the entire PRD
|
|
69
|
+
- A PRD with complete behavioral requirements at 150 lines is BETTER than one with implementation detail at 300 lines
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
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;
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|