@hyperdrive.bot/bmad-workflow 1.0.14 → 1.0.15
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/dist/commands/prd/fix.js
CHANGED
|
@@ -78,19 +78,34 @@ export default class PrdFix extends Command {
|
|
|
78
78
|
// First, check if the PRD already parses correctly
|
|
79
79
|
this.log(Colors.info(`\nAnalyzing PRD: ${relativePath}`));
|
|
80
80
|
let needsFix = false;
|
|
81
|
+
let fixReason = '';
|
|
81
82
|
try {
|
|
82
83
|
const epics = this.prdParser.parseEpics(originalContent, prdPath);
|
|
83
|
-
|
|
84
|
-
this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
// Also validate that each epic contains stories
|
|
85
|
+
const storyCounts = this.prdParser.countStoriesPerEpic(originalContent, epics);
|
|
86
|
+
const epicsWithoutStories = epics.filter((e) => (storyCounts.get(e.number) || 0) === 0);
|
|
87
|
+
if (epicsWithoutStories.length > 0) {
|
|
88
|
+
needsFix = true;
|
|
89
|
+
const missing = epicsWithoutStories.map((e) => `Epic ${e.number}`).join(', ');
|
|
90
|
+
fixReason = `Found ${epics.length} epic(s) but ${epicsWithoutStories.length} missing stories: ${missing}`;
|
|
91
|
+
this.log(Colors.warning(`\n⚠ ${fixReason}`));
|
|
92
|
+
this.log(Colors.warning(' Epics need story headers like: ### Story 1.1: Title'));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.log(Colors.success(`\n✓ PRD already valid - found ${epics.length} epic(s)`));
|
|
96
|
+
this.log(Colors.dim(' No changes needed.\n'));
|
|
97
|
+
// Show the epics found with story counts
|
|
98
|
+
for (const epic of epics) {
|
|
99
|
+
const count = storyCounts.get(epic.number) || 0;
|
|
100
|
+
this.log(Colors.dim(` • Epic ${epic.number}: ${epic.title} (${count} stories)`));
|
|
101
|
+
}
|
|
102
|
+
this.log('');
|
|
103
|
+
return;
|
|
88
104
|
}
|
|
89
|
-
this.log('');
|
|
90
|
-
return;
|
|
91
105
|
}
|
|
92
106
|
catch {
|
|
93
107
|
needsFix = true;
|
|
108
|
+
fixReason = 'No epics found';
|
|
94
109
|
this.log(Colors.warning('\n⚠ PRD format issues detected - attempting auto-fix...'));
|
|
95
110
|
}
|
|
96
111
|
if (needsFix) {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* PRD Fixer Service
|
|
3
3
|
*
|
|
4
4
|
* Uses AI to automatically reformat PRD documents that don't match
|
|
5
|
-
* the expected epic/story format.
|
|
6
|
-
*
|
|
5
|
+
* the expected epic/story format. The AI writes the fixed content
|
|
6
|
+
* directly to the file path, avoiding fragile stdout parsing.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```typescript
|
|
@@ -38,7 +38,8 @@ export interface PrdFixResult {
|
|
|
38
38
|
* PRD Fixer Service
|
|
39
39
|
*
|
|
40
40
|
* Automatically reformats PRD documents to match expected epic/story patterns
|
|
41
|
-
* using AI.
|
|
41
|
+
* using AI. The AI agent writes the fixed content directly to the file,
|
|
42
|
+
* creating a backup first.
|
|
42
43
|
*/
|
|
43
44
|
export declare class PrdFixer {
|
|
44
45
|
private readonly agentRunner;
|
|
@@ -56,8 +57,8 @@ export declare class PrdFixer {
|
|
|
56
57
|
* Fix a PRD document to match expected format
|
|
57
58
|
*
|
|
58
59
|
* Uses AI to analyze and reformat the PRD content to match the expected
|
|
59
|
-
* epic/story structure.
|
|
60
|
-
*
|
|
60
|
+
* epic/story structure. The AI creates a backup and writes the fixed
|
|
61
|
+
* content directly to the file path.
|
|
61
62
|
*
|
|
62
63
|
* @param prdPath - Path to the PRD file
|
|
63
64
|
* @param originalContent - Original PRD content that failed parsing
|
|
@@ -68,19 +69,12 @@ export declare class PrdFixer {
|
|
|
68
69
|
/**
|
|
69
70
|
* Build the prompt for fixing the PRD
|
|
70
71
|
*
|
|
72
|
+
* Instructs the AI to read the file at the given path, reformat it,
|
|
73
|
+
* and write it back directly to the same path.
|
|
74
|
+
*
|
|
71
75
|
* @param prdPath - Path to the PRD file
|
|
72
|
-
* @param content - Original PRD content
|
|
73
76
|
* @param references - Optional reference files
|
|
74
77
|
* @returns Complete prompt string for the AI agent
|
|
75
78
|
*/
|
|
76
79
|
private buildFixPrompt;
|
|
77
|
-
/**
|
|
78
|
-
* Extract the fixed PRD content from the AI response
|
|
79
|
-
*
|
|
80
|
-
* Looks for content wrapped in markdown code blocks and extracts it.
|
|
81
|
-
*
|
|
82
|
-
* @param output - Raw AI response output
|
|
83
|
-
* @returns Extracted PRD content or null if not found
|
|
84
|
-
*/
|
|
85
|
-
private extractFixedContent;
|
|
86
80
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* PRD Fixer Service
|
|
3
3
|
*
|
|
4
4
|
* Uses AI to automatically reformat PRD documents that don't match
|
|
5
|
-
* the expected epic/story format.
|
|
6
|
-
*
|
|
5
|
+
* the expected epic/story format. The AI writes the fixed content
|
|
6
|
+
* directly to the file path, avoiding fragile stdout parsing.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```typescript
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* PRD Fixer Service
|
|
19
19
|
*
|
|
20
20
|
* Automatically reformats PRD documents to match expected epic/story patterns
|
|
21
|
-
* using AI.
|
|
21
|
+
* using AI. The AI agent writes the fixed content directly to the file,
|
|
22
|
+
* creating a backup first.
|
|
22
23
|
*/
|
|
23
24
|
export class PrdFixer {
|
|
24
25
|
agentRunner;
|
|
@@ -40,8 +41,8 @@ export class PrdFixer {
|
|
|
40
41
|
* Fix a PRD document to match expected format
|
|
41
42
|
*
|
|
42
43
|
* Uses AI to analyze and reformat the PRD content to match the expected
|
|
43
|
-
* epic/story structure.
|
|
44
|
-
*
|
|
44
|
+
* epic/story structure. The AI creates a backup and writes the fixed
|
|
45
|
+
* content directly to the file path.
|
|
45
46
|
*
|
|
46
47
|
* @param prdPath - Path to the PRD file
|
|
47
48
|
* @param originalContent - Original PRD content that failed parsing
|
|
@@ -51,9 +52,13 @@ export class PrdFixer {
|
|
|
51
52
|
async fixPrd(prdPath, originalContent, references) {
|
|
52
53
|
this.logger.info({ prdPath, references: references?.length ?? 0 }, 'Attempting to auto-fix PRD format');
|
|
53
54
|
try {
|
|
55
|
+
// Create backup before AI modifies the file
|
|
56
|
+
const backupPath = `${prdPath}.bak`;
|
|
57
|
+
await this.fileManager.writeFile(backupPath, originalContent);
|
|
58
|
+
this.logger.info({ backupPath }, 'Created backup of original PRD');
|
|
54
59
|
// Build the prompt for fixing the PRD
|
|
55
|
-
const prompt = this.buildFixPrompt(prdPath,
|
|
56
|
-
// Execute the AI agent to fix the PRD
|
|
60
|
+
const prompt = this.buildFixPrompt(prdPath, references);
|
|
61
|
+
// Execute the AI agent to fix the PRD (writes directly to prdPath)
|
|
57
62
|
const result = await this.agentRunner.runAgent(prompt, {
|
|
58
63
|
agentType: 'prd-fixer',
|
|
59
64
|
timeout: 300_000, // 5 minutes should be enough for reformatting
|
|
@@ -66,23 +71,29 @@ export class PrdFixer {
|
|
|
66
71
|
fixed: false,
|
|
67
72
|
};
|
|
68
73
|
}
|
|
69
|
-
//
|
|
70
|
-
const fixedContent = this.
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
// Read the file back to verify the AI wrote it
|
|
75
|
+
const fixedContent = await this.fileManager.readFile(prdPath);
|
|
76
|
+
// Validate the AI actually changed the file and it looks like a PRD
|
|
77
|
+
if (fixedContent === originalContent) {
|
|
78
|
+
this.logger.error({ prdPath }, 'AI agent did not modify the PRD file');
|
|
73
79
|
return {
|
|
74
80
|
content: originalContent,
|
|
75
|
-
error: '
|
|
81
|
+
error: 'AI agent did not modify the PRD file',
|
|
76
82
|
fixed: false,
|
|
77
83
|
};
|
|
78
84
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
if (!/##\s+Epic\s+\d+/i.test(fixedContent)) {
|
|
86
|
+
this.logger.error({ prdPath }, 'Fixed file does not contain valid epic headers');
|
|
87
|
+
// Restore from backup
|
|
88
|
+
await this.fileManager.writeFile(prdPath, originalContent);
|
|
89
|
+
this.logger.info({ prdPath }, 'Restored original PRD from backup');
|
|
90
|
+
return {
|
|
91
|
+
content: originalContent,
|
|
92
|
+
error: 'AI output did not contain valid epic headers, original restored',
|
|
93
|
+
fixed: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
this.logger.info({ prdPath }, 'PRD fixed successfully');
|
|
86
97
|
return {
|
|
87
98
|
content: fixedContent,
|
|
88
99
|
fixed: true,
|
|
@@ -101,94 +112,30 @@ export class PrdFixer {
|
|
|
101
112
|
/**
|
|
102
113
|
* Build the prompt for fixing the PRD
|
|
103
114
|
*
|
|
115
|
+
* Instructs the AI to read the file at the given path, reformat it,
|
|
116
|
+
* and write it back directly to the same path.
|
|
117
|
+
*
|
|
104
118
|
* @param prdPath - Path to the PRD file
|
|
105
|
-
* @param content - Original PRD content
|
|
106
119
|
* @param references - Optional reference files
|
|
107
120
|
* @returns Complete prompt string for the AI agent
|
|
108
121
|
*/
|
|
109
|
-
buildFixPrompt(prdPath,
|
|
110
|
-
// Build reference context if provided
|
|
122
|
+
buildFixPrompt(prdPath, references) {
|
|
111
123
|
let referenceContext = '';
|
|
112
124
|
if (references && references.length > 0) {
|
|
113
|
-
referenceContext = `
|
|
114
|
-
|
|
115
|
-
REFERENCE FILES FOR CONTEXT:
|
|
116
|
-
${references.map((ref) => `- ${ref}`).join('\n')}
|
|
117
|
-
|
|
118
|
-
Read these reference files to understand the project context and patterns.`;
|
|
125
|
+
referenceContext = `\nAlso read these reference files for project context:\n${references.map((ref) => `- ${ref}`).join('\n')}\n`;
|
|
119
126
|
}
|
|
120
|
-
return `You are a PRD formatting expert.
|
|
121
|
-
|
|
122
|
-
TASK: Reformat the PRD file at "${prdPath}" to use the correct epic and story format.
|
|
123
|
-
|
|
124
|
-
EXPECTED FORMAT:
|
|
125
|
-
1. Epic headers MUST use this exact format: ## Epic N: Title
|
|
126
|
-
- Examples: "## Epic 1: Foundation & Setup", "## Epic 2: Core Features"
|
|
127
|
-
- The "Epic" keyword is REQUIRED
|
|
128
|
-
- The number MUST be sequential (1, 2, 3...)
|
|
129
|
-
- A colon separates the number from the title
|
|
130
|
-
|
|
131
|
-
2. Story sections within epics should use:
|
|
132
|
-
- ### Stories (as a section header)
|
|
133
|
-
- Bullet points: - Story N.M: Description
|
|
134
|
-
- Example: "- Story 1.1: Initialize project structure"
|
|
135
|
-
|
|
136
|
-
3. Each epic should have:
|
|
137
|
-
- A description paragraph after the header
|
|
138
|
-
- A ### Goals section with bullet points
|
|
139
|
-
- A ### Stories section with story bullet points
|
|
140
|
-
|
|
141
|
-
CURRENT PRD CONTENT:
|
|
142
|
-
\`\`\`markdown
|
|
143
|
-
${content}
|
|
144
|
-
\`\`\`
|
|
127
|
+
return `You are a PRD formatting expert. Read the PRD file at "${prdPath}", reformat it to match the expected structure, and write the result back to the same file.
|
|
145
128
|
${referenceContext}
|
|
129
|
+
EXPECTED FORMAT:
|
|
130
|
+
1. Epic headers: ## Epic N: Title (keyword "Epic" required, sequential numbering, colon separator)
|
|
131
|
+
2. Story headers within epics: ### Story N.M: Title (N=epic number, M=story number)
|
|
132
|
+
3. Each story should have acceptance criteria
|
|
146
133
|
|
|
147
134
|
INSTRUCTIONS:
|
|
148
|
-
1.
|
|
135
|
+
1. Read "${prdPath}"
|
|
149
136
|
2. Identify all epics/features/phases (they might be labeled differently)
|
|
150
|
-
3. Reformat
|
|
151
|
-
4.
|
|
152
|
-
5.
|
|
153
|
-
6. Number epics sequentially starting from 1
|
|
154
|
-
7. Number stories as N.M where N is the epic number and M is the story number within that epic
|
|
155
|
-
|
|
156
|
-
OUTPUT FORMAT:
|
|
157
|
-
Output ONLY the reformatted PRD content wrapped in a markdown code block.
|
|
158
|
-
Do not include any explanation before or after the code block.
|
|
159
|
-
The output must be a complete, valid markdown document.
|
|
160
|
-
|
|
161
|
-
\`\`\`markdown
|
|
162
|
-
[Your reformatted PRD here]
|
|
163
|
-
\`\`\``;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Extract the fixed PRD content from the AI response
|
|
167
|
-
*
|
|
168
|
-
* Looks for content wrapped in markdown code blocks and extracts it.
|
|
169
|
-
*
|
|
170
|
-
* @param output - Raw AI response output
|
|
171
|
-
* @returns Extracted PRD content or null if not found
|
|
172
|
-
*/
|
|
173
|
-
extractFixedContent(output) {
|
|
174
|
-
// Try to extract content from markdown code block
|
|
175
|
-
const codeBlockMatch = output.match(/```(?:markdown)?\s*\n([\s\S]*?)\n```/);
|
|
176
|
-
if (codeBlockMatch && codeBlockMatch[1]) {
|
|
177
|
-
const extracted = codeBlockMatch[1].trim();
|
|
178
|
-
// Validate it looks like a PRD (has at least one epic header)
|
|
179
|
-
if (/##\s+Epic\s+\d+/i.test(extracted)) {
|
|
180
|
-
return extracted;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// Fallback: if the output itself looks like a valid PRD
|
|
184
|
-
if (/##\s+Epic\s+\d+/i.test(output)) {
|
|
185
|
-
// Strip any leading/trailing explanation text
|
|
186
|
-
const lines = output.split('\n');
|
|
187
|
-
const prdStart = lines.findIndex((line) => line.startsWith('#'));
|
|
188
|
-
if (prdStart !== -1) {
|
|
189
|
-
return lines.slice(prdStart).join('\n').trim();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
137
|
+
3. Reformat epic headers to "## Epic N: Title" and story headers to "### Story N.M: Title"
|
|
138
|
+
4. Preserve ALL original content and meaning — only change the formatting
|
|
139
|
+
5. Write the complete reformatted PRD back to "${prdPath}"`;
|
|
193
140
|
}
|
|
194
141
|
}
|
|
@@ -90,6 +90,17 @@ export declare class PrdParser {
|
|
|
90
90
|
* ```
|
|
91
91
|
*/
|
|
92
92
|
parseEpics(prdContent: string, prdPath: string): Epic[];
|
|
93
|
+
/**
|
|
94
|
+
* Count stories per epic within a PRD document
|
|
95
|
+
*
|
|
96
|
+
* Splits the PRD into epic sections and counts story headers in each.
|
|
97
|
+
* Used by `prd fix` to validate that epics contain stories.
|
|
98
|
+
*
|
|
99
|
+
* @param prdContent - Full PRD markdown content
|
|
100
|
+
* @param epics - Parsed epics from `parseEpics()`
|
|
101
|
+
* @returns Map of epic number to story count
|
|
102
|
+
*/
|
|
103
|
+
countStoriesPerEpic(prdContent: string, epics: Epic[]): Map<number, number>;
|
|
93
104
|
/**
|
|
94
105
|
* Deduplicate matches that appear at the same position
|
|
95
106
|
*
|
|
@@ -166,6 +166,45 @@ export class PrdParser {
|
|
|
166
166
|
this.logger.info({ epicCount: epics.length, prdPath }, 'Successfully parsed epics from PRD');
|
|
167
167
|
return epics;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Count stories per epic within a PRD document
|
|
171
|
+
*
|
|
172
|
+
* Splits the PRD into epic sections and counts story headers in each.
|
|
173
|
+
* Used by `prd fix` to validate that epics contain stories.
|
|
174
|
+
*
|
|
175
|
+
* @param prdContent - Full PRD markdown content
|
|
176
|
+
* @param epics - Parsed epics from `parseEpics()`
|
|
177
|
+
* @returns Map of epic number to story count
|
|
178
|
+
*/
|
|
179
|
+
countStoriesPerEpic(prdContent, epics) {
|
|
180
|
+
const lines = prdContent.split('\n');
|
|
181
|
+
const result = new Map();
|
|
182
|
+
// Story header patterns matching EpicParser patterns
|
|
183
|
+
const storyPatterns = [
|
|
184
|
+
/^###\s+Story\s+(\d+)\.(\d+)\s*:\s*.+$/i,
|
|
185
|
+
/^###\s+Story\s+(\d+)\.(\d+)\s+-\s+.+$/i,
|
|
186
|
+
/^###\s+(\d+)\.(\d+)\s*:\s*.+$/i,
|
|
187
|
+
/^###\s+(\d+)\.(\d+)\s*\.\s+.+$/i,
|
|
188
|
+
];
|
|
189
|
+
for (const epic of epics) {
|
|
190
|
+
// Find the end of this epic's section (next epic's position or EOF)
|
|
191
|
+
const nextEpic = epics.find((e) => e.position > epic.position);
|
|
192
|
+
const sectionEnd = nextEpic ? nextEpic.position : lines.length;
|
|
193
|
+
const sectionLines = lines.slice(epic.position, sectionEnd);
|
|
194
|
+
let storyCount = 0;
|
|
195
|
+
for (const line of sectionLines) {
|
|
196
|
+
for (const pattern of storyPatterns) {
|
|
197
|
+
if (pattern.test(line)) {
|
|
198
|
+
storyCount++;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
result.set(epic.number, storyCount);
|
|
204
|
+
this.logger.debug({ epicNumber: epic.number, storyCount }, 'Counted stories in epic section');
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
169
208
|
/**
|
|
170
209
|
* Deduplicate matches that appear at the same position
|
|
171
210
|
*
|
package/package.json
CHANGED