@hyperdrive.bot/bmad-workflow 1.0.13 → 1.0.14

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.
@@ -1376,9 +1376,15 @@ Write output to: ${outputPath}`;
1376
1376
  this.logger.warn({ epicNumber: epic.number }, 'Epic file not found, skipping');
1377
1377
  return [];
1378
1378
  }
1379
- const epicContent = await this.fileManager.readFile(epicFilePath);
1380
- const stories = this.epicParser.parseStories(epicContent, epicFilePath);
1381
- return stories;
1379
+ try {
1380
+ const epicContent = await this.fileManager.readFile(epicFilePath);
1381
+ const stories = this.epicParser.parseStories(epicContent, epicFilePath);
1382
+ return stories;
1383
+ }
1384
+ catch (error) {
1385
+ this.logger.warn({ epicNumber: epic.number, error: error.message }, 'Failed to parse stories from epic, skipping');
1386
+ return [];
1387
+ }
1382
1388
  }));
1383
1389
  // Flatten the array of story arrays
1384
1390
  allStories.push(...epicStories.flat());
@@ -137,13 +137,10 @@ export class EpicParser {
137
137
  this.logger.debug({ matches: matches.length, patternName: pattern.name }, 'Pattern matched stories');
138
138
  }
139
139
  }
140
- // Check if any stories were found
140
+ // If no stories found, return empty array (epic may legitimately have no stories)
141
141
  if (allMatches.length === 0) {
142
- this.logger.error({ epicPath }, 'No stories found in epic');
143
- throw new ParserError('No stories found in epic. Expected format: `### Story 1.1: Title` or similar', {
144
- filePath: epicPath,
145
- suggestion: 'Ensure epic has story headers like: ### Story 1.1: Initialize Project',
146
- });
142
+ this.logger.warn({ epicPath }, 'No stories found in epic, returning empty array');
143
+ return [];
147
144
  }
148
145
  // Sort by position (natural document order)
149
146
  allMatches.sort((a, b) => a.position - b.position);
@@ -53,6 +53,14 @@ export declare class PrdParser {
53
53
  *
54
54
  * CRITICAL: Pattern order matches prd-tmpl.yaml output format
55
55
  * Template generates: ## Epic 1 Title (space-separated)
56
+ *
57
+ * Patterns support both H2 (##) and H3 (###) epic headers.
58
+ * PRDs may use H3 for epics when H2 is used for document sections
59
+ * (e.g., ## 6. Epic Details → ### Epic 1: Title).
60
+ *
61
+ * Generic patterns (N: and N.) are only used as fallback when no
62
+ * explicit "Epic N" patterns match — they can false-positive on
63
+ * numbered document sections like "## 1. Goals".
56
64
  */
57
65
  private readonly patterns;
58
66
  /**
@@ -32,12 +32,32 @@ export class PrdParser {
32
32
  *
33
33
  * CRITICAL: Pattern order matches prd-tmpl.yaml output format
34
34
  * Template generates: ## Epic 1 Title (space-separated)
35
+ *
36
+ * Patterns support both H2 (##) and H3 (###) epic headers.
37
+ * PRDs may use H3 for epics when H2 is used for document sections
38
+ * (e.g., ## 6. Epic Details → ### Epic 1: Title).
39
+ *
40
+ * Generic patterns (N: and N.) are only used as fallback when no
41
+ * explicit "Epic N" patterns match — they can false-positive on
42
+ * numbered document sections like "## 1. Goals".
35
43
  */
36
44
  patterns = [
37
45
  {
38
46
  name: 'Epic N Nested (N. Epic M:)',
39
47
  regex: /^##\s+\d+\.\s+Epic\s+(\d+)\s*:\s*(.+?)$/gim,
40
48
  },
49
+ {
50
+ name: 'H3 Epic N: Title',
51
+ regex: /^###\s+Epic\s+(\d+)\s*:\s*(.+?)$/gim,
52
+ },
53
+ {
54
+ name: 'H3 Epic N - Title',
55
+ regex: /^###\s+Epic\s+(\d+)\s+-\s+(.+?)$/gim,
56
+ },
57
+ {
58
+ name: 'H3 Epic N Title (space)',
59
+ regex: /^###\s+Epic\s+(\d+)\s+(?![:-])(.+?)$/gim,
60
+ },
41
61
  {
42
62
  name: 'Epic N: Title',
43
63
  regex: /^##\s+Epic\s+(\d+)\s*:\s*(.+?)$/gim,
@@ -106,16 +126,27 @@ export class PrdParser {
106
126
  // Collect matches from all patterns - patterns are ordered by specificity
107
127
  // More specific "Epic N" patterns come first in the array
108
128
  const allMatches = [];
129
+ let hasExplicitEpicMatches = false;
109
130
  for (const pattern of this.patterns) {
110
131
  // Skip "N. Title" pattern if nested format found (N. is used for sections)
111
132
  if (hasNestedEpicFormat && pattern.name === 'N. Title') {
112
133
  this.logger.debug({ patternName: pattern.name }, 'Skipping N. pattern - nested Epic format found in PRD');
113
134
  continue;
114
135
  }
136
+ // Skip generic number-only patterns when explicit "Epic N" patterns already matched
137
+ // Generic patterns like "## 1. Goals" false-positive on document section headers
138
+ if (hasExplicitEpicMatches && (pattern.name === 'N: Title' || pattern.name === 'N. Title')) {
139
+ this.logger.debug({ patternName: pattern.name }, 'Skipping generic pattern - explicit Epic patterns already matched');
140
+ continue;
141
+ }
115
142
  const matches = this.matchPattern(pattern, lines, prdContent);
116
143
  if (matches.length > 0) {
117
144
  allMatches.push(...matches);
118
145
  this.logger.debug({ matches: matches.length, patternName: pattern.name }, 'Pattern matched epics');
146
+ // Track if we found explicit "Epic N" patterns (any pattern with "Epic" in the name)
147
+ if (pattern.name.includes('Epic')) {
148
+ hasExplicitEpicMatches = true;
149
+ }
119
150
  }
120
151
  }
121
152
  // Check if any epics were found
@@ -212,12 +243,8 @@ export class PrdParser {
212
243
  const position = this.findLineNumber(lines, fullMatch);
213
244
  // Extract fullNumber based on pattern type
214
245
  let fullNumber;
215
- if (pattern.name.includes('Nested')) {
216
- // For nested format "## N. Epic M:", use "Epic M"
217
- fullNumber = `Epic ${epicNumber}`;
218
- }
219
- else if (pattern.name.startsWith('Epic N')) {
220
- // For Epic patterns, use "Epic N"
246
+ if (pattern.name.includes('Nested') || pattern.name.includes('Epic N') || pattern.name.includes('H3 Epic')) {
247
+ // For explicit Epic patterns (H2, H3, nested), use "Epic N"
221
248
  fullNumber = `Epic ${epicNumber}`;
222
249
  }
223
250
  else {
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.13",
4
+ "version": "1.0.14",
5
5
  "author": {
6
6
  "name": "DevSquad",
7
7
  "email": "marcelo@devsquad.email",