@hyperdrive.bot/bmad-workflow 1.0.12 → 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.
@@ -390,10 +390,11 @@ export declare class WorkflowOrchestrator {
390
390
  /**
391
391
  * Execute QA phase
392
392
  *
393
- * Runs QA workflow on all stories in the QA folder.
393
+ * Runs QA workflow on stories matching the workflow prefix.
394
394
  * Dynamically imports and delegates to StoriesQaCommand.
395
395
  *
396
396
  * @param config - Workflow configuration
397
+ * @param detection - Input detection result for prefix resolution
397
398
  * @returns PhaseResult with success count, failures, and duration
398
399
  * @private
399
400
  */
@@ -402,6 +403,7 @@ export declare class WorkflowOrchestrator {
402
403
  * Execute QA phase if needed
403
404
  *
404
405
  * @param config - Workflow configuration
406
+ * @param detection - Input detection result for prefix resolution
405
407
  * @param devPhase - Dev phase result
406
408
  * @param shouldExecute - Whether to execute QA phase
407
409
  * @returns QA phase result
@@ -112,7 +112,7 @@ export class WorkflowOrchestrator {
112
112
  return this.buildFailureResult(startTime, epicPhase);
113
113
  }
114
114
  const { devPhase, storyPhase } = await this.executeStoryAndDevPhases(config, detection, epicPhase, phaseFlags, startTime);
115
- const qaPhase = await this.executeQaPhaseIfNeeded(config, devPhase, phaseFlags.shouldExecuteQaPhase);
115
+ const qaPhase = await this.executeQaPhaseIfNeeded(config, detection, devPhase, phaseFlags.shouldExecuteQaPhase);
116
116
  return this.buildSuccessResult(startTime, epicPhase, storyPhase, devPhase, qaPhase);
117
117
  }
118
118
  /**
@@ -1126,14 +1126,15 @@ Write output to: ${outputPath}`;
1126
1126
  /**
1127
1127
  * Execute QA phase
1128
1128
  *
1129
- * Runs QA workflow on all stories in the QA folder.
1129
+ * Runs QA workflow on stories matching the workflow prefix.
1130
1130
  * Dynamically imports and delegates to StoriesQaCommand.
1131
1131
  *
1132
1132
  * @param config - Workflow configuration
1133
+ * @param detection - Input detection result for prefix resolution
1133
1134
  * @returns PhaseResult with success count, failures, and duration
1134
1135
  * @private
1135
1136
  */
1136
- async executeQaPhase(config) {
1137
+ async executeQaPhase(config, detection) {
1137
1138
  const startTime = Date.now();
1138
1139
  const failures = [];
1139
1140
  let successCount = 0;
@@ -1143,8 +1144,9 @@ Write output to: ${outputPath}`;
1143
1144
  try {
1144
1145
  // Get QA story directory
1145
1146
  const qaStoryDir = await this.pathResolver.getQaStoryDir();
1146
- // Find all stories in QA folder
1147
- const storyPattern = '*.md';
1147
+ // Find stories matching the workflow prefix
1148
+ const prefix = this.resolvePrefix(config, detection);
1149
+ const storyPattern = `${prefix}-story-*.md`;
1148
1150
  const storyFiles = await this.fileManager.listFiles(qaStoryDir, storyPattern);
1149
1151
  if (storyFiles.length === 0) {
1150
1152
  this.logger.info('No stories found in QA folder, skipping QA phase');
@@ -1159,9 +1161,9 @@ Write output to: ${outputPath}`;
1159
1161
  this.logger.info({ storyCount: storyFiles.length }, 'Found stories for QA phase');
1160
1162
  // Dynamically import QA command to avoid circular dependencies
1161
1163
  const { default: StoriesQaCommand } = await import('../../commands/stories/qa.js');
1162
- // Process each story through QA
1164
+ // Process each story through QA (storyFile is already an absolute path from listFiles)
1163
1165
  for (const storyFile of storyFiles) {
1164
- const storyPath = `${qaStoryDir}/${storyFile}`;
1166
+ const storyPath = storyFile;
1165
1167
  this.logger.info({ storyPath }, 'Running QA workflow for story');
1166
1168
  try {
1167
1169
  // Build args for QA command
@@ -1229,12 +1231,13 @@ Write output to: ${outputPath}`;
1229
1231
  * Execute QA phase if needed
1230
1232
  *
1231
1233
  * @param config - Workflow configuration
1234
+ * @param detection - Input detection result for prefix resolution
1232
1235
  * @param devPhase - Dev phase result
1233
1236
  * @param shouldExecute - Whether to execute QA phase
1234
1237
  * @returns QA phase result
1235
1238
  * @private
1236
1239
  */
1237
- async executeQaPhaseIfNeeded(config, devPhase, shouldExecute) {
1240
+ async executeQaPhaseIfNeeded(config, detection, devPhase, shouldExecute) {
1238
1241
  if (!shouldExecute || !devPhase || devPhase.success === 0) {
1239
1242
  return this.createSkippedPhaseResult('qa');
1240
1243
  }
@@ -1242,7 +1245,7 @@ Write output to: ${outputPath}`;
1242
1245
  this.logger.info('[DRY RUN] Would execute QA phase');
1243
1246
  return this.createSkippedPhaseResult('qa');
1244
1247
  }
1245
- return this.executeQaPhase(config);
1248
+ return this.executeQaPhase(config, detection);
1246
1249
  }
1247
1250
  /**
1248
1251
  * Execute dev phase in sequential mode
@@ -1373,9 +1376,15 @@ Write output to: ${outputPath}`;
1373
1376
  this.logger.warn({ epicNumber: epic.number }, 'Epic file not found, skipping');
1374
1377
  return [];
1375
1378
  }
1376
- const epicContent = await this.fileManager.readFile(epicFilePath);
1377
- const stories = this.epicParser.parseStories(epicContent, epicFilePath);
1378
- 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
+ }
1379
1388
  }));
1380
1389
  // Flatten the array of story arrays
1381
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.12",
4
+ "version": "1.0.14",
5
5
  "author": {
6
6
  "name": "DevSquad",
7
7
  "email": "marcelo@devsquad.email",