@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.
@@ -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
- this.log(Colors.success(`\n✓ PRD already valid - found ${epics.length} epic(s)`));
84
- this.log(Colors.dim(' No changes needed.\n'));
85
- // Show the epics found
86
- for (const epic of epics) {
87
- this.log(Colors.dim(` • Epic ${epic.number}: ${epic.title}`));
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. This allows the workflow to handle
6
- * PRDs written in various formats by normalizing them before parsing.
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. Creates a backup before modifying the original file.
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. Creates a backup of the original file before
60
- * overwriting with the fixed content.
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. This allows the workflow to handle
6
- * PRDs written in various formats by normalizing them before parsing.
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. Creates a backup before modifying the original file.
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. Creates a backup of the original file before
44
- * overwriting with the fixed content.
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, originalContent, references);
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
- // Extract the fixed content from the AI response
70
- const fixedContent = this.extractFixedContent(result.output);
71
- if (!fixedContent) {
72
- this.logger.error({ prdPath }, 'Could not extract fixed content from AI response');
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: 'Could not extract fixed PRD content from AI response',
81
+ error: 'AI agent did not modify the PRD file',
76
82
  fixed: false,
77
83
  };
78
84
  }
79
- // Create backup of original file
80
- const backupPath = `${prdPath}.bak`;
81
- await this.fileManager.writeFile(backupPath, originalContent);
82
- this.logger.info({ backupPath }, 'Created backup of original PRD');
83
- // Write the fixed content to the original path
84
- await this.fileManager.writeFile(prdPath, fixedContent);
85
- this.logger.info({ prdPath }, 'Wrote fixed PRD content');
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, content, references) {
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. Your task is to reformat a Product Requirements Document (PRD) to match the expected structure for automated workflow processing.
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. Analyze the current PRD structure
135
+ 1. Read "${prdPath}"
149
136
  2. Identify all epics/features/phases (they might be labeled differently)
150
- 3. Reformat them to use "## Epic N: Title" format
151
- 4. Ensure stories are properly formatted under each epic
152
- 5. Preserve ALL original content and meaning - only change the formatting
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/bmad-workflow",
3
3
  "description": "AI-driven development workflow orchestration CLI for BMAD projects",
4
- "version": "1.0.14",
4
+ "version": "1.0.15",
5
5
  "author": {
6
6
  "name": "DevSquad",
7
7
  "email": "marcelo@devsquad.email",