@hyperdrive.bot/bmad-workflow 1.0.2

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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1017 -0
  3. package/bin/dev +5 -0
  4. package/bin/dev.cmd +3 -0
  5. package/bin/dev.js +5 -0
  6. package/bin/run +5 -0
  7. package/bin/run.cmd +3 -0
  8. package/bin/run.js +5 -0
  9. package/dist/commands/config/show.d.ts +34 -0
  10. package/dist/commands/config/show.js +108 -0
  11. package/dist/commands/config/validate.d.ts +29 -0
  12. package/dist/commands/config/validate.js +131 -0
  13. package/dist/commands/decompose.d.ts +79 -0
  14. package/dist/commands/decompose.js +327 -0
  15. package/dist/commands/demo.d.ts +18 -0
  16. package/dist/commands/demo.js +107 -0
  17. package/dist/commands/epics/create.d.ts +123 -0
  18. package/dist/commands/epics/create.js +459 -0
  19. package/dist/commands/epics/list.d.ts +120 -0
  20. package/dist/commands/epics/list.js +280 -0
  21. package/dist/commands/hello/index.d.ts +12 -0
  22. package/dist/commands/hello/index.js +34 -0
  23. package/dist/commands/hello/world.d.ts +8 -0
  24. package/dist/commands/hello/world.js +24 -0
  25. package/dist/commands/prd/fix.d.ts +39 -0
  26. package/dist/commands/prd/fix.js +140 -0
  27. package/dist/commands/prd/validate.d.ts +112 -0
  28. package/dist/commands/prd/validate.js +302 -0
  29. package/dist/commands/stories/create.d.ts +95 -0
  30. package/dist/commands/stories/create.js +431 -0
  31. package/dist/commands/stories/develop.d.ts +91 -0
  32. package/dist/commands/stories/develop.js +460 -0
  33. package/dist/commands/stories/list.d.ts +84 -0
  34. package/dist/commands/stories/list.js +291 -0
  35. package/dist/commands/stories/move.d.ts +66 -0
  36. package/dist/commands/stories/move.js +273 -0
  37. package/dist/commands/stories/qa.d.ts +99 -0
  38. package/dist/commands/stories/qa.js +530 -0
  39. package/dist/commands/workflow.d.ts +97 -0
  40. package/dist/commands/workflow.js +390 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +1 -0
  43. package/dist/models/agent-options.d.ts +50 -0
  44. package/dist/models/agent-options.js +1 -0
  45. package/dist/models/agent-result.d.ts +29 -0
  46. package/dist/models/agent-result.js +1 -0
  47. package/dist/models/index.d.ts +10 -0
  48. package/dist/models/index.js +10 -0
  49. package/dist/models/phase-result.d.ts +65 -0
  50. package/dist/models/phase-result.js +7 -0
  51. package/dist/models/provider.d.ts +28 -0
  52. package/dist/models/provider.js +18 -0
  53. package/dist/models/story.d.ts +154 -0
  54. package/dist/models/story.js +18 -0
  55. package/dist/models/workflow-config.d.ts +148 -0
  56. package/dist/models/workflow-config.js +1 -0
  57. package/dist/models/workflow-result.d.ts +164 -0
  58. package/dist/models/workflow-result.js +7 -0
  59. package/dist/services/agents/agent-runner-factory.d.ts +31 -0
  60. package/dist/services/agents/agent-runner-factory.js +44 -0
  61. package/dist/services/agents/agent-runner.d.ts +46 -0
  62. package/dist/services/agents/agent-runner.js +29 -0
  63. package/dist/services/agents/claude-agent-runner.d.ts +81 -0
  64. package/dist/services/agents/claude-agent-runner.js +332 -0
  65. package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
  66. package/dist/services/agents/gemini-agent-runner.js +350 -0
  67. package/dist/services/agents/index.d.ts +7 -0
  68. package/dist/services/agents/index.js +7 -0
  69. package/dist/services/file-system/file-manager.d.ts +110 -0
  70. package/dist/services/file-system/file-manager.js +223 -0
  71. package/dist/services/file-system/glob-matcher.d.ts +75 -0
  72. package/dist/services/file-system/glob-matcher.js +126 -0
  73. package/dist/services/file-system/path-resolver.d.ts +183 -0
  74. package/dist/services/file-system/path-resolver.js +400 -0
  75. package/dist/services/logging/workflow-logger.d.ts +232 -0
  76. package/dist/services/logging/workflow-logger.js +552 -0
  77. package/dist/services/orchestration/batch-processor.d.ts +113 -0
  78. package/dist/services/orchestration/batch-processor.js +187 -0
  79. package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
  80. package/dist/services/orchestration/dependency-graph-executor.js +447 -0
  81. package/dist/services/orchestration/index.d.ts +10 -0
  82. package/dist/services/orchestration/index.js +8 -0
  83. package/dist/services/orchestration/input-detector.d.ts +125 -0
  84. package/dist/services/orchestration/input-detector.js +381 -0
  85. package/dist/services/orchestration/story-queue.d.ts +94 -0
  86. package/dist/services/orchestration/story-queue.js +170 -0
  87. package/dist/services/orchestration/story-type-detector.d.ts +80 -0
  88. package/dist/services/orchestration/story-type-detector.js +258 -0
  89. package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
  90. package/dist/services/orchestration/task-decomposition-service.js +607 -0
  91. package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
  92. package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
  93. package/dist/services/parsers/epic-parser.d.ts +117 -0
  94. package/dist/services/parsers/epic-parser.js +264 -0
  95. package/dist/services/parsers/prd-fixer.d.ts +86 -0
  96. package/dist/services/parsers/prd-fixer.js +194 -0
  97. package/dist/services/parsers/prd-parser.d.ts +123 -0
  98. package/dist/services/parsers/prd-parser.js +286 -0
  99. package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
  100. package/dist/services/parsers/standalone-story-parser.js +255 -0
  101. package/dist/services/parsers/story-parser-factory.d.ts +81 -0
  102. package/dist/services/parsers/story-parser-factory.js +108 -0
  103. package/dist/services/parsers/story-parser.d.ts +122 -0
  104. package/dist/services/parsers/story-parser.js +262 -0
  105. package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
  106. package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
  107. package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
  108. package/dist/services/scaffolding/file-scaffolder.js +314 -0
  109. package/dist/services/validation/config-validator.d.ts +88 -0
  110. package/dist/services/validation/config-validator.js +167 -0
  111. package/dist/types/task-graph.d.ts +142 -0
  112. package/dist/types/task-graph.js +5 -0
  113. package/dist/utils/colors.d.ts +49 -0
  114. package/dist/utils/colors.js +50 -0
  115. package/dist/utils/error-formatter.d.ts +64 -0
  116. package/dist/utils/error-formatter.js +279 -0
  117. package/dist/utils/errors.d.ts +170 -0
  118. package/dist/utils/errors.js +233 -0
  119. package/dist/utils/formatters.d.ts +84 -0
  120. package/dist/utils/formatters.js +162 -0
  121. package/dist/utils/logger.d.ts +63 -0
  122. package/dist/utils/logger.js +78 -0
  123. package/dist/utils/progress.d.ts +104 -0
  124. package/dist/utils/progress.js +161 -0
  125. package/dist/utils/retry.d.ts +114 -0
  126. package/dist/utils/retry.js +160 -0
  127. package/dist/utils/shared-flags.d.ts +28 -0
  128. package/dist/utils/shared-flags.js +43 -0
  129. package/package.json +119 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Stories QA Command
