@hyperdrive.bot/bmad-workflow 1.0.18 → 1.0.19

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 (98) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +5 -2
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +8 -0
  36. package/dist/commands/workflow.js +110 -2
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/workflow-config.d.ts +77 -0
  45. package/dist/models/workflow-result.d.ts +7 -0
  46. package/dist/services/agents/claude-agent-runner.js +19 -3
  47. package/dist/services/file-system/path-resolver.d.ts +10 -0
  48. package/dist/services/file-system/path-resolver.js +12 -0
  49. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  50. package/dist/services/mcp/mcp-config-manager.js +146 -0
  51. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  52. package/dist/services/mcp/mcp-context-injector.js +168 -0
  53. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  54. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  55. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  56. package/dist/services/mcp/mcp-health-checker.js +162 -0
  57. package/dist/services/mcp/types/health-types.d.ts +31 -0
  58. package/dist/services/mcp/types/health-types.js +7 -0
  59. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  60. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  61. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  62. package/dist/services/orchestration/workflow-orchestrator.d.ts +54 -2
  63. package/dist/services/orchestration/workflow-orchestrator.js +303 -17
  64. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  65. package/dist/services/review/ai-review-scanner.js +142 -0
  66. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  67. package/dist/services/review/coderabbit-scanner.js +31 -0
  68. package/dist/services/review/index.d.ts +20 -0
  69. package/dist/services/review/index.js +15 -0
  70. package/dist/services/review/lint-scanner.d.ts +46 -0
  71. package/dist/services/review/lint-scanner.js +172 -0
  72. package/dist/services/review/review-config.d.ts +62 -0
  73. package/dist/services/review/review-config.js +91 -0
  74. package/dist/services/review/review-phase-executor.d.ts +69 -0
  75. package/dist/services/review/review-phase-executor.js +152 -0
  76. package/dist/services/review/review-queue.d.ts +98 -0
  77. package/dist/services/review/review-queue.js +174 -0
  78. package/dist/services/review/review-reporter.d.ts +94 -0
  79. package/dist/services/review/review-reporter.js +386 -0
  80. package/dist/services/review/scanner-factory.d.ts +42 -0
  81. package/dist/services/review/scanner-factory.js +60 -0
  82. package/dist/services/review/self-heal-loop.d.ts +58 -0
  83. package/dist/services/review/self-heal-loop.js +132 -0
  84. package/dist/services/review/severity-classifier.d.ts +17 -0
  85. package/dist/services/review/severity-classifier.js +314 -0
  86. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  87. package/dist/services/review/tech-debt-tracker.js +245 -0
  88. package/dist/services/review/types.d.ts +93 -0
  89. package/dist/services/review/types.js +23 -0
  90. package/dist/services/validation/config-validator.d.ts +84 -0
  91. package/dist/services/validation/config-validator.js +78 -0
  92. package/dist/utils/credential-utils.d.ts +14 -0
  93. package/dist/utils/credential-utils.js +19 -0
  94. package/dist/utils/duration.d.ts +41 -0
  95. package/dist/utils/duration.js +89 -0
  96. package/dist/utils/shared-flags.d.ts +1 -0
  97. package/dist/utils/shared-flags.js +11 -2
  98. package/package.json +4 -2
@@ -53,6 +53,8 @@ import { EpicParser } from '../parsers/epic-parser.js';
53
53
  import { PrdParser } from '../parsers/prd-parser.js';
54
54
  import { BatchProcessor } from './batch-processor.js';
55
55
  import { StoryTypeDetector } from './story-type-detector.js';
56
+ import type { McpContextInjector } from '../mcp/mcp-context-injector.js';
57
+ import type { ReviewPhaseExecutor } from '../review/review-phase-executor.js';
56
58
  /**
57
59
  * InputDetector interface (to be implemented in Story 4.2)
58
60
  *
@@ -87,10 +89,14 @@ export interface WorkflowOrchestratorConfig {
87
89
  inputDetector: InputDetector;
88
90
  /** Logger instance for structured logging */
89
91
  logger: pino.Logger;
92
+ /** Optional MCP context injector for tool discovery in agent prompts */
93
+ mcpContextInjector?: McpContextInjector;
90
94
  /** Service to resolve file paths from config */
91
95
  pathResolver: PathResolver;
92
96
  /** Service to parse PRD files and extract epics */
93
97
  prdParser: PrdParser;
98
+ /** Optional ReviewPhaseExecutor for automated code review */
99
+ reviewPhaseExecutor?: ReviewPhaseExecutor;
94
100
  /** Service to detect story type for auto-documentation */
95
101
  storyTypeDetector: StoryTypeDetector;
96
102
  /** Optional WorkflowLogger for pipeline progress tracking */
