@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.
- package/dist/commands/config/show.js +8 -2
- package/dist/commands/decompose.js +26 -5
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/mcp/add.d.ts +16 -0
- package/dist/commands/mcp/add.js +77 -0
- package/dist/commands/mcp/credential/get.d.ts +14 -0
- package/dist/commands/mcp/credential/get.js +35 -0
- package/dist/commands/mcp/credential/list.d.ts +17 -0
- package/dist/commands/mcp/credential/list.js +67 -0
- package/dist/commands/mcp/credential/remove.d.ts +18 -0
- package/dist/commands/mcp/credential/remove.js +84 -0
- package/dist/commands/mcp/credential/set.d.ts +16 -0
- package/dist/commands/mcp/credential/set.js +41 -0
- package/dist/commands/mcp/credential/validate.d.ts +12 -0
- package/dist/commands/mcp/credential/validate.js +150 -0
- package/dist/commands/mcp/list.d.ts +17 -0
- package/dist/commands/mcp/list.js +80 -0
- package/dist/commands/mcp/logs.d.ts +15 -0
- package/dist/commands/mcp/logs.js +64 -0
- package/dist/commands/mcp/preset.d.ts +15 -0
- package/dist/commands/mcp/preset.js +84 -0
- package/dist/commands/mcp/remove.d.ts +14 -0
- package/dist/commands/mcp/remove.js +36 -0
- package/dist/commands/mcp/start.d.ts +12 -0
- package/dist/commands/mcp/start.js +80 -0
- package/dist/commands/mcp/status.d.ts +30 -0
- package/dist/commands/mcp/status.js +180 -0
- package/dist/commands/mcp/stop.d.ts +12 -0
- package/dist/commands/mcp/stop.js +47 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +1 -0
- package/dist/commands/stories/qa.js +5 -2
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +8 -0
- package/dist/commands/workflow.js +110 -2
- package/dist/mcp/types.d.ts +99 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/utils/docker-utils.d.ts +56 -0
- package/dist/mcp/utils/docker-utils.js +108 -0
- package/dist/mcp/utils/template-loader.d.ts +21 -0
- package/dist/mcp/utils/template-loader.js +60 -0
- package/dist/models/agent-options.d.ts +10 -1
- package/dist/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/agents/claude-agent-runner.js +19 -3
- package/dist/services/file-system/path-resolver.d.ts +10 -0
- package/dist/services/file-system/path-resolver.js +12 -0
- package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
- package/dist/services/mcp/mcp-config-manager.js +146 -0
- package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
- package/dist/services/mcp/mcp-context-injector.js +168 -0
- package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
- package/dist/services/mcp/mcp-credential-manager.js +124 -0
- package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
- package/dist/services/mcp/mcp-health-checker.js +162 -0
- package/dist/services/mcp/types/health-types.d.ts +31 -0
- package/dist/services/mcp/types/health-types.js +7 -0
- package/dist/services/orchestration/dependency-graph-executor.js +1 -1
- package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
- package/dist/services/orchestration/task-decomposition-service.js +90 -36
- package/dist/services/orchestration/workflow-orchestrator.d.ts +54 -2
- package/dist/services/orchestration/workflow-orchestrator.js +303 -17
- package/dist/services/review/ai-review-scanner.d.ts +66 -0
- package/dist/services/review/ai-review-scanner.js +142 -0
- package/dist/services/review/coderabbit-scanner.d.ts +25 -0
- package/dist/services/review/coderabbit-scanner.js +31 -0
- package/dist/services/review/index.d.ts +20 -0
- package/dist/services/review/index.js +15 -0
- package/dist/services/review/lint-scanner.d.ts +46 -0
- package/dist/services/review/lint-scanner.js +172 -0
- package/dist/services/review/review-config.d.ts +62 -0
- package/dist/services/review/review-config.js +91 -0
- package/dist/services/review/review-phase-executor.d.ts +69 -0
- package/dist/services/review/review-phase-executor.js +152 -0
- package/dist/services/review/review-queue.d.ts +98 -0
- package/dist/services/review/review-queue.js +174 -0
- package/dist/services/review/review-reporter.d.ts +94 -0
- package/dist/services/review/review-reporter.js +386 -0
- package/dist/services/review/scanner-factory.d.ts +42 -0
- package/dist/services/review/scanner-factory.js +60 -0
- package/dist/services/review/self-heal-loop.d.ts +58 -0
- package/dist/services/review/self-heal-loop.js +132 -0
- package/dist/services/review/severity-classifier.d.ts +17 -0
- package/dist/services/review/severity-classifier.js +314 -0
- package/dist/services/review/tech-debt-tracker.d.ts +52 -0
- package/dist/services/review/tech-debt-tracker.js +245 -0
- package/dist/services/review/types.d.ts +93 -0
- package/dist/services/review/types.js +23 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/credential-utils.d.ts +14 -0
- package/dist/utils/credential-utils.js +19 -0
- package/dist/utils/duration.d.ts +41 -0
- package/dist/utils/duration.js +89 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- 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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
1665
|
+
* @returns Story, dev, and optional review phase results
|
|
1469
1666
|
* @private
|
|
1470
1667
|
*/
|
|
1471
1668
|
async executePipelinedWorkflow(config, detection) {
|
|
1472
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|