3
+ *
4
+ * Executes QA workflow for stories matching glob patterns sequentially.
5
+ * Runs QA agent for deep dive review, then Dev agent for fix-forward,
6
+ * with configurable retry loops until gate passes or max retries reached.
7
+ *
8
+ * @example
9
+ * ```bash
10
+ * bmad-workflow stories qa "docs/qa/stories/AUTH-*.md"
11
+ * bmad-workflow stories qa "docs/qa/stories/*.md" --max-retries 3
12
+ * bmad-workflow stories qa "stories/*.md" --qa-prompt "Focus on security"
13
+ * ```
14
+ */
15
+ import { Command } from '@oclif/core';
16
+ /**
17
+ * Stories QA Command
18
+ *
19
+ * Runs QA review and dev fix-forward cycles for stories sequentially.
20
+ * CRITICAL: No parallel execution - stories are processed one at a time.
21
+ */
22
+ export default class StoriesQaCommand extends Command {
23
+ static args: {
24
+ pattern: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
25
+ };
26
+ static description: string;
27
+ static examples: {
28
+ command: string;
29
+ description: string;
30
+ }[];
31
+ static flags: {
32
+ 'dev-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
33
+ interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
34
+ 'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
35
+ provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
36
+ 'qa-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
+ reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
38
+ };
39
+ private agentRunner;
40
+ private fileManager;
41
+ private globMatcher;
42
+ private logger;
43
+ private pathResolver;
44
+ private storyParserFactory;
45
+ /**
46
+ * Run the command
47
+ */
48
+ run(): Promise<void>;
49
+ /**
50
+ * Append QA workflow summary to story file
51
+ */
52
+ private appendQaSummaryToStory;
53
+ /**
54
+ * Build Claude CLI prompt for Dev fix-forward
55
+ */
56
+ private buildDevFixPrompt;
57
+ /**
58
+ * Build Claude CLI prompt for QA review
59
+ */
60
+ private buildQaPrompt;
61
+ /**
62
+ * Display countdown timer between stories
63
+ */
64
+ private displayCountdown;
65
+ /**
66
+ * Display summary report
67
+ */
68
+ private displaySummaryReport;
69
+ /**
70
+ * Extract gate status from story content or gate file
71
+ * This is a simple heuristic - looks for Gate: STATUS pattern in story
72
+ * IMPORTANT: Returns the LAST occurrence since QA reviews are appended
73
+ */
74
+ private extractGateStatus;
75
+ /**
76
+ * Initialize service dependencies
77
+ */
78
+ private initializeServices;
79
+ /**
80
+ * Match story files using glob pattern
81
+ */
82
+ private matchStoryFiles;
83
+ /**
84
+ * Move story to appropriate folder based on gate status
85
+ */
86
+ private moveStoryBasedOnGate;
87
+ /**
88
+ * Process stories sequentially through QA workflow
89
+ */
90
+ private qaStoriesSequentially;
91
+ /**
92
+ * Process a single story through QA workflow
93
+ */
94
+ private qaStory;
95
+ /**
96
+ * Sleep for specified milliseconds
97
+ */
98
+ private sleep;
99
+ }
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Stories QA Command
3
+ *
4
+ * Executes QA workflow for stories matching glob patterns sequentially.
5
+ * Runs QA agent for deep dive review, then Dev agent for fix-forward,
6
+ * with configurable retry loops until gate passes or max retries reached.
7
+ *
8
+ * @example
9
+ * ```bash
10
+ * bmad-workflow stories qa "docs/qa/stories/AUTH-*.md"
11
+ * bmad-workflow stories qa "docs/qa/stories/*.md" --max-retries 3
12
+ * bmad-workflow stories qa "stories/*.md" --qa-prompt "Focus on security"
13
+ * ```
14
+ */
15
+ import { Args, Command, Flags } from '@oclif/core';
16
+ import path from 'node:path';
17
+ import { isEpicStory } from '../../models/story.js';
18
+ import { createAgentRunner } from '../../services/agents/agent-runner-factory.js';
19
+ import { FileManager } from '../../services/file-system/file-manager.js';
20
+ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
21
+ import { PathResolver } from '../../services/file-system/path-resolver.js';
22
+ import { StoryParserFactory } from '../../services/parsers/story-parser-factory.js';
23
+ import * as colors from '../../utils/colors.js';
24
+ import { createLogger, generateCorrelationId } from '../../utils/logger.js';
25
+ import { createSpinner } from '../../utils/progress.js';
26
+ /**
27
+ * Agent timeout in milliseconds (30 minutes)
28
+ * QA and Dev agents need longer timeouts for comprehensive analysis
29
+ */
30
+ const AGENT_TIMEOUT_MS = 1_800_000;
31
+ /**
32
+ * Stories QA Command
33
+ *
34
+ * Runs QA review and dev fix-forward cycles for stories sequentially.
35
+ * CRITICAL: No parallel execution - stories are processed one at a time.
36
+ */
37
+ export default class StoriesQaCommand extends Command {
38
+ static args = {
39
+ pattern: Args.string({
40
+ description: 'Glob pattern to match story files',
41
+ required: true,
42
+ }),
43
+ };
44
+ static description = 'Execute QA workflow for stories: deep dive review, dev fix-forward, and gate validation';
45
+ static examples = [
46
+ {
47
+ command: '<%= config.bin %> <%= command.id %> "docs/qa/stories/AUTH-*.md"',
48
+ description: 'QA all AUTH stories in qa folder',
49
+ },
50
+ {
51
+ command: '<%= config.bin %> <%= command.id %> "docs/qa/stories/*.md" --max-retries 3',
52
+ description: 'QA stories with up to 3 fix-forward cycles',
53
+ },
54
+ {
55
+ command: '<%= config.bin %> <%= command.id %> "stories/*.md" --qa-prompt "Focus on security vulnerabilities"',
56
+ description: 'QA with custom prompt instructions',
57
+ },
58
+ {
59
+ command: '<%= config.bin %> <%= command.id %> "stories/*.md" --interval 60',
60
+ description: 'QA stories with 60s countdown between each',
61
+ },
62
+ ];
63
+ static flags = {
64
+ 'dev-prompt': Flags.string({
65
+ description: 'Custom prompt/instructions for dev fix-forward phase',
66
+ helpGroup: 'Prompt Customization',
67
+ }),
68
+ interval: Flags.integer({
69
+ default: 30,
70
+ description: 'Seconds to wait between stories',
71
+ }),
72
+ 'max-retries': Flags.integer({
73
+ default: 2,
74
+ description: 'Maximum QA → Dev fix cycles before giving up (0 = QA only, no fix-forward)',
75
+ }),
76
+ provider: Flags.string({
77
+ default: 'claude',
78
+ description: 'AI provider to use (claude or gemini)',
79
+ options: ['claude', 'gemini'],
80
+ }),
81
+ 'qa-prompt': Flags.string({
82
+ description: 'Custom prompt/instructions for QA review phase',
83
+ helpGroup: 'Prompt Customization',
84
+ }),
85
+ reference: Flags.string({
86
+ description: 'Additional context files for agents',
87
+ multiple: true,
88
+ }),
89
+ };
90
+ // Service instances
91
+ agentRunner;
92
+ fileManager;
93
+ globMatcher;
94
+ logger;
95
+ pathResolver;
96
+ storyParserFactory;
97
+ /**
98
+ * Run the command
99
+ */
100
+ async run() {
101
+ const { args, flags } = await this.parse(StoriesQaCommand);
102
+ const startTime = Date.now();
103
+ const correlationId = generateCorrelationId();
104
+ // Initialize services with selected provider
105
+ const provider = (flags.provider || 'claude');
106
+ this.initializeServices(provider);
107
+ this.logger.info({
108
+ correlationId,
109
+ flags,
110
+ pattern: args.pattern,
111
+ }, 'Starting stories QA command');
112
+ try {
113
+ // Match story files using glob pattern
114
+ const storyFiles = await this.matchStoryFiles(args.pattern);
115
+ if (storyFiles.length === 0) {
116
+ this.log(colors.warning(`No story files matched pattern: ${args.pattern}`));
117
+ return;
118
+ }
119
+ this.log(colors.info(`Found ${storyFiles.length} story file(s) to QA`));
120
+ this.log('');
121
+ // Process stories sequentially
122
+ const results = await this.qaStoriesSequentially(storyFiles, flags);
123
+ // Display summary report
124
+ const duration = Date.now() - startTime;
125
+ this.displaySummaryReport(results, duration);
126
+ this.logger.info({
127
+ correlationId,
128
+ duration,
129
+ failed: results.filter((r) => !r.success).length,
130
+ passed: results.filter((r) => r.finalGate === 'PASS').length,
131
+ succeeded: results.filter((r) => r.success).length,
132
+ total: results.length,
133
+ }, 'Stories QA command completed');
134
+ // Exit with error if any stories failed
135
+ const failedCount = results.filter((r) => !r.success).length;
136
+ if (failedCount > 0) {
137
+ this.error(colors.error(`${failedCount} story QA(s) failed`), { exit: 1 });
138
+ }
139
+ }
140
+ catch (error) {
141
+ const err = error;
142
+ this.logger.error({ correlationId, error: err }, 'Command failed');
143
+ this.error(colors.error(err.message), { exit: 1 });
144
+ }
145
+ }
146
+ /**
147
+ * Append QA workflow summary to story file
148
+ */
149
+ async appendQaSummaryToStory(storyPath, result, totalRetries) {
150
+ const content = await this.fileManager.readFile(storyPath);
151
+ const timestamp = new Date().toISOString();
152
+ const summary = `
153
+
154
+ ## QA Workflow Summary
155
+
156
+ ### Workflow Date: ${timestamp.split('T')[0]}
157
+
158
+ ### Final Gate Status: ${result.finalGate}
159
+
160
+ ### Workflow Statistics
161
+ - Retries Used: ${result.retriesUsed}/${totalRetries}
162
+ - Final Location: ${result.movedTo === 'done' ? 'docs/done/stories' : result.movedTo === 'stories' ? 'docs/stories (returned for rework)' : 'unchanged'}
163
+ - Success: ${result.success ? 'Yes' : 'No'}
164
+ ${result.error ? `- Error: ${result.error}` : ''}
165
+
166
+ ### Workflow Phases Completed
167
+ 1. [${result.retriesUsed >= 0 ? 'x' : ' '}] Initial QA Deep Dive Review
168
+ ${result.retriesUsed > 0 ? `2. [x] Dev Fix-Forward (${result.retriesUsed} cycle${result.retriesUsed > 1 ? 's' : ''})` : ''}
169
+ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.finalGate === 'WAIVED' ? '3. [x] Final QA Validation - WAIVED' : '3. [ ] Final QA Validation - Not Passed'}
170
+ `;
171
+ // Append summary to file
172
+ await this.fileManager.writeFile(storyPath, content + summary);
173
+ this.logger.info({ storyPath }, 'Appended QA workflow summary to story');
174
+ }
175
+ /**
176
+ * Build Claude CLI prompt for Dev fix-forward
177
+ */
178
+ buildDevFixPrompt(storyPath, customPrompt, references) {
179
+ let prompt = `@.bmad-core/agents/dev.md\n\n`;
180
+ prompt += `*review-qa ${storyPath}\n\n`;
181
+ if (references && references.length > 0) {
182
+ prompt += 'References:\n';
183
+ for (const ref of references) {
184
+ const resolvedPath = path.resolve(ref);
185
+ prompt += `@${resolvedPath}\n`;
186
+ }
187
+ prompt += '\n';
188
+ }
189
+ // Default dev fix-forward instructions
190
+ const defaultInstructions = `Fix forward based on QA findings:
191
+ - Read the QA Results section and gate file for this story
192
+ - Address all high-severity issues first
193
+ - Fix medium and low severity issues
194
+ - Add missing tests to close coverage gaps
195
+ - Update the story's Dev Agent Record section
196
+ - Run all tests to ensure fixes don't break functionality
197
+ - Set status to Ready for Review when complete`;
198
+ prompt += customPrompt || defaultInstructions;
199
+ prompt += '\n\n*yolo mode* always update the story file as you go\n';
200
+ return prompt;
201
+ }
202
+ /**
203
+ * Build Claude CLI prompt for QA review
204
+ */
205
+ buildQaPrompt(storyPath, customPrompt, references) {
206
+ let prompt = `@.bmad-core/agents/qa.md\n\n`;
207
+ prompt += `*review ${storyPath}\n\n`;
208
+ if (references && references.length > 0) {
209
+ prompt += 'References:\n';
210
+ for (const ref of references) {
211
+ const resolvedPath = path.resolve(ref);
212
+ prompt += `@${resolvedPath}\n`;
213
+ }
214
+ prompt += '\n';
215
+ }
216
+ // Default QA deep dive instructions
217
+ const defaultInstructions = `Perform a comprehensive deep dive review of this story implementation:
218
+ - Analyze each acceptance criterion against the actual implementation
219
+ - Review code quality, architecture patterns, and best practices
220
+ - Check test coverage and test quality
221
+ - Identify security vulnerabilities and performance issues
222
+ - Validate error handling and edge cases
223
+ - Assess technical debt and maintainability
224
+ - Update the story's QA Results section with detailed findings
225
+ - Create/update the gate file with your assessment (PASS/CONCERNS/FAIL/WAIVED)`;
226
+ prompt += customPrompt || defaultInstructions;
227
+ prompt += '\n\n*yolo mode* always update the story file as you go\n';
228
+ return prompt;
229
+ }
230
+ /**
231
+ * Display countdown timer between stories
232
+ */
233
+ async displayCountdown(intervalSeconds) {
234
+ /* eslint-disable no-await-in-loop */
235
+ for (let remaining = intervalSeconds; remaining > 0; remaining--) {
236
+ process.stdout.write(`\r${colors.warning(`⏳ Next story in ${remaining}s...`)}`);
237
+ await this.sleep(1000);
238
+ }
239
+ /* eslint-enable no-await-in-loop */
240
+ process.stdout.write('\r' + ' '.repeat(40) + '\r'); // Clear countdown line
241
+ }
242
+ /**
243
+ * Display summary report
244
+ */
245
+ displaySummaryReport(results, duration) {
246
+ const passCount = results.filter((r) => r.finalGate === 'PASS').length;
247
+ const concernsCount = results.filter((r) => r.finalGate === 'CONCERNS').length;
248
+ const failCount = results.filter((r) => r.finalGate === 'FAIL').length;
249
+ const waivedCount = results.filter((r) => r.finalGate === 'WAIVED').length;
250
+ const errorCount = results.filter((r) => !r.success).length;
251
+ const movedToDone = results.filter((r) => r.movedTo === 'done').length;
252
+ const movedBack = results.filter((r) => r.movedTo === 'stories').length;
253
+ // Box drawing
254
+ const boxTop = '┌─────────────────────────────────────────┐';
255
+ const boxDivider = '├─────────────────────────────────────────┤';
256
+ const boxBottom = '└─────────────────────────────────────────┘';
257
+ this.log('');
258
+ this.log(boxTop);
259
+ this.log('│ Story QA Summary │');
260
+ this.log(boxDivider);
261
+ this.log(`│ ${colors.success('PASS:')} ${passCount.toString().padEnd(20)}│`);
262
+ if (concernsCount > 0) {
263
+ this.log(`│ ${colors.warning('CONCERNS:')} ${concernsCount.toString().padEnd(20)}│`);
264
+ }
265
+ if (failCount > 0) {
266
+ this.log(`│ ${colors.error('FAIL:')} ${failCount.toString().padEnd(20)}│`);
267
+ }
268
+ if (waivedCount > 0) {
269
+ this.log(`│ WAIVED: ${waivedCount.toString().padEnd(20)}│`);
270
+ }
271
+ if (errorCount > 0) {
272
+ this.log(`│ ${colors.error('Errors:')} ${errorCount.toString().padEnd(20)}│`);
273
+ }
274
+ this.log(boxDivider);
275
+ this.log(`│ Moved to Done: ${movedToDone.toString().padEnd(20)}│`);
276
+ this.log(`│ Returned to Dev: ${movedBack.toString().padEnd(20)}│`);
277
+ this.log(boxDivider);
278
+ this.log(`│ Duration: ${(duration / 1000).toFixed(2)}s${' '.repeat(15)}│`);
279
+ this.log(boxBottom);
280
+ // List details for non-PASS stories
281
+ const nonPassStories = results.filter((r) => r.finalGate !== 'PASS' || !r.success);
282
+ if (nonPassStories.length > 0) {
283
+ this.log('');
284
+ this.log(colors.bold('Stories Requiring Attention:'));
285
+ for (const result of nonPassStories) {
286
+ const status = result.success ? result.finalGate : 'ERROR';
287
+ const statusColor = status === 'CONCERNS' ? colors.warning : colors.error;
288
+ this.log(statusColor(` ${status}: ${result.storyNumber} - ${path.basename(result.storyPath)}${result.error ? ` (${result.error})` : ''}`));
289
+ }
290
+ }
291
+ this.log('');
292
+ }
293
+ /**
294
+ * Extract gate status from story content or gate file
295
+ * This is a simple heuristic - looks for Gate: STATUS pattern in story
296
+ * IMPORTANT: Returns the LAST occurrence since QA reviews are appended
297
+ */
298
+ extractGateStatus(storyContent) {
299
+ // Look for ALL "Gate: STATUS" patterns and use the LAST one (most recent review)
300
+ const gateMatches = [...storyContent.matchAll(/\*?\*?Gate:\*?\*?\s*(PASS|CONCERNS|FAIL|WAIVED)/gi)];
301
+ if (gateMatches.length > 0) {
302
+ const lastMatch = gateMatches.at(-1);
303
+ return lastMatch[1].toUpperCase();
304
+ }
305
+ // Look for "Gate Status" section with status (also get last match)
306
+ const statusMatches = [...storyContent.matchAll(/###\s*Gate\s*Status[\s\S]*?(PASS|CONCERNS|FAIL|WAIVED)/gi)];
307
+ if (statusMatches.length > 0) {
308
+ const lastMatch = statusMatches.at(-1);
309
+ return lastMatch[1].toUpperCase();
310
+ }
311
+ return 'UNKNOWN';
312
+ }
313
+ /**
314
+ * Initialize service dependencies
315
+ */
316
+ initializeServices(provider = 'claude') {
317
+ this.logger = createLogger({ namespace: 'commands:stories:qa' });
318
+ this.logger.info({ provider }, 'Initializing services with AI provider');
319
+ this.fileManager = new FileManager(this.logger);
320
+ this.pathResolver = new PathResolver(this.fileManager, this.logger);
321
+ this.globMatcher = new GlobMatcher(this.fileManager, this.logger);
322
+ this.storyParserFactory = new StoryParserFactory(this.fileManager, this.logger);
323
+ this.agentRunner = createAgentRunner(provider, this.logger);
324
+ this.logger.debug({ provider }, 'Services initialized successfully');
325
+ }
326
+ /**
327
+ * Match story files using glob pattern
328
+ */
329
+ async matchStoryFiles(pattern) {
330
+ this.logger.info({ pattern }, 'Matching story files with glob pattern');
331
+ // Expand glob pattern
332
+ const matches = await this.globMatcher.expandPattern(pattern);
333
+ this.logger.info({ matchCount: matches.length, pattern }, 'Glob pattern matching complete');
334
+ return matches;
335
+ }
336
+ /**
337
+ * Move story to appropriate folder based on gate status
338
+ */
339
+ async moveStoryBasedOnGate(storyPath, gateStatus) {
340
+ try {
341
+ if (gateStatus === 'PASS' || gateStatus === 'WAIVED') {
342
+ // Move to done/stories
343
+ const doneStoryDir = this.pathResolver.getDoneStoryDir();
344
+ const destPath = path.join(doneStoryDir, path.basename(storyPath));
345
+ // Ensure done directory exists
346
+ await this.fileManager.createDirectory(doneStoryDir);
347
+ await this.fileManager.moveFile(storyPath, destPath);
348
+ this.logger.info({ destPath, gateStatus, storyPath }, 'Moved story to done folder');
349
+ return 'done';
350
+ }
351
+ if (gateStatus === 'FAIL' || gateStatus === 'CONCERNS') {
352
+ // Move back to stories for rework
353
+ const storyDir = this.pathResolver.getStoryDir();
354
+ const destPath = path.join(storyDir, path.basename(storyPath));
355
+ // Only move if not already in stories dir (normalize paths for comparison)
356
+ if (path.resolve(path.dirname(storyPath)) !== path.resolve(storyDir)) {
357
+ await this.fileManager.moveFile(storyPath, destPath);
358
+ this.logger.info({ destPath, gateStatus, storyPath }, 'Moved story back to stories folder for rework');
359
+ return 'stories';
360
+ }
361
+ return 'none';
362
+ }
363
+ return 'none';
364
+ }
365
+ catch (error) {
366
+ const err = error;
367
+ this.logger.error({ error: err, storyPath }, 'Failed to move story');
368
+ return 'none';
369
+ }
370
+ }
371
+ /**
372
+ * Process stories sequentially through QA workflow
373
+ */
374
+ async qaStoriesSequentially(storyFiles, flags) {
375
+ const results = [];
376
+ const total = storyFiles.length;
377
+ /* eslint-disable no-await-in-loop */
378
+ // Sequential loop - DO NOT use Promise.all - await in loop is REQUIRED here
379
+ for (let index = 0; index < storyFiles.length; index++) {
380
+ const storyPath = storyFiles[index];
381
+ const storyNum = index + 1;
382
+ this.log(colors.bold(`\n[${storyNum}/${total}] QA: ${path.basename(storyPath)}`));
383
+ // Parse story metadata
384
+ const spinner = createSpinner('Parsing story metadata...');
385
+ spinner.start();
386
+ let storyMetadata;
387
+ try {
388
+ storyMetadata = await this.storyParserFactory.parseStory(storyPath);
389
+ const storyId = storyMetadata.id;
390
+ spinner.succeed(colors.success(`Story ${storyId}: ${storyMetadata.title}`));
391
+ }
392
+ catch (error) {
393
+ const err = error;
394
+ spinner.fail(colors.error(`Failed to parse story: ${err.message}`));
395
+ results.push({
396
+ error: `Parse error: ${err.message}`,
397
+ finalGate: 'UNKNOWN',
398
+ movedTo: 'none',
399
+ retriesUsed: 0,
400
+ storyNumber: 'unknown',
401
+ storyPath,
402
+ success: false,
403
+ });
404
+ continue; // Skip to next story
405
+ }
406
+ // Run QA workflow
407
+ const result = await this.qaStory(storyPath, storyMetadata, {
408
+ 'dev-prompt': flags['dev-prompt'],
409
+ 'max-retries': flags['max-retries'],
410
+ 'qa-prompt': flags['qa-prompt'],
411
+ reference: flags.reference,
412
+ });
413
+ results.push(result);
414
+ // Display result
415
+ if (result.finalGate === 'PASS') {
416
+ this.log(colors.success(` ✓ PASSED - Moved to done`));
417
+ }
418
+ else if (result.finalGate === 'WAIVED') {
419
+ this.log(colors.warning(` ⚠ WAIVED - Moved to done`));
420
+ }
421
+ else if (result.success) {
422
+ this.log(colors.warning(` ⚠ ${result.finalGate} - Returned for rework`));
423
+ }
424
+ else {
425
+ this.log(colors.error(` ✗ Error: ${result.error}`));
426
+ }
427
+ // Display countdown timer before next story (except for last story)
428
+ if (index < storyFiles.length - 1) {
429
+ await this.displayCountdown(flags.interval);
430
+ }
431
+ }
432
+ /* eslint-enable no-await-in-loop */
433
+ return results;
434
+ }
435
+ /**
436
+ * Process a single story through QA workflow
437
+ */
438
+ async qaStory(storyPath, storyMetadata, flags) {
439
+ const storyNumber = isEpicStory(storyMetadata) ? storyMetadata.number : storyMetadata.id;
440
+ let retriesUsed = 0;
441
+ let currentGate = 'UNKNOWN';
442
+ this.logger.info({ storyNumber, storyPath }, 'Starting story QA workflow');
443
+ try {
444
+ // Phase 1: Initial QA Deep Dive
445
+ this.log(colors.info(' Phase 1: QA Deep Dive Review...'));
446
+ const qaPrompt = this.buildQaPrompt(storyPath, flags['qa-prompt'], flags.reference);
447
+ const qaResult = await this.agentRunner.runAgent(qaPrompt, {
448
+ agentType: 'tea',
449
+ timeout: AGENT_TIMEOUT_MS,
450
+ });
451
+ if (!qaResult.success) {
452
+ throw new Error(`QA agent failed: ${qaResult.errors}`);
453
+ }
454
+ // Read story to get gate status
455
+ let storyContent = await this.fileManager.readFile(storyPath);
456
+ currentGate = this.extractGateStatus(storyContent);
457
+ this.log(colors.info(` Initial Gate: ${currentGate}`));
458
+ // Phase 2: Dev Fix-Forward Loop (if needed and retries allowed)
459
+ while ((currentGate === 'CONCERNS' || currentGate === 'FAIL') && retriesUsed < flags['max-retries']) {
460
+ retriesUsed++;
461
+ this.log(colors.warning(` Phase 2: Dev Fix-Forward (Retry ${retriesUsed}/${flags['max-retries']})...`));
462
+ // Run Dev agent to fix issues (sequential retry loop by design)
463
+ const devPrompt = this.buildDevFixPrompt(storyPath, flags['dev-prompt'], flags.reference);
464
+ // eslint-disable-next-line no-await-in-loop
465
+ const devResult = await this.agentRunner.runAgent(devPrompt, {
466
+ agentType: 'dev',
467
+ timeout: AGENT_TIMEOUT_MS,
468
+ });
469
+ if (!devResult.success) {
470
+ this.logger.warn({ errors: devResult.errors, retriesUsed }, 'Dev fix-forward failed, continuing...');
471
+ // Continue anyway - QA will re-evaluate
472
+ }
473
+ // Phase 3: Re-run QA to validate fixes
474
+ this.log(colors.info(` Phase 3: QA Re-validation (Retry ${retriesUsed})...`));
475
+ // eslint-disable-next-line no-await-in-loop
476
+ const reQaResult = await this.agentRunner.runAgent(qaPrompt, {
477
+ agentType: 'tea',
478
+ timeout: AGENT_TIMEOUT_MS,
479
+ });
480
+ if (!reQaResult.success) {
481
+ this.logger.warn({ errors: reQaResult.errors, retriesUsed }, 'QA re-validation failed, continuing...');
482
+ }
483
+ // Re-read story to get updated gate status
484
+ // eslint-disable-next-line no-await-in-loop
485
+ storyContent = await this.fileManager.readFile(storyPath);
486
+ currentGate = this.extractGateStatus(storyContent);
487
+ this.log(colors.info(` Gate after retry ${retriesUsed}: ${currentGate}`));
488
+ }
489
+ // Append QA workflow summary to story
490
+ const result = {
491
+ finalGate: currentGate,
492
+ movedTo: 'none',
493
+ retriesUsed,
494
+ storyNumber,
495
+ storyPath,
496
+ success: true,
497
+ };
498
+ await this.appendQaSummaryToStory(storyPath, result, flags['max-retries']);
499
+ // Move story based on final gate status
500
+ result.movedTo = await this.moveStoryBasedOnGate(storyPath, currentGate);
501
+ this.logger.info({
502
+ finalGate: currentGate,
503
+ movedTo: result.movedTo,
504
+ retriesUsed,
505
+ storyNumber,
506
+ }, 'Story QA workflow completed');
507
+ return result;
508
+ }
509
+ catch (error) {
510
+ const err = error;
511
+ this.logger.error({ error: err, storyNumber, storyPath }, 'Story QA workflow failed');
512
+ return {
513
+ error: err.message,
514
+ finalGate: currentGate,
515
+ movedTo: 'none',
516
+ retriesUsed,
517
+ storyNumber,
518
+ storyPath,
519
+ success: false,
520
+ };
521
+ }
522
+ }
523
+ /**
524
+ * Sleep for specified milliseconds
525
+ */
526
+ async sleep(ms) {
527
+ // eslint-disable-next-line no-promise-executor-return -- Simple setTimeout wrapper
528
+ return new Promise((resolve) => setTimeout(resolve, ms));
529
+ }
530
+ }