@@ -144,9 +150,11 @@ export declare class WorkflowOrchestrator {
144
150
  private readonly fileScaffolder;
145
151
  private readonly inputDetector;
146
152
  private readonly logger;
153
+ private readonly mcpContextInjector?;
147
154
  private readonly pathResolver;
148
155
  private readonly prdFixer;
149
156
  private readonly prdParser;
157
+ private readonly reviewPhaseExecutor?;
150
158
  private readonly storyTypeDetector;
151
159
  private readonly workflowLogger?;
152
160
  /**
@@ -166,6 +174,20 @@ export declare class WorkflowOrchestrator {
166
174
  * @throws {ValidationError} If configuration is invalid
167
175
  */
168
176
  execute(config: WorkflowConfig): Promise<WorkflowResult>;
177
+ /**
178
+ * Get MCP prompt prefix for a given phase
179
+ *
180
+ * Returns MCP tool discovery instructions to prepend before agent prompts.
181
+ * Returns empty string when MCP is disabled, injector is unavailable,
182
+ * phase is filtered out, or injector returns empty/errors.
183
+ *
184
+ * @param phase - Workflow phase (epic, story, dev, review, qa)
185
+ * @param agentType - Agent type being spawned
186
+ * @param config - Workflow configuration
187
+ * @returns Instructions string to prepend, or empty string
188
+ * @private
189
+ */
190
+ private getMcpPromptPrefix;
169
191
  /**
170
192
  * Build prompt for epic creation
171
193
  *
@@ -411,11 +433,41 @@ export declare class WorkflowOrchestrator {
411
433
  */
412
434
  private executePipelinedDevPhase;
413
435
  /**
414
- * Execute pipelined workflow (story and dev phases in parallel)
436
+ * Review worker that consumes stories from the ReviewQueue and processes them.
437
+ *
438
+ * Worker loop:
439
+ * 1. Dequeue next story (blocks/waits if queue empty)
440
+ * 2. If null returned, queue is closed and empty → terminate worker
441
+ * 3. For each story, invoke ReviewPhaseExecutor.reviewStory()
442
+ * 4. Collect pass/fail results
443
+ *
444
+ * @param workerId - Worker identifier for logging
445
+ * @param reviewQueue - ReviewQueue to dequeue stories from
446
+ * @param config - Workflow configuration
447
+ * @returns Worker result with passed and failed story arrays
448
+ * @private
449
+ */
450
+ private reviewWorker;
451
+ /**
452
+ * Execute pipelined review phase with worker pool.
453
+ *
454
+ * @param reviewQueue - ReviewQueue to consume stories from
455
+ * @param config - Workflow configuration
456
+ * @returns PhaseResult with aggregate success/failure counts and passed stories
457
+ * @private
458
+ */
459
+ private executePipelinedReviewPhase;
460
+ /**
461
+ * Execute pipelined workflow (story, dev, and optionally review phases in parallel)
462
+ *
463
+ * When config.review === true && reviewPhaseExecutor is available:
464
+ * [StoryQueue] → dev workers → [ReviewQueue] → review workers → QA collection
465
+ * When config.review === false (default):
466
+ * [StoryQueue] → dev workers → QA collection (unchanged behavior)
415
467
  *
416
468
  * @param config - Workflow configuration
417
469
  * @param detection - Input detection result
418
- * @returns Story and dev phase results
470
+ * @returns Story, dev, and optional review phase results
419
471
  * @private
420
472
  */
421
473
  private executePipelinedWorkflow;
@@ -50,6 +50,7 @@ import { PrdFixer } from '../parsers/prd-fixer.js';
50
50
  import { FileScaffolder } from '../scaffolding/file-scaffolder.js';
51
51
  import { BatchProcessor } from './batch-processor.js';
52
52
  import { StoryQueue } from './story-queue.js';
53
+ import { ReviewQueue } from '../review/review-queue.js';
53
54
  /**
54
55
  * WorkflowOrchestrator service for coordinating multi-phase workflows
55
56
  *
@@ -66,9 +67,11 @@ export class WorkflowOrchestrator {
66
67
  fileScaffolder;
67
68
  inputDetector;
68
69
  logger;
70
+ mcpContextInjector;
69
71
  pathResolver;
70
72
  prdFixer;
71
73
  prdParser;
74
+ reviewPhaseExecutor;
72
75
  storyTypeDetector;
73
76
  workflowLogger;
74
77
  /**
@@ -83,7 +86,9 @@ export class WorkflowOrchestrator {
83
86
  this.agentRunner = config.agentRunner;
84
87
  this.batchProcessor = config.batchProcessor;
85
88
  this.fileManager = config.fileManager;
89
+ this.mcpContextInjector = config.mcpContextInjector;
86
90
  this.pathResolver = config.pathResolver;
91
+ this.reviewPhaseExecutor = config.reviewPhaseExecutor;
87
92
  this.storyTypeDetector = config.storyTypeDetector;
88
93
  this.logger = config.logger;
89
94
  this.workflowLogger = config.workflowLogger;
@@ -113,9 +118,48 @@ export class WorkflowOrchestrator {
113
118
  if (this.shouldAbortAfterEpicFailure(epicPhase)) {
114
119
  return this.buildFailureResult(startTime, epicPhase);
115
120
  }
116
- const { devPhase, storyPhase } = await this.executeStoryAndDevPhases(config, detection, epicPhase, phaseFlags, startTime);
121
+ const { devPhase, reviewPhase, storyPhase } = await this.executeStoryAndDevPhases(config, detection, epicPhase, phaseFlags, startTime);
117
122
  const qaPhase = await this.executeQaPhaseIfNeeded(config, detection, devPhase, phaseFlags.shouldExecuteQaPhase);
118
- return this.buildSuccessResult(startTime, epicPhase, storyPhase, devPhase, qaPhase);
123
+ return this.buildSuccessResult(startTime, epicPhase, storyPhase, devPhase, qaPhase, reviewPhase);
124
+ }
125
+ /**
126
+ * Get MCP prompt prefix for a given phase
127
+ *
128
+ * Returns MCP tool discovery instructions to prepend before agent prompts.
129
+ * Returns empty string when MCP is disabled, injector is unavailable,
130
+ * phase is filtered out, or injector returns empty/errors.
131
+ *
132
+ * @param phase - Workflow phase (epic, story, dev, review, qa)
133
+ * @param agentType - Agent type being spawned
134
+ * @param config - Workflow configuration
135
+ * @returns Instructions string to prepend, or empty string
136
+ * @private
137
+ */
138
+ async getMcpPromptPrefix(phase, agentType, config) {
139
+ // Skip if MCP is not enabled
140
+ if (!config.mcp) {
141
+ return '';
142
+ }
143
+ // Skip if injector is not available
144
+ if (!this.mcpContextInjector) {
145
+ return '';
146
+ }
147
+ // Skip if phase is filtered out by mcpPhases
148
+ if (config.mcpPhases && config.mcpPhases.length > 0 && !config.mcpPhases.includes(phase)) {
149
+ return '';
150
+ }
151
+ try {
152
+ const mcpContext = await this.mcpContextInjector.getContextForAgent(agentType, phase);
153
+ if (!mcpContext.instructions) {
154
+ this.logger.warn({ phase }, 'MCP enabled but injector returned empty instructions');
155
+ return '';
156
+ }
157
+ return mcpContext.instructions;
158
+ }
159
+ catch (error) {
160
+ this.logger.warn({ error: error.message, phase }, 'MCP context injection failed, continuing without MCP tools');
161
+ return '';
162
+ }
119
163
  }
120
164
  /**
121
165
  * Build prompt for epic creation
@@ -199,13 +243,14 @@ Write output to: ${outputPath}`;
199
243
  * @returns WorkflowResult
200
244
  * @private
201
245
  */
202
- buildSuccessResult(startTime, epicPhase, storyPhase, devPhase, qaPhase) {
246
+ buildSuccessResult(startTime, epicPhase, storyPhase, devPhase, qaPhase, reviewPhase) {
203
247
  const totalDuration = Date.now() - startTime;
204
248
  const totalFilesProcessed = (epicPhase?.success ?? 0) + (storyPhase?.success ?? 0) + (devPhase?.success ?? 0) + (qaPhase?.success ?? 0);
205
249
  const totalFailures = (epicPhase?.failures.length ?? 0) +
206
250
  (storyPhase?.failures.length ?? 0) +
207
251
  (devPhase?.failures.length ?? 0) +
208
- (qaPhase?.failures.length ?? 0);
252
+ (qaPhase?.failures.length ?? 0) +
253
+ (reviewPhase?.failures.length ?? 0);
209
254
  const overallSuccess = totalFailures === 0;
210
255
  this.logger.info({
211
256
  overallSuccess,
@@ -218,6 +263,7 @@ Write output to: ${outputPath}`;
218
263
  epicPhase,
219
264
  overallSuccess,
220
265
  qaPhase,
266
+ reviewPhase,
221
267
  storyPhase,
222
268
  totalDuration,
223
269
  totalFailures,
@@ -452,11 +498,13 @@ Write output to: ${outputPath}`;
452
498
  const shouldExecuteEpicPhase = detection.type === 'prd' && !config.skipEpics;
453
499
  const shouldExecuteStoryPhase = !config.skipStories && detection.type !== 'story-pattern';
454
500
  const shouldExecuteDevPhase = !config.skipDev;
501
+ const shouldExecuteReviewPhase = config.review === true && shouldExecuteDevPhase;
455
502
  const shouldExecuteQaPhase = config.qa === true && shouldExecuteDevPhase;
456
503
  return {
457
504
  shouldExecuteDevPhase,
458
505
  shouldExecuteEpicPhase,
459
506
  shouldExecuteQaPhase,
507
+ shouldExecuteReviewPhase,
460
508
  shouldExecuteStoryPhase,
461
509
  };
462
510
  }
@@ -493,7 +541,7 @@ Write output to: ${outputPath}`;
493
541
  * @returns Promise resolving to object with success count and failure array
494
542
  * @private
495
543
  */
496
- async devWorker(workerId, queue, config) {
544
+ async devWorker(workerId, queue, config, reviewQueue) {
497
545
  const workerLogger = this.logger.child({ workerId });
498
546
  let successCount = 0;
499
547
  const failures = [];
@@ -574,7 +622,9 @@ Write output to: ${outputPath}`;
574
622
  storyType: detection.type,
575
623
  }, 'Story type detected, auto-including documentation references');
576
624
  // Build prompt with auto-detected references
577
- let prompt = `@.bmad-core/agents/dev.md\n\n`;
625
+ const mcpPrefix = await this.getMcpPromptPrefix('dev', 'dev', config);
626
+ let prompt = mcpPrefix ? `${mcpPrefix}\n\n` : '';
627
+ prompt += `@.bmad-core/agents/dev.md\n\n`;
578
628
  // Add working directory instruction if specified
579
629
  if (config.cwd) {
580
630
  prompt += `Working directory: ${config.cwd}\n\n`;
@@ -636,6 +686,11 @@ Write output to: ${outputPath}`;
636
686
  await this.updateStoryStatus(storyFilePath, 'Done');
637
687
  // Move to QA folder
638
688
  await this.fileManager.moveFile(storyFilePath, qaFilePath);
689
+ // Enqueue to review queue if review is enabled (pipelined review path)
690
+ if (reviewQueue) {
691
+ reviewQueue.enqueue(story);
692
+ workerLogger.info({ storyNumber: story.fullNumber }, 'Story enqueued for review');
693
+ }
639
694
  successCount++;
640
695
  // Log story completed in pipeline mode
641
696
  const storyDuration = Date.now() - storyStartTime;
@@ -842,7 +897,9 @@ Write output to: ${outputPath}`;
842
897
  storyType: detection.type,
843
898
  }, 'Story type detected, auto-including documentation references');
844
899
  // Build prompt with auto-detected references
845
- let prompt = `@.bmad-core/agents/dev.md\n\n`;
900
+ const mcpPrefix = await this.getMcpPromptPrefix('dev', 'dev', config);
901
+ let prompt = mcpPrefix ? `${mcpPrefix}\n\n` : '';
902
+ prompt += `@.bmad-core/agents/dev.md\n\n`;
846
903
  // Add working directory instruction if specified
847
904
  if (config.cwd) {
848
905
  prompt += `Working directory: ${config.cwd}\n\n`;
@@ -1177,13 +1234,15 @@ Write output to: ${outputPath}`;
1177
1234
  }, 'Epic scaffolded file created');
1178
1235
  }
1179
1236
  // Step 2: Build Claude prompt to populate the scaffolded file
1180
- const prompt = this.buildEpicPrompt(epic, {
1237
+ const mcpPrefix = await this.getMcpPromptPrefix('epic', 'architect', config);
1238
+ const basePrompt = this.buildEpicPrompt(epic, {
1181
1239
  cwd: config.cwd,
1182
1240
  outputPath: epicFilePath,
1183
1241
  prdPath: prdFilePath,
1184
1242
  prefix,
1185
1243
  references: config.references,
1186
1244
  });
1245
+ const prompt = mcpPrefix ? `${mcpPrefix}\n\n${basePrompt}` : basePrompt;
1187
1246
  // Log prompt if verbose
1188
1247
  if (config.verbose) {
1189
1248
  this.logger.info({
@@ -1389,7 +1448,7 @@ Write output to: ${outputPath}`;
1389
1448
  * @returns PhaseResult with aggregate success/failure counts and duration
1390
1449
  * @private
1391
1450
  */
1392
- async executePipelinedDevPhase(queue, config) {
1451
+ async executePipelinedDevPhase(queue, config, reviewQueue) {
1393
1452
  const startTime = Date.now();
1394
1453
  // PIPELINE MODE: Use single worker for sequential story processing
1395
1454
  // Stories are processed in order, one at a time as they become available
@@ -1397,13 +1456,19 @@ Write output to: ${outputPath}`;
1397
1456
  this.logger.info({
1398
1457
  interval: config.storyInterval,
1399
1458
  mode: 'sequential',
1459
+ reviewEnabled: !!reviewQueue,
1400
1460
  workerCount,
1401
1461
  }, 'Starting pipelined development phase (sequential processing)');
1402
1462
  try {
1403
- // Create single worker for sequential processing
1404
- const workers = Array.from({ length: workerCount }, (_, i) => this.devWorker(i, queue, config));
1463
+ // Create single worker for sequential processing, passing reviewQueue if review is enabled
1464
+ const workers = Array.from({ length: workerCount }, (_, i) => this.devWorker(i, queue, config, reviewQueue));
1405
1465
  // Wait for worker to complete
1406
1466
  const workerResults = await Promise.all(workers);
1467
+ // Close ReviewQueue after all dev workers finish (Task 3: close/drain semantics)
1468
+ if (reviewQueue) {
1469
+ reviewQueue.close();
1470
+ this.logger.info('ReviewQueue closed after all dev workers completed');
1471
+ }
1407
1472
  // Aggregate results from all workers
1408
1473
  let totalSuccess = 0;
1409
1474
  const allFailures = [];
@@ -1437,6 +1502,11 @@ Write output to: ${outputPath}`;
1437
1502
  }
1438
1503
  catch (error) {
1439
1504
  this.logger.error({ error: error.message }, 'Pipelined development phase failed');
1505
+ // Ensure ReviewQueue is closed on failure so review workers can terminate
1506
+ if (reviewQueue) {
1507
+ reviewQueue.close();
1508
+ this.logger.info('ReviewQueue closed after dev phase failure');
1509
+ }
1440
1510
  this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
1441
1511
  duration: Date.now() - startTime,
1442
1512
  endTime: Date.now(),
@@ -1461,19 +1531,187 @@ Write output to: ${outputPath}`;
1461
1531
  }
1462
1532
  }
1463
1533
  /**
1464
- * Execute pipelined workflow (story and dev phases in parallel)
1534
+ * Review worker that consumes stories from the ReviewQueue and processes them.
1535
+ *
1536
+ * Worker loop:
1537
+ * 1. Dequeue next story (blocks/waits if queue empty)
1538
+ * 2. If null returned, queue is closed and empty → terminate worker
1539
+ * 3. For each story, invoke ReviewPhaseExecutor.reviewStory()
1540
+ * 4. Collect pass/fail results
1541
+ *
1542
+ * @param workerId - Worker identifier for logging
1543
+ * @param reviewQueue - ReviewQueue to dequeue stories from
1544
+ * @param config - Workflow configuration
1545
+ * @returns Worker result with passed and failed story arrays
1546
+ * @private
1547
+ */
1548
+ async reviewWorker(workerId, reviewQueue, config) {
1549
+ const workerLogger = this.logger.child({ reviewWorkerId: workerId });
1550
+ const passedStories = [];
1551
+ const failures = [];
1552
+ let successCount = 0;
1553
+ workerLogger.info('Review worker started');
1554
+ try {
1555
+ while (true) {
1556
+ const story = await reviewQueue.dequeue();
1557
+ if (!story) {
1558
+ workerLogger.info('ReviewQueue closed and empty, review worker terminating');
1559
+ break;
1560
+ }
1561
+ workerLogger.info({ storyNumber: story.fullNumber, storyTitle: story.title }, 'Review worker processing story');
1562
+ try {
1563
+ const result = await this.reviewPhaseExecutor.reviewStory(story, config);
1564
+ if (result.verdict === 'PASS') {
1565
+ passedStories.push(story);
1566
+ successCount++;
1567
+ workerLogger.info({ storyNumber: story.fullNumber, verdict: 'PASS' }, 'Story review passed');
1568
+ }
1569
+ else {
1570
+ const failureReason = result.message || `Review failed with ${result.issues.length} issue(s)`;
1571
+ failures.push({
1572
+ error: failureReason,
1573
+ identifier: story.fullNumber,
1574
+ });
1575
+ workerLogger.warn({
1576
+ issueCount: result.issues.length,
1577
+ reason: failureReason,
1578
+ storyNumber: story.fullNumber,
1579
+ verdict: 'FAIL',
1580
+ }, 'Story review failed — excluded from QA');
1581
+ }
1582
+ }
1583
+ catch (error) {
1584
+ workerLogger.error({ error: error.message, storyNumber: story.fullNumber }, 'Error reviewing story, marking as failed');
1585
+ failures.push({
1586
+ error: error.message,
1587
+ identifier: story.fullNumber,
1588
+ });
1589
+ }
1590
+ }
1591
+ workerLogger.info({ failureCount: failures.length, passedCount: passedStories.length, successCount }, 'Review worker completed');
1592
+ return { failures, passedStories, success: successCount };
1593
+ }
1594
+ catch (error) {
1595
+ workerLogger.error({ error: error.message }, 'Review worker failed with unhandled error');
1596
+ return { failures, passedStories, success: successCount };
1597
+ }
1598
+ }
1599
+ /**
1600
+ * Execute pipelined review phase with worker pool.
1601
+ *
1602
+ * @param reviewQueue - ReviewQueue to consume stories from
1603
+ * @param config - Workflow configuration
1604
+ * @returns PhaseResult with aggregate success/failure counts and passed stories
1605
+ * @private
1606
+ */
1607
+ async executePipelinedReviewPhase(reviewQueue, config) {
1608
+ const startTime = Date.now();
1609
+ const workerCount = 1;
1610
+ this.logger.info({ mode: 'sequential', workerCount }, 'Starting pipelined review phase');
1611
+ try {
1612
+ const workers = Array.from({ length: workerCount }, (_, i) => this.reviewWorker(i, reviewQueue, config));
1613
+ const workerResults = await Promise.all(workers);
1614
+ let totalSuccess = 0;
1615
+ const allFailures = [];
1616
+ const allPassedStories = [];
1617
+ for (const result of workerResults) {
1618
+ totalSuccess += result.success;
1619
+ allFailures.push(...result.failures);
1620
+ allPassedStories.push(...result.passedStories);
1621
+ }
1622
+ const duration = Date.now() - startTime;
1623
+ this.logger.info({
1624
+ duration,
1625
+ failures: allFailures.length,
1626
+ passedStories: allPassedStories.length,
1627
+ success: totalSuccess,
1628
+ workerCount,
1629
+ }, 'Pipelined review phase completed');
1630
+ return {
1631
+ passedStories: allPassedStories,
1632
+ phaseResult: {
1633
+ duration,
1634
+ failures: allFailures,
1635
+ phaseName: 'review',
1636
+ skipped: false,
1637
+ success: totalSuccess,
1638
+ },
1639
+ };
1640
+ }
1641
+ catch (error) {
1642
+ this.logger.error({ error: error.message }, 'Pipelined review phase failed');
1643
+ return {
1644
+ passedStories: [],
1645
+ phaseResult: {
1646
+ duration: Date.now() - startTime,
1647
+ failures: [{ error: error.message, identifier: 'review-phase' }],
1648
+ phaseName: 'review',
1649
+ skipped: false,
1650
+ success: 0,
1651
+ },
1652
+ };
1653
+ }
1654
+ }
1655
+ /**
1656
+ * Execute pipelined workflow (story, dev, and optionally review phases in parallel)
1657
+ *
1658
+ * When config.review === true && reviewPhaseExecutor is available:
1659
+ * [StoryQueue] → dev workers → [ReviewQueue] → review workers → QA collection
1660
+ * When config.review === false (default):
1661
+ * [StoryQueue] → dev workers → QA collection (unchanged behavior)
1465
1662
  *
1466
1663
  * @param config - Workflow configuration
1467
1664
  * @param detection - Input detection result
1468
- * @returns Story and dev phase results
1665
+ * @returns Story, dev, and optional review phase results
1469
1666
  * @private
1470
1667
  */
1471
1668
  async executePipelinedWorkflow(config, detection) {
1472
- this.logger.info('Executing pipelined workflow (sequential dev starts as stories are created)');
1669
+ const pipelineStartTime = Date.now();
1670
+ const enableReview = config.review === true && this.reviewPhaseExecutor != null;
1671
+ this.logger.info({ review: enableReview }, 'Executing pipelined workflow (sequential dev starts as stories are created)');
1473
1672
  const queue = new StoryQueue(this.logger);
1673
+ const reviewQueue = enableReview ? new ReviewQueue(this.logger) : undefined;
1474
1674
  try {
1475
1675
  const storyPhasePromise = this.executeStoryPhaseWithQueue(config, detection, queue);
1476
- const devPhasePromise = this.executePipelinedDevPhase(queue, config);
1676
+ const devPhasePromise = this.executePipelinedDevPhase(queue, config, reviewQueue);
1677
+ if (enableReview && reviewQueue) {
1678
+ // Three-phase pipeline: story → dev → review
1679
+ const reviewPhasePromise = this.executePipelinedReviewPhase(reviewQueue, config);
1680
+ const [storyResult, devResult, reviewResult] = await Promise.allSettled([
1681
+ storyPhasePromise,
1682
+ devPhasePromise,
1683
+ reviewPhasePromise,
1684
+ ]);
1685
+ const devPhase = this.handlePhaseResult(devResult, 'dev');
1686
+ const storyPhase = this.handlePhaseResult(storyResult, 'story');
1687
+ let reviewPhase;
1688
+ if (reviewResult.status === 'fulfilled') {
1689
+ reviewPhase = reviewResult.value.phaseResult;
1690
+ }
1691
+ else {
1692
+ this.logger.error({ error: reviewResult.reason?.message }, 'Review phase promise rejected');
1693
+ reviewPhase = {
1694
+ duration: 0,
1695
+ failures: [{ error: reviewResult.reason?.message || 'Review phase failed', identifier: 'review-phase' }],
1696
+ phaseName: 'review',
1697
+ skipped: false,
1698
+ success: 0,
1699
+ };
1700
+ }
1701
+ // Timing instrumentation (NFR4)
1702
+ const totalDuration = Date.now() - pipelineStartTime;
1703
+ const devDuration = devPhase?.duration ?? 0;
1704
+ const reviewDuration = reviewPhase?.duration ?? 0;
1705
+ const reviewOverheadPercent = devDuration > 0 ? Math.round((reviewDuration / devDuration) * 100) : 0;
1706
+ this.logger.info({
1707
+ devDuration,
1708
+ reviewDuration,
1709
+ reviewOverheadPercent,
1710
+ totalDuration,
1711
+ }, 'Pipeline timing: dev + review phase durations');
1712
+ return { devPhase, reviewPhase, storyPhase };
1713
+ }
1714
+ // Two-phase pipeline: story → dev (default, no review)
1477
1715
  const [storyResult, devResult] = await Promise.allSettled([storyPhasePromise, devPhasePromise]);
1478
1716
  return {
1479
1717
  devPhase: this.handlePhaseResult(devResult, 'dev'),
@@ -1567,6 +1805,13 @@ Write output to: ${outputPath}`;
1567
1805
  qaFlags.push(`--reference=${ref}`);
1568
1806
  }
1569
1807
  }
1808
+ // Forward MCP flags to QA command (when QA command supports them)
1809
+ if (config.mcp) {
1810
+ qaFlags.push('--mcp');
1811
+ if (config.mcpPhases && config.mcpPhases.length > 0) {
1812
+ qaFlags.push(`--mcp-phases=${config.mcpPhases.join(',')}`);
1813
+ }
1814
+ }
1570
1815
  // Suppress QA command's pino logger in non-verbose mode
1571
1816
  const prevLogLevel = process.env.LOG_LEVEL;
1572
1817
  if (!config.verbose) {
@@ -1740,6 +1985,43 @@ Write output to: ${outputPath}`;
1740
1985
  return { devPhase: this.createSkippedPhaseResult('dev'), storyPhase };
1741
1986
  }
1742
1987
  const devPhase = await this.executeSequentialDevPhase(config, detection, phaseFlags.shouldExecuteDevPhase);
1988
+ // Review phase: after dev, before QA (sequential batch-style)
1989
+ if (phaseFlags.shouldExecuteReviewPhase && this.reviewPhaseExecutor && devPhase && devPhase.success > 0) {
1990
+ const reviewStartTime = Date.now();
1991
+ const stories = await this.getStoriesForDevPhase(config, detection);
1992
+ const reviewFailures = [];
1993
+ let reviewSuccess = 0;
1994
+ this.logger.info({ storyCount: stories.length }, 'Starting sequential review phase');
1995
+ for (const story of stories) {
1996
+ try {
1997
+ const result = await this.reviewPhaseExecutor.reviewStory(story, config);
1998
+ if (result.verdict === 'PASS') {
1999
+ reviewSuccess++;
2000
+ }
2001
+ else {
2002
+ reviewFailures.push({
2003
+ error: result.message || `Review failed with ${result.issues.length} issue(s)`,
2004
+ identifier: story.fullNumber,
2005
+ });
2006
+ this.logger.warn({ storyNumber: story.fullNumber, verdict: 'FAIL' }, 'Story review failed — excluded from QA');
2007
+ }
2008
+ }
2009
+ catch (error) {
2010
+ reviewFailures.push({
2011
+ error: error.message,
2012
+ identifier: story.fullNumber,
2013
+ });
2014
+ }
2015
+ }
2016
+ const reviewPhase = {
2017
+ duration: Date.now() - reviewStartTime,
2018
+ failures: reviewFailures,
2019
+ phaseName: 'review',
2020
+ skipped: false,
2021
+ success: reviewSuccess,
2022
+ };
2023
+ return { devPhase, reviewPhase, storyPhase };
2024
+ }
1743
2025
  return { devPhase, storyPhase };
1744
2026
  }
1745
2027
  /**
@@ -1943,13 +2225,15 @@ Write output to: ${outputPath}`;
1943
2225
  const epicFileName = this.generateEpicFileName(prefix, story.epicNumber);
1944
2226
  const epicFilePath = `${epicDir}/${epicFileName}`;
1945
2227
  // Step 3: Build Claude prompt to populate the scaffolded file
1946
- const prompt = this.buildStoryPrompt(story, {
2228
+ const mcpPrefix = await this.getMcpPromptPrefix('story', 'sm', config);
2229
+ const basePrompt = this.buildStoryPrompt(story, {
1947
2230
  cwd: config.cwd,
1948
2231
  epicPath: epicFilePath,
1949
2232
  outputPath: storyFilePath,
1950
2233
  prefix,
1951
2234
  references: config.references,
1952
2235
  });
2236
+ const prompt = mcpPrefix ? `${mcpPrefix}\n\n${basePrompt}` : basePrompt;
1953
2237
  // Log prompt if verbose
1954
2238
  if (config.verbose) {
1955
2239
  this.logger.info({
@@ -2265,13 +2549,15 @@ Write output to: ${outputPath}`;
2265
2549
  }, 'Story scaffolded file created');
2266
2550
  // Step 2: Use the epic file path that was passed to this method
2267
2551
  // Step 3: Build Claude prompt to populate the scaffolded file
2268
- const prompt = this.buildStoryPrompt(story, {
2552
+ const mcpPrefix = await this.getMcpPromptPrefix('story', 'sm', config);
2553
+ const basePrompt = this.buildStoryPrompt(story, {
2269
2554
  cwd: config.cwd,
2270
2555
  epicPath: epicFilePath,
2271
2556
  outputPath: storyFilePath,
2272
2557
  prefix,
2273
2558
  references: config.references,
2274
2559
  });
2560
+ const prompt = mcpPrefix ? `${mcpPrefix}\n\n${basePrompt}` : basePrompt;
2275
2561
  // Log prompt if verbose
2276
2562
  if (config.verbose) {
2277
2563
  this.logger.info({
@@ -0,0 +1,66 @@
1
+ /**
2
+ * AI Review Scanner
3
+ *
4
+ * Spawns a QA agent to perform structured code review on story implementations.
5
+ * Produces RawReviewOutput with source 'ai' for downstream SeverityClassifier parsing.
6
+ *
7
+ * Prompt enforces strict output format: SEVERITY/FILE/LINE/ISSUE/FIX blocks
8
+ * separated by `---`, or `REVIEW_PASS: No issues found` sentinel for clean reviews.
9
+ */
10
+ import type pino from 'pino';
11
+ import type { AIProviderRunner } from '../agents/agent-runner.js';
12
+ import type { RawReviewOutput, ReviewContext, ReviewScanner } from './types.js';
13
+ /** Path-specific review rule loaded from core-config.yaml */
14
+ export interface PathRule {
15
+ /** Review focus areas for files matching this pattern */
16
+ focus: string[];
17
+ /** Glob pattern to match against changed files */
18
+ pattern: string;
19
+ }
20
+ /** Review configuration — subset of core-config relevant to AI review */
21
+ export interface ReviewConfig {
22
+ /** Set to false to disable AI review scanner */
23
+ aiReview?: boolean;
24
+ /** Set to true to enable CodeRabbit scanner (requires CLI installed) */
25
+ coderabbit?: boolean;
26
+ /** Path-specific review rules */
27
+ pathRules?: PathRule[];
28
+ }
29
+ /**
30
+ * AIReviewScanner — spawns a QA agent and parses structured review output.
31
+ *
32
+ * Implements ReviewScanner interface. Uses composition over inheritance —
33
+ * composes AIProviderRunner, doesn't extend it.
34
+ */
35
+ export declare class AIReviewScanner implements ReviewScanner {
36
+ private readonly agentRunner;
37
+ private readonly config;
38
+ private readonly logger;
39
+ private readonly timeout;
40
+ constructor(agentRunner: AIProviderRunner, config: ReviewConfig, logger: pino.Logger, timeout?: number);
41
+ /**
42
+ * Scan the given context by spawning a QA agent for AI-powered code review
43
+ *
44
+ * @param context - Review context describing what to scan
45
+ * @returns Raw output from the AI agent with source 'ai'
46
+ */
47
+ scan(context: ReviewContext): Promise<RawReviewOutput>;
48
+ /**
49
+ * Build the review prompt with severity definitions, output format spec,
50
+ * changed files, and path-specific rules.
51
+ */
52
+ private buildReviewPrompt;
53
+ /**
54
+ * Get path rules that match any of the changed files
55
+ */
56
+ private getMatchingPathRules;
57
+ /**
58
+ * Parse agent output into RawReviewOutput
59
+ *
60
+ * Handles three cases:
61
+ * 1. REVIEW_PASS sentinel → clean result
62
+ * 2. Structured output → pass raw text for SeverityClassifier
63
+ * 3. Malformed/empty output → graceful fallback
64
+ */
65
+ private parseAgentOutput;
66
+ }