@hyperdrive.bot/bmad-workflow 1.0.17 → 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 +34 -75
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +89 -0
- package/dist/commands/workflow.js +487 -14
- 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/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/workflow-callbacks.d.ts +251 -0
- package/dist/models/workflow-callbacks.js +10 -0
- package/dist/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +25 -4
- 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 +87 -3
- package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
- 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/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- 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/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +6 -3
|
@@ -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
|
*
|
|
@@ -60,14 +61,17 @@ import { StoryQueue } from './story-queue.js';
|
|
|
60
61
|
export class WorkflowOrchestrator {
|
|
61
62
|
agentRunner;
|
|
62
63
|
batchProcessor;
|
|
64
|
+
callbacks;
|
|
63
65
|
epicParser;
|
|
64
66
|
fileManager;
|
|
65
67
|
fileScaffolder;
|
|
66
68
|
inputDetector;
|
|
67
69
|
logger;
|
|
70
|
+
mcpContextInjector;
|
|
68
71
|
pathResolver;
|
|
69
72
|
prdFixer;
|
|
70
73
|
prdParser;
|
|
74
|
+
reviewPhaseExecutor;
|
|
71
75
|
storyTypeDetector;
|
|
72
76
|
workflowLogger;
|
|
73
77
|
/**
|
|
@@ -82,10 +86,13 @@ export class WorkflowOrchestrator {
|
|
|
82
86
|
this.agentRunner = config.agentRunner;
|
|
83
87
|
this.batchProcessor = config.batchProcessor;
|
|
84
88
|
this.fileManager = config.fileManager;
|
|
89
|
+
this.mcpContextInjector = config.mcpContextInjector;
|
|
85
90
|
this.pathResolver = config.pathResolver;
|
|
91
|
+
this.reviewPhaseExecutor = config.reviewPhaseExecutor;
|
|
86
92
|
this.storyTypeDetector = config.storyTypeDetector;
|
|
87
93
|
this.logger = config.logger;
|
|
88
94
|
this.workflowLogger = config.workflowLogger;
|
|
95
|
+
this.callbacks = config.callbacks;
|
|
89
96
|
this.fileScaffolder = new FileScaffolder(config.logger);
|
|
90
97
|
this.prdFixer = new PrdFixer(config.agentRunner, config.fileManager, config.logger);
|
|
91
98
|
this.logger.debug('WorkflowOrchestrator initialized');
|
|
@@ -111,9 +118,48 @@ export class WorkflowOrchestrator {
|
|
|
111
118
|
if (this.shouldAbortAfterEpicFailure(epicPhase)) {
|
|
112
119
|
return this.buildFailureResult(startTime, epicPhase);
|
|
113
120
|
}
|
|
114
|
-
const { devPhase, storyPhase } = await this.executeStoryAndDevPhases(config, detection, epicPhase, phaseFlags, startTime);
|
|
121
|
+
const { devPhase, reviewPhase, storyPhase } = await this.executeStoryAndDevPhases(config, detection, epicPhase, phaseFlags, startTime);
|
|
115
122
|
const qaPhase = await this.executeQaPhaseIfNeeded(config, detection, devPhase, phaseFlags.shouldExecuteQaPhase);
|
|
116
|
-
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
|
+
}
|
|
117
163
|
}
|
|
118
164
|
/**
|
|
119
165
|
* Build prompt for epic creation
|
|
@@ -197,13 +243,14 @@ Write output to: ${outputPath}`;
|
|
|
197
243
|
* @returns WorkflowResult
|
|
198
244
|
* @private
|
|
199
245
|
*/
|
|
200
|
-
buildSuccessResult(startTime, epicPhase, storyPhase, devPhase, qaPhase) {
|
|
246
|
+
buildSuccessResult(startTime, epicPhase, storyPhase, devPhase, qaPhase, reviewPhase) {
|
|
201
247
|
const totalDuration = Date.now() - startTime;
|
|
202
248
|
const totalFilesProcessed = (epicPhase?.success ?? 0) + (storyPhase?.success ?? 0) + (devPhase?.success ?? 0) + (qaPhase?.success ?? 0);
|
|
203
249
|
const totalFailures = (epicPhase?.failures.length ?? 0) +
|
|
204
250
|
(storyPhase?.failures.length ?? 0) +
|
|
205
251
|
(devPhase?.failures.length ?? 0) +
|
|
206
|
-
(qaPhase?.failures.length ?? 0)
|
|
252
|
+
(qaPhase?.failures.length ?? 0) +
|
|
253
|
+
(reviewPhase?.failures.length ?? 0);
|
|
207
254
|
const overallSuccess = totalFailures === 0;
|
|
208
255
|
this.logger.info({
|
|
209
256
|
overallSuccess,
|
|
@@ -216,12 +263,90 @@ Write output to: ${outputPath}`;
|
|
|
216
263
|
epicPhase,
|
|
217
264
|
overallSuccess,
|
|
218
265
|
qaPhase,
|
|
266
|
+
reviewPhase,
|
|
219
267
|
storyPhase,
|
|
220
268
|
totalDuration,
|
|
221
269
|
totalFailures,
|
|
222
270
|
totalFilesProcessed,
|
|
223
271
|
};
|
|
224
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Invoke a callback safely, catching and logging any errors
|
|
275
|
+
*
|
|
276
|
+
* All callback invocations are wrapped in try-catch to prevent
|
|
277
|
+
* callback errors from interrupting workflow execution.
|
|
278
|
+
*
|
|
279
|
+
* @param callbackName - Name of the callback for logging
|
|
280
|
+
* @param callback - The callback function to invoke
|
|
281
|
+
* @param context - The context to pass to the callback
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
invokeCallback(callbackName, callback, context) {
|
|
285
|
+
if (!callback)
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
callback(context);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
this.logger.warn({
|
|
292
|
+
callbackName,
|
|
293
|
+
error: error.message,
|
|
294
|
+
}, 'Callback error (ignored to prevent workflow interruption)');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Invoke the onSpawnOutput callback safely
|
|
299
|
+
*
|
|
300
|
+
* @param callback - The callback function to invoke
|
|
301
|
+
* @param context - The spawn context
|
|
302
|
+
* @param output - The output text
|
|
303
|
+
* @private
|
|
304
|
+
*/
|
|
305
|
+
invokeSpawnOutputCallback(callback, context, output) {
|
|
306
|
+
if (!callback)
|
|
307
|
+
return;
|
|
308
|
+
try {
|
|
309
|
+
callback(context, output);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
this.logger.warn({
|
|
313
|
+
callbackName: 'onSpawnOutput',
|
|
314
|
+
error: error.message,
|
|
315
|
+
}, 'Callback error (ignored to prevent workflow interruption)');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Invoke the onError callback safely
|
|
320
|
+
*
|
|
321
|
+
* @param error - The error that occurred
|
|
322
|
+
* @param options - Additional context for the error
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
invokeErrorCallback(error, options = {}) {
|
|
326
|
+
const callback = this.callbacks?.onError;
|
|
327
|
+
if (!callback)
|
|
328
|
+
return;
|
|
329
|
+
const context = {
|
|
330
|
+
error,
|
|
331
|
+
itemId: options.itemId,
|
|
332
|
+
message: error.message,
|
|
333
|
+
metadata: options.metadata,
|
|
334
|
+
phaseName: options.phaseName,
|
|
335
|
+
recoverable: options.recoverable ?? true,
|
|
336
|
+
spawnId: options.spawnId,
|
|
337
|
+
stack: error.stack,
|
|
338
|
+
timestamp: Date.now(),
|
|
339
|
+
};
|
|
340
|
+
try {
|
|
341
|
+
callback(context);
|
|
342
|
+
}
|
|
343
|
+
catch (callbackError) {
|
|
344
|
+
this.logger.warn({
|
|
345
|
+
callbackName: 'onError',
|
|
346
|
+
error: callbackError.message,
|
|
347
|
+
}, 'Callback error (ignored to prevent workflow interruption)');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
225
350
|
/**
|
|
226
351
|
* Check which files already exist in a directory
|
|
227
352
|
*
|
|
@@ -297,7 +422,7 @@ Write output to: ${outputPath}`;
|
|
|
297
422
|
const content = await this.fileManager.readFile(filePath);
|
|
298
423
|
const isScaffolded = content.includes('_[AI Agent will populate');
|
|
299
424
|
if (isScaffolded) {
|
|
300
|
-
this.logger.
|
|
425
|
+
this.logger.info({
|
|
301
426
|
fileName,
|
|
302
427
|
filePath,
|
|
303
428
|
}, 'Epic file exists but is only scaffolded (will be re-populated)');
|
|
@@ -373,11 +498,13 @@ Write output to: ${outputPath}`;
|
|
|
373
498
|
const shouldExecuteEpicPhase = detection.type === 'prd' && !config.skipEpics;
|
|
374
499
|
const shouldExecuteStoryPhase = !config.skipStories && detection.type !== 'story-pattern';
|
|
375
500
|
const shouldExecuteDevPhase = !config.skipDev;
|
|
501
|
+
const shouldExecuteReviewPhase = config.review === true && shouldExecuteDevPhase;
|
|
376
502
|
const shouldExecuteQaPhase = config.qa === true && shouldExecuteDevPhase;
|
|
377
503
|
return {
|
|
378
504
|
shouldExecuteDevPhase,
|
|
379
505
|
shouldExecuteEpicPhase,
|
|
380
506
|
shouldExecuteQaPhase,
|
|
507
|
+
shouldExecuteReviewPhase,
|
|
381
508
|
shouldExecuteStoryPhase,
|
|
382
509
|
};
|
|
383
510
|
}
|
|
@@ -414,10 +541,11 @@ Write output to: ${outputPath}`;
|
|
|
414
541
|
* @returns Promise resolving to object with success count and failure array
|
|
415
542
|
* @private
|
|
416
543
|
*/
|
|
417
|
-
async devWorker(workerId, queue, config) {
|
|
544
|
+
async devWorker(workerId, queue, config, reviewQueue) {
|
|
418
545
|
const workerLogger = this.logger.child({ workerId });
|
|
419
546
|
let successCount = 0;
|
|
420
547
|
const failures = [];
|
|
548
|
+
let devPhaseStarted = false;
|
|
421
549
|
workerLogger.info('Worker started');
|
|
422
550
|
try {
|
|
423
551
|
const storyDir = await this.pathResolver.getStoryDir();
|
|
@@ -430,6 +558,16 @@ Write output to: ${outputPath}`;
|
|
|
430
558
|
workerLogger.info('Queue closed and empty, worker terminating');
|
|
431
559
|
break;
|
|
432
560
|
}
|
|
561
|
+
// Fire onPhaseStart on first dequeue (after story phase is done creating)
|
|
562
|
+
if (!devPhaseStarted) {
|
|
563
|
+
devPhaseStarted = true;
|
|
564
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
565
|
+
itemCount: 0,
|
|
566
|
+
metadata: { mode: 'pipeline', workerId },
|
|
567
|
+
phaseName: 'dev',
|
|
568
|
+
startTime: Date.now(),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
433
571
|
workerLogger.info({
|
|
434
572
|
storyNumber: story.fullNumber,
|
|
435
573
|
storyTitle: story.title,
|
|
@@ -484,7 +622,9 @@ Write output to: ${outputPath}`;
|
|
|
484
622
|
storyType: detection.type,
|
|
485
623
|
}, 'Story type detected, auto-including documentation references');
|
|
486
624
|
// Build prompt with auto-detected references
|
|
487
|
-
|
|
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`;
|
|
488
628
|
// Add working directory instruction if specified
|
|
489
629
|
if (config.cwd) {
|
|
490
630
|
prompt += `Working directory: ${config.cwd}\n\n`;
|
|
@@ -500,6 +640,20 @@ Write output to: ${outputPath}`;
|
|
|
500
640
|
prompt += '\n';
|
|
501
641
|
}
|
|
502
642
|
prompt += '*yolo mode*\n';
|
|
643
|
+
// Generate unique spawn ID
|
|
644
|
+
const spawnId = `dev-${story.fullNumber}-${storyStartTime}`;
|
|
645
|
+
// Invoke onSpawnStart callback
|
|
646
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
647
|
+
agentType: 'dev',
|
|
648
|
+
itemId: story.fullNumber,
|
|
649
|
+
itemTitle: story.title,
|
|
650
|
+
outputPath: storyFilePath,
|
|
651
|
+
phaseName: 'dev',
|
|
652
|
+
prompt,
|
|
653
|
+
spawnId,
|
|
654
|
+
startTime: storyStartTime,
|
|
655
|
+
workerId,
|
|
656
|
+
});
|
|
503
657
|
// Execute dev agent with retry on timeout/killed
|
|
504
658
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
505
659
|
agentType: 'dev',
|
|
@@ -510,11 +664,33 @@ Write output to: ${outputPath}`;
|
|
|
510
664
|
logger: this.logger,
|
|
511
665
|
maxRetries: config.maxRetries,
|
|
512
666
|
});
|
|
667
|
+
const spawnEndTime = Date.now();
|
|
668
|
+
const spawnDuration = spawnEndTime - storyStartTime;
|
|
513
669
|
if (result.success) {
|
|
670
|
+
// Invoke onSpawnComplete callback for success
|
|
671
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
672
|
+
agentType: 'dev',
|
|
673
|
+
duration: spawnDuration,
|
|
674
|
+
endTime: spawnEndTime,
|
|
675
|
+
itemId: story.fullNumber,
|
|
676
|
+
itemTitle: story.title,
|
|
677
|
+
output: result.output,
|
|
678
|
+
outputPath: storyFilePath,
|
|
679
|
+
phaseName: 'dev',
|
|
680
|
+
spawnId,
|
|
681
|
+
startTime: storyStartTime,
|
|
682
|
+
success: true,
|
|
683
|
+
workerId,
|
|
684
|
+
});
|
|
514
685
|
// Update story status to Done
|
|
515
686
|
await this.updateStoryStatus(storyFilePath, 'Done');
|
|
516
687
|
// Move to QA folder
|
|
517
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
|
+
}
|
|
518
694
|
successCount++;
|
|
519
695
|
// Log story completed in pipeline mode
|
|
520
696
|
const storyDuration = Date.now() - storyStartTime;
|
|
@@ -530,6 +706,28 @@ Write output to: ${outputPath}`;
|
|
|
530
706
|
workerLogger.info({ storyNumber: story.fullNumber }, 'Story development completed successfully');
|
|
531
707
|
}
|
|
532
708
|
else {
|
|
709
|
+
// Invoke onSpawnComplete callback for failure
|
|
710
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
711
|
+
agentType: 'dev',
|
|
712
|
+
duration: spawnDuration,
|
|
713
|
+
endTime: spawnEndTime,
|
|
714
|
+
error: result.errors,
|
|
715
|
+
itemId: story.fullNumber,
|
|
716
|
+
itemTitle: story.title,
|
|
717
|
+
outputPath: storyFilePath,
|
|
718
|
+
phaseName: 'dev',
|
|
719
|
+
spawnId,
|
|
720
|
+
startTime: storyStartTime,
|
|
721
|
+
success: false,
|
|
722
|
+
workerId,
|
|
723
|
+
});
|
|
724
|
+
// Invoke onError callback
|
|
725
|
+
this.invokeErrorCallback(new Error(result.errors), {
|
|
726
|
+
itemId: story.fullNumber,
|
|
727
|
+
phaseName: 'dev',
|
|
728
|
+
recoverable: true,
|
|
729
|
+
spawnId,
|
|
730
|
+
});
|
|
533
731
|
// Log story failed in pipeline mode
|
|
534
732
|
const storyDuration = Date.now() - storyStartTime;
|
|
535
733
|
if (this.workflowLogger) {
|
|
@@ -552,6 +750,12 @@ Write output to: ${outputPath}`;
|
|
|
552
750
|
}
|
|
553
751
|
}
|
|
554
752
|
catch (error) {
|
|
753
|
+
// Invoke onError callback for unexpected errors
|
|
754
|
+
this.invokeErrorCallback(error, {
|
|
755
|
+
itemId: story.fullNumber,
|
|
756
|
+
phaseName: 'dev',
|
|
757
|
+
recoverable: true,
|
|
758
|
+
});
|
|
555
759
|
// Catch errors for individual stories to prevent worker crash
|
|
556
760
|
workerLogger.error({
|
|
557
761
|
error: error.message,
|
|
@@ -636,10 +840,18 @@ Write output to: ${outputPath}`;
|
|
|
636
840
|
const startTime = Date.now();
|
|
637
841
|
const failures = [];
|
|
638
842
|
let successCount = 0;
|
|
843
|
+
const itemCount = stories.length;
|
|
639
844
|
this.logger.info({
|
|
640
845
|
interval: config.storyInterval,
|
|
641
846
|
storyCount: stories.length,
|
|
642
847
|
}, 'Starting development phase');
|
|
848
|
+
// Invoke onPhaseStart callback
|
|
849
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
850
|
+
itemCount,
|
|
851
|
+
metadata: { interval: config.storyInterval },
|
|
852
|
+
phaseName: 'dev',
|
|
853
|
+
startTime,
|
|
854
|
+
});
|
|
643
855
|
try {
|
|
644
856
|
const storyDir = await this.pathResolver.getStoryDir();
|
|
645
857
|
const qaStoryDir = await this.pathResolver.getQaStoryDir();
|
|
@@ -685,7 +897,9 @@ Write output to: ${outputPath}`;
|
|
|
685
897
|
storyType: detection.type,
|
|
686
898
|
}, 'Story type detected, auto-including documentation references');
|
|
687
899
|
// Build prompt with auto-detected references
|
|
688
|
-
|
|
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`;
|
|
689
903
|
// Add working directory instruction if specified
|
|
690
904
|
if (config.cwd) {
|
|
691
905
|
prompt += `Working directory: ${config.cwd}\n\n`;
|
|
@@ -701,6 +915,20 @@ Write output to: ${outputPath}`;
|
|
|
701
915
|
prompt += '\n';
|
|
702
916
|
}
|
|
703
917
|
prompt += '*yolo mode*\n';
|
|
918
|
+
// Generate unique spawn ID and timestamp
|
|
919
|
+
const spawnStartTime = Date.now();
|
|
920
|
+
const spawnId = `dev-${story.fullNumber}-${spawnStartTime}`;
|
|
921
|
+
// Invoke onSpawnStart callback
|
|
922
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
923
|
+
agentType: 'dev',
|
|
924
|
+
itemId: story.fullNumber,
|
|
925
|
+
itemTitle: story.title,
|
|
926
|
+
outputPath: storyFilePath,
|
|
927
|
+
phaseName: 'dev',
|
|
928
|
+
prompt,
|
|
929
|
+
spawnId,
|
|
930
|
+
startTime: spawnStartTime,
|
|
931
|
+
});
|
|
704
932
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
705
933
|
agentType: 'dev',
|
|
706
934
|
references: config.references,
|
|
@@ -710,7 +938,23 @@ Write output to: ${outputPath}`;
|
|
|
710
938
|
logger: this.logger,
|
|
711
939
|
maxRetries: config.maxRetries,
|
|
712
940
|
});
|
|
941
|
+
const spawnEndTime = Date.now();
|
|
942
|
+
const spawnDuration = spawnEndTime - spawnStartTime;
|
|
713
943
|
if (result.success) {
|
|
944
|
+
// Invoke onSpawnComplete callback for success
|
|
945
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
946
|
+
agentType: 'dev',
|
|
947
|
+
duration: spawnDuration,
|
|
948
|
+
endTime: spawnEndTime,
|
|
949
|
+
itemId: story.fullNumber,
|
|
950
|
+
itemTitle: story.title,
|
|
951
|
+
output: result.output,
|
|
952
|
+
outputPath: storyFilePath,
|
|
953
|
+
phaseName: 'dev',
|
|
954
|
+
spawnId,
|
|
955
|
+
startTime: spawnStartTime,
|
|
956
|
+
success: true,
|
|
957
|
+
});
|
|
714
958
|
// Update story status to Done
|
|
715
959
|
await this.updateStoryStatus(storyFilePath, 'Done');
|
|
716
960
|
// Move to QA folder (qaFilePath already defined above with correct filename)
|
|
@@ -719,6 +963,27 @@ Write output to: ${outputPath}`;
|
|
|
719
963
|
this.logger.info({ storyNumber: story.fullNumber }, 'Story development completed successfully');
|
|
720
964
|
}
|
|
721
965
|
else {
|
|
966
|
+
// Invoke onSpawnComplete callback for failure
|
|
967
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
968
|
+
agentType: 'dev',
|
|
969
|
+
duration: spawnDuration,
|
|
970
|
+
endTime: spawnEndTime,
|
|
971
|
+
error: result.errors,
|
|
972
|
+
itemId: story.fullNumber,
|
|
973
|
+
itemTitle: story.title,
|
|
974
|
+
outputPath: storyFilePath,
|
|
975
|
+
phaseName: 'dev',
|
|
976
|
+
spawnId,
|
|
977
|
+
startTime: spawnStartTime,
|
|
978
|
+
success: false,
|
|
979
|
+
});
|
|
980
|
+
// Invoke onError callback for spawn failure
|
|
981
|
+
this.invokeErrorCallback(new Error(result.errors), {
|
|
982
|
+
itemId: story.fullNumber,
|
|
983
|
+
phaseName: 'dev',
|
|
984
|
+
recoverable: true,
|
|
985
|
+
spawnId,
|
|
986
|
+
});
|
|
722
987
|
this.logger.error({
|
|
723
988
|
error: result.errors,
|
|
724
989
|
storyNumber: story.fullNumber,
|
|
@@ -735,11 +1000,22 @@ Write output to: ${outputPath}`;
|
|
|
735
1000
|
}
|
|
736
1001
|
}
|
|
737
1002
|
const duration = Date.now() - startTime;
|
|
1003
|
+
const endTime = Date.now();
|
|
738
1004
|
this.logger.info({
|
|
739
1005
|
duration,
|
|
740
1006
|
failures: failures.length,
|
|
741
1007
|
success: successCount,
|
|
742
1008
|
}, 'Development phase completed');
|
|
1009
|
+
// Invoke onPhaseComplete callback
|
|
1010
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1011
|
+
duration,
|
|
1012
|
+
endTime,
|
|
1013
|
+
failureCount: failures.length,
|
|
1014
|
+
itemCount,
|
|
1015
|
+
phaseName: 'dev',
|
|
1016
|
+
startTime,
|
|
1017
|
+
successCount,
|
|
1018
|
+
});
|
|
743
1019
|
return {
|
|
744
1020
|
duration,
|
|
745
1021
|
failures,
|
|
@@ -749,9 +1025,28 @@ Write output to: ${outputPath}`;
|
|
|
749
1025
|
};
|
|
750
1026
|
}
|
|
751
1027
|
catch (error) {
|
|
1028
|
+
const duration = Date.now() - startTime;
|
|
1029
|
+
const endTime = Date.now();
|
|
752
1030
|
this.logger.error({ error: error.message }, 'Development phase failed');
|
|
1031
|
+
// Invoke onError callback
|
|
1032
|
+
this.invokeErrorCallback(error, {
|
|
1033
|
+
itemId: 'dev-phase',
|
|
1034
|
+
phaseName: 'dev',
|
|
1035
|
+
recoverable: false,
|
|
1036
|
+
});
|
|
1037
|
+
// Invoke onPhaseComplete callback even on error
|
|
1038
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1039
|
+
duration,
|
|
1040
|
+
endTime,
|
|
1041
|
+
failureCount: failures.length + 1,
|
|
1042
|
+
itemCount,
|
|
1043
|
+
metadata: { error: error.message },
|
|
1044
|
+
phaseName: 'dev',
|
|
1045
|
+
startTime,
|
|
1046
|
+
successCount,
|
|
1047
|
+
});
|
|
753
1048
|
return {
|
|
754
|
-
duration
|
|
1049
|
+
duration,
|
|
755
1050
|
failures: [
|
|
756
1051
|
...failures,
|
|
757
1052
|
{
|
|
@@ -780,11 +1075,19 @@ Write output to: ${outputPath}`;
|
|
|
780
1075
|
const startTime = Date.now();
|
|
781
1076
|
const failures = [];
|
|
782
1077
|
let successCount = 0;
|
|
1078
|
+
let itemCount = 0;
|
|
783
1079
|
this.logger.info({
|
|
784
1080
|
interval: config.prdInterval,
|
|
785
1081
|
parallel: config.parallel,
|
|
786
1082
|
prdFile: prdFilePath,
|
|
787
1083
|
}, 'Starting epic creation phase');
|
|
1084
|
+
// Invoke onPhaseStart callback (itemCount will be updated once epics are parsed)
|
|
1085
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
1086
|
+
itemCount: 0,
|
|
1087
|
+
metadata: { parallel: config.parallel, prdFilePath },
|
|
1088
|
+
phaseName: 'epic',
|
|
1089
|
+
startTime,
|
|
1090
|
+
});
|
|
788
1091
|
try {
|
|
789
1092
|
// Read PRD file
|
|
790
1093
|
let prdContent = await this.fileManager.readFile(prdFilePath);
|
|
@@ -819,8 +1122,19 @@ Write output to: ${outputPath}`;
|
|
|
819
1122
|
}
|
|
820
1123
|
}
|
|
821
1124
|
this.logger.info({ epicCount: epics.length }, 'Epics extracted from PRD');
|
|
1125
|
+
itemCount = epics.length;
|
|
822
1126
|
if (epics.length === 0) {
|
|
823
1127
|
this.logger.warn('No epics found in PRD file');
|
|
1128
|
+
// Invoke onPhaseComplete for empty phase
|
|
1129
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1130
|
+
duration: Date.now() - startTime,
|
|
1131
|
+
endTime: Date.now(),
|
|
1132
|
+
failureCount: 0,
|
|
1133
|
+
itemCount: 0,
|
|
1134
|
+
phaseName: 'epic',
|
|
1135
|
+
startTime,
|
|
1136
|
+
successCount: 0,
|
|
1137
|
+
});
|
|
824
1138
|
return {
|
|
825
1139
|
duration: Date.now() - startTime,
|
|
826
1140
|
failures: [],
|
|
@@ -837,8 +1151,18 @@ Write output to: ${outputPath}`;
|
|
|
837
1151
|
const epicsToCreate = epics.filter((epic) => !properlyPopulatedEpics.includes(this.generateEpicFileName(prefix, epic.number)));
|
|
838
1152
|
if (epicsToCreate.length === 0) {
|
|
839
1153
|
this.logger.info('All epics already exist and are properly populated, skipping creation');
|
|
1154
|
+
const duration = Date.now() - startTime;
|
|
1155
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1156
|
+
duration,
|
|
1157
|
+
endTime: Date.now(),
|
|
1158
|
+
failureCount: 0,
|
|
1159
|
+
itemCount: epics.length,
|
|
1160
|
+
phaseName: 'epic',
|
|
1161
|
+
startTime,
|
|
1162
|
+
successCount: epics.length,
|
|
1163
|
+
});
|
|
840
1164
|
return {
|
|
841
|
-
duration
|
|
1165
|
+
duration,
|
|
842
1166
|
failures: [],
|
|
843
1167
|
phaseName: 'epic',
|
|
844
1168
|
skipped: false,
|
|
@@ -851,99 +1175,142 @@ Write output to: ${outputPath}`;
|
|
|
851
1175
|
}, 'Creating/re-populating epic files');
|
|
852
1176
|
// Create epic files using BatchProcessor and ClaudeAgentRunner
|
|
853
1177
|
const results = await this.batchProcessor.processBatch(epicsToCreate, async (epic) => {
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1178
|
+
const spawnStartTime = Date.now();
|
|
1179
|
+
const spawnId = `epic-${epic.number}`;
|
|
1180
|
+
// Fire onSpawnStart so the reporter shows which epic is being generated
|
|
1181
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
1182
|
+
agentType: 'architect',
|
|
1183
|
+
itemId: `epic-${epic.number}`,
|
|
1184
|
+
itemTitle: epic.title,
|
|
1185
|
+
phaseName: 'epic',
|
|
1186
|
+
spawnId,
|
|
1187
|
+
startTime: spawnStartTime,
|
|
1188
|
+
});
|
|
1189
|
+
try {
|
|
1190
|
+
// Generate epic file path with prefix
|
|
1191
|
+
const epicFileName = this.generateEpicFileName(prefix, epic.number);
|
|
1192
|
+
const epicFilePath = `${epicDir}/${epicFileName}`;
|
|
1193
|
+
// Check if file already exists and is properly populated
|
|
1194
|
+
const fileExists = await this.fileManager.fileExists(epicFilePath);
|
|
1195
|
+
if (fileExists) {
|
|
1196
|
+
// Check if the file is properly populated (has actual story entries, not placeholder text)
|
|
1197
|
+
const existingContent = await this.fileManager.readFile(epicFilePath);
|
|
1198
|
+
const isScaffolded = existingContent.includes('_[AI Agent will populate');
|
|
1199
|
+
if (!isScaffolded) {
|
|
1200
|
+
// File is properly populated, skip recreation
|
|
1201
|
+
this.logger.info({
|
|
1202
|
+
epicNumber: epic.number,
|
|
1203
|
+
epicTitle: epic.title,
|
|
1204
|
+
filePath: epicFilePath,
|
|
1205
|
+
}, 'Epic file already exists and is properly populated, skipping creation');
|
|
1206
|
+
return epicFilePath;
|
|
1207
|
+
}
|
|
1208
|
+
// File exists but is only scaffolded (incomplete) - will re-populate it
|
|
865
1209
|
this.logger.info({
|
|
866
1210
|
epicNumber: epic.number,
|
|
867
1211
|
epicTitle: epic.title,
|
|
868
1212
|
filePath: epicFilePath,
|
|
869
|
-
}, 'Epic file
|
|
870
|
-
return epicFilePath;
|
|
1213
|
+
}, 'Epic file exists but is only scaffolded (incomplete), re-populating with Claude agent');
|
|
871
1214
|
}
|
|
872
|
-
//
|
|
873
|
-
this.
|
|
874
|
-
epicNumber: epic.number,
|
|
875
|
-
epicTitle: epic.title,
|
|
876
|
-
filePath: epicFilePath,
|
|
877
|
-
}, 'Epic file exists but is only scaffolded (incomplete), re-populating with Claude agent');
|
|
878
|
-
}
|
|
879
|
-
// Step 1: Create scaffolded file with structured sections and populated metadata (if not exists)
|
|
880
|
-
const scaffoldedContent = this.fileScaffolder.scaffoldEpic({
|
|
881
|
-
epicNumber: epic.number,
|
|
882
|
-
epicTitle: epic.title,
|
|
883
|
-
prefix,
|
|
884
|
-
});
|
|
885
|
-
if (fileExists) {
|
|
886
|
-
this.logger.info({
|
|
887
|
-
epicNumber: epic.number,
|
|
888
|
-
epicTitle: epic.title,
|
|
889
|
-
filePath: epicFilePath,
|
|
890
|
-
}, 'Using existing scaffolded epic file');
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
await this.fileManager.writeFile(epicFilePath, scaffoldedContent);
|
|
894
|
-
this.logger.info({
|
|
895
|
-
epicNumber: epic.number,
|
|
896
|
-
epicTitle: epic.title,
|
|
897
|
-
filePath: epicFilePath,
|
|
898
|
-
}, 'Epic scaffolded file created');
|
|
899
|
-
}
|
|
900
|
-
// Step 2: Build Claude prompt to populate the scaffolded file
|
|
901
|
-
const prompt = this.buildEpicPrompt(epic, {
|
|
902
|
-
cwd: config.cwd,
|
|
903
|
-
outputPath: epicFilePath,
|
|
904
|
-
prdPath: prdFilePath,
|
|
905
|
-
prefix,
|
|
906
|
-
references: config.references,
|
|
907
|
-
});
|
|
908
|
-
// Log prompt if verbose
|
|
909
|
-
if (config.verbose) {
|
|
910
|
-
this.logger.info({
|
|
1215
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata (if not exists)
|
|
1216
|
+
const scaffoldedContent = this.fileScaffolder.scaffoldEpic({
|
|
911
1217
|
epicNumber: epic.number,
|
|
912
1218
|
epicTitle: epic.title,
|
|
1219
|
+
prefix,
|
|
1220
|
+
});
|
|
1221
|
+
if (fileExists) {
|
|
1222
|
+
this.logger.info({
|
|
1223
|
+
epicNumber: epic.number,
|
|
1224
|
+
epicTitle: epic.title,
|
|
1225
|
+
filePath: epicFilePath,
|
|
1226
|
+
}, 'Using existing scaffolded epic file');
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
await this.fileManager.writeFile(epicFilePath, scaffoldedContent);
|
|
1230
|
+
this.logger.info({
|
|
1231
|
+
epicNumber: epic.number,
|
|
1232
|
+
epicTitle: epic.title,
|
|
1233
|
+
filePath: epicFilePath,
|
|
1234
|
+
}, 'Epic scaffolded file created');
|
|
1235
|
+
}
|
|
1236
|
+
// Step 2: Build Claude prompt to populate the scaffolded file
|
|
1237
|
+
const mcpPrefix = await this.getMcpPromptPrefix('epic', 'architect', config);
|
|
1238
|
+
const basePrompt = this.buildEpicPrompt(epic, {
|
|
1239
|
+
cwd: config.cwd,
|
|
913
1240
|
outputPath: epicFilePath,
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
this.
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1241
|
+
prdPath: prdFilePath,
|
|
1242
|
+
prefix,
|
|
1243
|
+
references: config.references,
|
|
1244
|
+
});
|
|
1245
|
+
const prompt = mcpPrefix ? `${mcpPrefix}\n\n${basePrompt}` : basePrompt;
|
|
1246
|
+
// Log prompt if verbose
|
|
1247
|
+
if (config.verbose) {
|
|
1248
|
+
this.logger.info({
|
|
1249
|
+
epicNumber: epic.number,
|
|
1250
|
+
epicTitle: epic.title,
|
|
1251
|
+
outputPath: epicFilePath,
|
|
1252
|
+
prompt,
|
|
1253
|
+
}, 'Claude Prompt (Epic)');
|
|
1254
|
+
}
|
|
1255
|
+
// Step 3: Run Claude agent to populate content sections
|
|
1256
|
+
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
1257
|
+
agentType: 'architect',
|
|
1258
|
+
references: config.references,
|
|
1259
|
+
timeout: config.timeout ?? 2_700_000,
|
|
1260
|
+
}, {
|
|
1261
|
+
backoffMs: config.retryBackoffMs,
|
|
1262
|
+
logger: this.logger,
|
|
1263
|
+
maxRetries: config.maxRetries,
|
|
1264
|
+
});
|
|
1265
|
+
// Log output if verbose
|
|
1266
|
+
if (config.verbose) {
|
|
1267
|
+
this.logger.info({
|
|
1268
|
+
duration: result.duration,
|
|
1269
|
+
epicNumber: epic.number,
|
|
1270
|
+
errors: result.errors,
|
|
1271
|
+
output: result.output,
|
|
1272
|
+
outputLength: result.output.length,
|
|
1273
|
+
success: result.success,
|
|
1274
|
+
}, 'Claude Response (Epic)');
|
|
1275
|
+
}
|
|
1276
|
+
if (!result.success) {
|
|
1277
|
+
throw new Error(result.errors);
|
|
1278
|
+
}
|
|
1279
|
+
// Step 4: Verify file was updated by Claude
|
|
1280
|
+
const updatedContent = await this.fileManager.readFile(epicFilePath);
|
|
1281
|
+
if (updatedContent === scaffoldedContent) {
|
|
1282
|
+
throw new Error(`Claude did not update the epic file at ${epicFilePath}`);
|
|
1283
|
+
}
|
|
1284
|
+
// Fire onSpawnComplete for success
|
|
1285
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
1286
|
+
agentType: 'architect',
|
|
1287
|
+
duration: Date.now() - spawnStartTime,
|
|
1288
|
+
endTime: Date.now(),
|
|
1289
|
+
itemId: `epic-${epic.number}`,
|
|
1290
|
+
itemTitle: epic.title,
|
|
1291
|
+
phaseName: 'epic',
|
|
1292
|
+
spawnId,
|
|
1293
|
+
startTime: spawnStartTime,
|
|
1294
|
+
success: true,
|
|
1295
|
+
});
|
|
1296
|
+
return epicFilePath;
|
|
940
1297
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1298
|
+
catch (spawnError) {
|
|
1299
|
+
// Fire onSpawnComplete for failure
|
|
1300
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
1301
|
+
agentType: 'architect',
|
|
1302
|
+
duration: Date.now() - spawnStartTime,
|
|
1303
|
+
endTime: Date.now(),
|
|
1304
|
+
error: spawnError.message,
|
|
1305
|
+
itemId: `epic-${epic.number}`,
|
|
1306
|
+
itemTitle: epic.title,
|
|
1307
|
+
phaseName: 'epic',
|
|
1308
|
+
spawnId,
|
|
1309
|
+
startTime: spawnStartTime,
|
|
1310
|
+
success: false,
|
|
1311
|
+
});
|
|
1312
|
+
throw spawnError;
|
|
945
1313
|
}
|
|
946
|
-
return epicFilePath;
|
|
947
1314
|
}, (info) => {
|
|
948
1315
|
this.logger.info({
|
|
949
1316
|
completedItems: info.completedItems,
|
|
@@ -967,11 +1334,23 @@ Write output to: ${outputPath}`;
|
|
|
967
1334
|
// Add properly populated epics to success count
|
|
968
1335
|
successCount += properlyPopulatedEpics.length;
|
|
969
1336
|
const duration = Date.now() - startTime;
|
|
1337
|
+
const endTime = Date.now();
|
|
970
1338
|
this.logger.info({
|
|
971
1339
|
duration,
|
|
972
1340
|
failures: failures.length,
|
|
973
1341
|
success: successCount,
|
|
974
1342
|
}, 'Epic creation phase completed');
|
|
1343
|
+
// Invoke onPhaseComplete callback
|
|
1344
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1345
|
+
duration,
|
|
1346
|
+
endTime,
|
|
1347
|
+
failureCount: failures.length,
|
|
1348
|
+
itemCount,
|
|
1349
|
+
metadata: { parallel: config.parallel, prdFilePath },
|
|
1350
|
+
phaseName: 'epic',
|
|
1351
|
+
startTime,
|
|
1352
|
+
successCount,
|
|
1353
|
+
});
|
|
975
1354
|
return {
|
|
976
1355
|
duration,
|
|
977
1356
|
failures,
|
|
@@ -981,9 +1360,28 @@ Write output to: ${outputPath}`;
|
|
|
981
1360
|
};
|
|
982
1361
|
}
|
|
983
1362
|
catch (error) {
|
|
1363
|
+
const duration = Date.now() - startTime;
|
|
1364
|
+
const endTime = Date.now();
|
|
984
1365
|
this.logger.error({ error: error.message }, 'Epic phase failed');
|
|
1366
|
+
// Invoke onError callback
|
|
1367
|
+
this.invokeErrorCallback(error, {
|
|
1368
|
+
itemId: 'epic-phase',
|
|
1369
|
+
phaseName: 'epic',
|
|
1370
|
+
recoverable: false,
|
|
1371
|
+
});
|
|
1372
|
+
// Invoke onPhaseComplete callback even on error
|
|
1373
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1374
|
+
duration,
|
|
1375
|
+
endTime,
|
|
1376
|
+
failureCount: failures.length + 1,
|
|
1377
|
+
itemCount,
|
|
1378
|
+
metadata: { error: error.message },
|
|
1379
|
+
phaseName: 'epic',
|
|
1380
|
+
startTime,
|
|
1381
|
+
successCount,
|
|
1382
|
+
});
|
|
985
1383
|
return {
|
|
986
|
-
duration
|
|
1384
|
+
duration,
|
|
987
1385
|
failures: [
|
|
988
1386
|
...failures,
|
|
989
1387
|
{
|
|
@@ -1050,7 +1448,7 @@ Write output to: ${outputPath}`;
|
|
|
1050
1448
|
* @returns PhaseResult with aggregate success/failure counts and duration
|
|
1051
1449
|
* @private
|
|
1052
1450
|
*/
|
|
1053
|
-
async executePipelinedDevPhase(queue, config) {
|
|
1451
|
+
async executePipelinedDevPhase(queue, config, reviewQueue) {
|
|
1054
1452
|
const startTime = Date.now();
|
|
1055
1453
|
// PIPELINE MODE: Use single worker for sequential story processing
|
|
1056
1454
|
// Stories are processed in order, one at a time as they become available
|
|
@@ -1058,13 +1456,19 @@ Write output to: ${outputPath}`;
|
|
|
1058
1456
|
this.logger.info({
|
|
1059
1457
|
interval: config.storyInterval,
|
|
1060
1458
|
mode: 'sequential',
|
|
1459
|
+
reviewEnabled: !!reviewQueue,
|
|
1061
1460
|
workerCount,
|
|
1062
1461
|
}, 'Starting pipelined development phase (sequential processing)');
|
|
1063
1462
|
try {
|
|
1064
|
-
// Create single worker for sequential processing
|
|
1065
|
-
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));
|
|
1066
1465
|
// Wait for worker to complete
|
|
1067
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
|
+
}
|
|
1068
1472
|
// Aggregate results from all workers
|
|
1069
1473
|
let totalSuccess = 0;
|
|
1070
1474
|
const allFailures = [];
|
|
@@ -1079,6 +1483,15 @@ Write output to: ${outputPath}`;
|
|
|
1079
1483
|
success: totalSuccess,
|
|
1080
1484
|
workerCount,
|
|
1081
1485
|
}, 'Pipelined development phase completed');
|
|
1486
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1487
|
+
duration,
|
|
1488
|
+
endTime: Date.now(),
|
|
1489
|
+
failureCount: allFailures.length,
|
|
1490
|
+
itemCount: totalSuccess + allFailures.length,
|
|
1491
|
+
phaseName: 'dev',
|
|
1492
|
+
startTime,
|
|
1493
|
+
successCount: totalSuccess,
|
|
1494
|
+
});
|
|
1082
1495
|
return {
|
|
1083
1496
|
duration,
|
|
1084
1497
|
failures: allFailures,
|
|
@@ -1089,6 +1502,20 @@ Write output to: ${outputPath}`;
|
|
|
1089
1502
|
}
|
|
1090
1503
|
catch (error) {
|
|
1091
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
|
+
}
|
|
1510
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1511
|
+
duration: Date.now() - startTime,
|
|
1512
|
+
endTime: Date.now(),
|
|
1513
|
+
failureCount: 1,
|
|
1514
|
+
itemCount: 1,
|
|
1515
|
+
phaseName: 'dev',
|
|
1516
|
+
startTime,
|
|
1517
|
+
successCount: 0,
|
|
1518
|
+
});
|
|
1092
1519
|
return {
|
|
1093
1520
|
duration: Date.now() - startTime,
|
|
1094
1521
|
failures: [
|
|
@@ -1104,19 +1531,187 @@ Write output to: ${outputPath}`;
|
|
|
1104
1531
|
}
|
|
1105
1532
|
}
|
|
1106
1533
|
/**
|
|
1107
|
-
*
|
|
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)
|
|
1108
1662
|
*
|
|
1109
1663
|
* @param config - Workflow configuration
|
|
1110
1664
|
* @param detection - Input detection result
|
|
1111
|
-
* @returns Story and
|
|
1665
|
+
* @returns Story, dev, and optional review phase results
|
|
1112
1666
|
* @private
|
|
1113
1667
|
*/
|
|
1114
1668
|
async executePipelinedWorkflow(config, detection) {
|
|
1115
|
-
|
|
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)');
|
|
1116
1672
|
const queue = new StoryQueue(this.logger);
|
|
1673
|
+
const reviewQueue = enableReview ? new ReviewQueue(this.logger) : undefined;
|
|
1117
1674
|
try {
|
|
1118
1675
|
const storyPhasePromise = this.executeStoryPhaseWithQueue(config, detection, queue);
|
|
1119
|
-
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)
|
|
1120
1715
|
const [storyResult, devResult] = await Promise.allSettled([storyPhasePromise, devPhasePromise]);
|
|
1121
1716
|
return {
|
|
1122
1717
|
devPhase: this.handlePhaseResult(devResult, 'dev'),
|
|
@@ -1146,9 +1741,17 @@ Write output to: ${outputPath}`;
|
|
|
1146
1741
|
const startTime = Date.now();
|
|
1147
1742
|
const failures = [];
|
|
1148
1743
|
let successCount = 0;
|
|
1744
|
+
let itemCount = 0;
|
|
1149
1745
|
this.logger.info({
|
|
1150
1746
|
qaRetries: config.qaRetries ?? 2,
|
|
1151
1747
|
}, 'Starting QA phase');
|
|
1748
|
+
// Invoke onPhaseStart callback
|
|
1749
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
1750
|
+
itemCount: 0,
|
|
1751
|
+
metadata: { qaRetries: config.qaRetries ?? 2 },
|
|
1752
|
+
phaseName: 'qa',
|
|
1753
|
+
startTime,
|
|
1754
|
+
});
|
|
1152
1755
|
try {
|
|
1153
1756
|
// Get QA story directory
|
|
1154
1757
|
const qaStoryDir = await this.pathResolver.getQaStoryDir();
|
|
@@ -1158,6 +1761,17 @@ Write output to: ${outputPath}`;
|
|
|
1158
1761
|
const storyFiles = await this.fileManager.listFiles(qaStoryDir, storyPattern);
|
|
1159
1762
|
if (storyFiles.length === 0) {
|
|
1160
1763
|
this.logger.info('No stories found in QA folder, skipping QA phase');
|
|
1764
|
+
// Invoke onPhaseComplete for skipped phase
|
|
1765
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1766
|
+
duration: Date.now() - startTime,
|
|
1767
|
+
endTime: Date.now(),
|
|
1768
|
+
failureCount: 0,
|
|
1769
|
+
itemCount: 0,
|
|
1770
|
+
phaseName: 'qa',
|
|
1771
|
+
skipped: true,
|
|
1772
|
+
startTime,
|
|
1773
|
+
successCount: 0,
|
|
1774
|
+
});
|
|
1161
1775
|
return {
|
|
1162
1776
|
duration: Date.now() - startTime,
|
|
1163
1777
|
failures: [],
|
|
@@ -1166,6 +1780,7 @@ Write output to: ${outputPath}`;
|
|
|
1166
1780
|
success: 0,
|
|
1167
1781
|
};
|
|
1168
1782
|
}
|
|
1783
|
+
itemCount = storyFiles.length;
|
|
1169
1784
|
this.logger.info({ storyCount: storyFiles.length }, 'Found stories for QA phase');
|
|
1170
1785
|
// Dynamically import QA command to avoid circular dependencies
|
|
1171
1786
|
const { default: StoriesQaCommand } = await import('../../commands/stories/qa.js');
|
|
@@ -1190,8 +1805,29 @@ Write output to: ${outputPath}`;
|
|
|
1190
1805
|
qaFlags.push(`--reference=${ref}`);
|
|
1191
1806
|
}
|
|
1192
1807
|
}
|
|
1193
|
-
//
|
|
1194
|
-
|
|
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
|
+
}
|
|
1815
|
+
// Suppress QA command's pino logger in non-verbose mode
|
|
1816
|
+
const prevLogLevel = process.env.LOG_LEVEL;
|
|
1817
|
+
if (!config.verbose) {
|
|
1818
|
+
process.env.LOG_LEVEL = 'silent';
|
|
1819
|
+
}
|
|
1820
|
+
try {
|
|
1821
|
+
await StoriesQaCommand.run([...qaArgs, ...qaFlags]);
|
|
1822
|
+
}
|
|
1823
|
+
finally {
|
|
1824
|
+
if (prevLogLevel === undefined) {
|
|
1825
|
+
delete process.env.LOG_LEVEL;
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
process.env.LOG_LEVEL = prevLogLevel;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1195
1831
|
successCount++;
|
|
1196
1832
|
this.logger.info({ storyPath }, 'QA workflow completed successfully for story');
|
|
1197
1833
|
}
|
|
@@ -1205,11 +1841,22 @@ Write output to: ${outputPath}`;
|
|
|
1205
1841
|
}
|
|
1206
1842
|
}
|
|
1207
1843
|
const duration = Date.now() - startTime;
|
|
1844
|
+
const endTime = Date.now();
|
|
1208
1845
|
this.logger.info({
|
|
1209
1846
|
duration,
|
|
1210
1847
|
failures: failures.length,
|
|
1211
1848
|
success: successCount,
|
|
1212
1849
|
}, 'QA phase completed');
|
|
1850
|
+
// Invoke onPhaseComplete callback
|
|
1851
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1852
|
+
duration,
|
|
1853
|
+
endTime,
|
|
1854
|
+
failureCount: failures.length,
|
|
1855
|
+
itemCount,
|
|
1856
|
+
phaseName: 'qa',
|
|
1857
|
+
startTime,
|
|
1858
|
+
successCount,
|
|
1859
|
+
});
|
|
1213
1860
|
return {
|
|
1214
1861
|
duration,
|
|
1215
1862
|
failures,
|
|
@@ -1219,9 +1866,28 @@ Write output to: ${outputPath}`;
|
|
|
1219
1866
|
};
|
|
1220
1867
|
}
|
|
1221
1868
|
catch (error) {
|
|
1869
|
+
const duration = Date.now() - startTime;
|
|
1870
|
+
const endTime = Date.now();
|
|
1222
1871
|
this.logger.error({ error: error.message }, 'QA phase failed');
|
|
1872
|
+
// Invoke onError callback
|
|
1873
|
+
this.invokeErrorCallback(error, {
|
|
1874
|
+
itemId: 'qa-phase',
|
|
1875
|
+
phaseName: 'qa',
|
|
1876
|
+
recoverable: false,
|
|
1877
|
+
});
|
|
1878
|
+
// Invoke onPhaseComplete callback even on error
|
|
1879
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1880
|
+
duration,
|
|
1881
|
+
endTime,
|
|
1882
|
+
failureCount: failures.length + 1,
|
|
1883
|
+
itemCount,
|
|
1884
|
+
metadata: { error: error.message },
|
|
1885
|
+
phaseName: 'qa',
|
|
1886
|
+
startTime,
|
|
1887
|
+
successCount,
|
|
1888
|
+
});
|
|
1223
1889
|
return {
|
|
1224
|
-
duration
|
|
1890
|
+
duration,
|
|
1225
1891
|
failures: [
|
|
1226
1892
|
...failures,
|
|
1227
1893
|
{
|
|
@@ -1319,6 +1985,43 @@ Write output to: ${outputPath}`;
|
|
|
1319
1985
|
return { devPhase: this.createSkippedPhaseResult('dev'), storyPhase };
|
|
1320
1986
|
}
|
|
1321
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
|
+
}
|
|
1322
2025
|
return { devPhase, storyPhase };
|
|
1323
2026
|
}
|
|
1324
2027
|
/**
|
|
@@ -1355,16 +2058,24 @@ Write output to: ${outputPath}`;
|
|
|
1355
2058
|
const startTime = Date.now();
|
|
1356
2059
|
const failures = [];
|
|
1357
2060
|
let successCount = 0;
|
|
2061
|
+
let itemCount = 0;
|
|
1358
2062
|
// In pipeline mode, stories must be created sequentially (one at a time)
|
|
1359
2063
|
// to ensure they are queued in the correct order for development
|
|
1360
2064
|
const isPipelineMode = Boolean(onStoryComplete);
|
|
1361
|
-
const effectiveParallel =
|
|
2065
|
+
const effectiveParallel = config.parallel;
|
|
1362
2066
|
this.logger.info({
|
|
1363
2067
|
epicCount: epics.length,
|
|
1364
2068
|
interval: config.epicInterval,
|
|
1365
2069
|
mode: isPipelineMode ? 'sequential (pipeline)' : 'parallel',
|
|
1366
2070
|
parallel: effectiveParallel,
|
|
1367
2071
|
}, 'Starting story creation phase');
|
|
2072
|
+
// Invoke onPhaseStart callback
|
|
2073
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
2074
|
+
itemCount: 0,
|
|
2075
|
+
metadata: { epicCount: epics.length, parallel: effectiveParallel, pipelineMode: isPipelineMode },
|
|
2076
|
+
phaseName: 'story',
|
|
2077
|
+
startTime,
|
|
2078
|
+
});
|
|
1368
2079
|
try {
|
|
1369
2080
|
const allStories = [];
|
|
1370
2081
|
// Get epic directory
|
|
@@ -1397,8 +2108,19 @@ Write output to: ${outputPath}`;
|
|
|
1397
2108
|
// Flatten the array of story arrays
|
|
1398
2109
|
allStories.push(...epicStories.flat());
|
|
1399
2110
|
this.logger.info({ storyCount: allStories.length }, 'Stories extracted from epics');
|
|
2111
|
+
itemCount = allStories.length;
|
|
1400
2112
|
if (allStories.length === 0) {
|
|
1401
2113
|
this.logger.warn('No stories found in epic files');
|
|
2114
|
+
// Invoke onPhaseComplete for empty phase
|
|
2115
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2116
|
+
duration: Date.now() - startTime,
|
|
2117
|
+
endTime: Date.now(),
|
|
2118
|
+
failureCount: 0,
|
|
2119
|
+
itemCount: 0,
|
|
2120
|
+
phaseName: 'story',
|
|
2121
|
+
startTime,
|
|
2122
|
+
successCount: 0,
|
|
2123
|
+
});
|
|
1402
2124
|
return {
|
|
1403
2125
|
duration: Date.now() - startTime,
|
|
1404
2126
|
failures: [],
|
|
@@ -1420,8 +2142,18 @@ Write output to: ${outputPath}`;
|
|
|
1420
2142
|
if (onStoryComplete) {
|
|
1421
2143
|
await this.enqueueExistingStoriesSequentially(allStories, storyDir, prefix, onStoryComplete);
|
|
1422
2144
|
}
|
|
2145
|
+
const duration = Date.now() - startTime;
|
|
2146
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2147
|
+
duration,
|
|
2148
|
+
endTime: Date.now(),
|
|
2149
|
+
failureCount: 0,
|
|
2150
|
+
itemCount: allStories.length,
|
|
2151
|
+
phaseName: 'story',
|
|
2152
|
+
startTime,
|
|
2153
|
+
successCount: allStories.length,
|
|
2154
|
+
});
|
|
1423
2155
|
return {
|
|
1424
|
-
duration
|
|
2156
|
+
duration,
|
|
1425
2157
|
failures: [],
|
|
1426
2158
|
phaseName: 'story',
|
|
1427
2159
|
skipped: false,
|
|
@@ -1441,107 +2173,161 @@ Write output to: ${outputPath}`;
|
|
|
1441
2173
|
: this.batchProcessor;
|
|
1442
2174
|
// Create story files using BatchProcessor and ClaudeAgentRunner
|
|
1443
2175
|
const results = await storyBatchProcessor.processBatch(storiesToCreate, async (story) => {
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2176
|
+
const spawnStartTime = Date.now();
|
|
2177
|
+
const spawnId = `story-${story.fullNumber}`;
|
|
2178
|
+
// Fire onSpawnStart so the reporter shows which story is being created
|
|
2179
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
2180
|
+
agentType: 'sm',
|
|
2181
|
+
itemId: `story-${story.fullNumber}`,
|
|
2182
|
+
itemTitle: story.title,
|
|
2183
|
+
phaseName: 'story',
|
|
2184
|
+
spawnId,
|
|
2185
|
+
startTime: spawnStartTime,
|
|
2186
|
+
});
|
|
2187
|
+
try {
|
|
2188
|
+
// Generate story file path with prefix
|
|
2189
|
+
const storyFileName = this.generateStoryFileName(prefix, story.fullNumber);
|
|
2190
|
+
const storyFilePath = `${storyDir}/${storyFileName}`;
|
|
2191
|
+
// Check if file already exists (might have been created by parallel process)
|
|
2192
|
+
const fileExists = await this.fileManager.fileExists(storyFilePath);
|
|
2193
|
+
if (fileExists) {
|
|
2194
|
+
this.logger.info({
|
|
2195
|
+
filePath: storyFilePath,
|
|
2196
|
+
storyNumber: story.fullNumber,
|
|
2197
|
+
storyTitle: story.title,
|
|
2198
|
+
}, 'Story file already exists, skipping creation');
|
|
2199
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2200
|
+
agentType: 'sm',
|
|
2201
|
+
duration: Date.now() - spawnStartTime,
|
|
2202
|
+
endTime: Date.now(),
|
|
2203
|
+
itemId: `story-${story.fullNumber}`,
|
|
2204
|
+
itemTitle: story.title,
|
|
2205
|
+
phaseName: 'story',
|
|
2206
|
+
spawnId,
|
|
2207
|
+
startTime: spawnStartTime,
|
|
2208
|
+
success: true,
|
|
2209
|
+
});
|
|
2210
|
+
return storyFilePath;
|
|
2211
|
+
}
|
|
2212
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata
|
|
2213
|
+
const scaffoldedContent = this.fileScaffolder.scaffoldStory({
|
|
2214
|
+
epicNumber: story.epicNumber,
|
|
2215
|
+
storyNumber: story.number,
|
|
2216
|
+
storyTitle: story.title,
|
|
2217
|
+
});
|
|
2218
|
+
await this.fileManager.writeFile(storyFilePath, scaffoldedContent);
|
|
1450
2219
|
this.logger.info({
|
|
1451
2220
|
filePath: storyFilePath,
|
|
1452
2221
|
storyNumber: story.fullNumber,
|
|
1453
2222
|
storyTitle: story.title,
|
|
1454
|
-
}, 'Story file
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
await this.fileManager.writeFile(storyFilePath, scaffoldedContent);
|
|
1464
|
-
this.logger.info({
|
|
1465
|
-
filePath: storyFilePath,
|
|
1466
|
-
storyNumber: story.fullNumber,
|
|
1467
|
-
storyTitle: story.title,
|
|
1468
|
-
}, 'Story scaffolded file created');
|
|
1469
|
-
// Step 2: Generate epic file path for this story
|
|
1470
|
-
const epicFileName = this.generateEpicFileName(prefix, story.epicNumber);
|
|
1471
|
-
const epicFilePath = `${epicDir}/${epicFileName}`;
|
|
1472
|
-
// Step 3: Build Claude prompt to populate the scaffolded file
|
|
1473
|
-
const prompt = this.buildStoryPrompt(story, {
|
|
1474
|
-
cwd: config.cwd,
|
|
1475
|
-
epicPath: epicFilePath,
|
|
1476
|
-
outputPath: storyFilePath,
|
|
1477
|
-
prefix,
|
|
1478
|
-
references: config.references,
|
|
1479
|
-
});
|
|
1480
|
-
// Log prompt if verbose
|
|
1481
|
-
if (config.verbose) {
|
|
1482
|
-
this.logger.info({
|
|
2223
|
+
}, 'Story scaffolded file created');
|
|
2224
|
+
// Step 2: Generate epic file path for this story
|
|
2225
|
+
const epicFileName = this.generateEpicFileName(prefix, story.epicNumber);
|
|
2226
|
+
const epicFilePath = `${epicDir}/${epicFileName}`;
|
|
2227
|
+
// Step 3: Build Claude prompt to populate the scaffolded file
|
|
2228
|
+
const mcpPrefix = await this.getMcpPromptPrefix('story', 'sm', config);
|
|
2229
|
+
const basePrompt = this.buildStoryPrompt(story, {
|
|
2230
|
+
cwd: config.cwd,
|
|
2231
|
+
epicPath: epicFilePath,
|
|
1483
2232
|
outputPath: storyFilePath,
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
timeout: config.timeout ?? 2_700_000,
|
|
1494
|
-
}, {
|
|
1495
|
-
backoffMs: config.retryBackoffMs,
|
|
1496
|
-
logger: this.logger,
|
|
1497
|
-
maxRetries: config.maxRetries,
|
|
1498
|
-
});
|
|
1499
|
-
// Log output if verbose
|
|
1500
|
-
if (config.verbose) {
|
|
1501
|
-
this.logger.info({
|
|
1502
|
-
duration: result.duration,
|
|
1503
|
-
errors: result.errors,
|
|
1504
|
-
output: result.output,
|
|
1505
|
-
outputLength: result.output.length,
|
|
1506
|
-
storyNumber: story.fullNumber,
|
|
1507
|
-
success: result.success,
|
|
1508
|
-
}, 'Claude Response (Story)');
|
|
1509
|
-
}
|
|
1510
|
-
if (!result.success) {
|
|
1511
|
-
throw new Error(result.errors);
|
|
1512
|
-
}
|
|
1513
|
-
// Step 5: Verify file was updated by Claude
|
|
1514
|
-
const updatedContent = await this.fileManager.readFile(storyFilePath);
|
|
1515
|
-
if (updatedContent === scaffoldedContent) {
|
|
1516
|
-
throw new Error(`Claude did not update the story file at ${storyFilePath}`);
|
|
1517
|
-
}
|
|
1518
|
-
// Invoke callback after successful story creation
|
|
1519
|
-
if (onStoryComplete) {
|
|
1520
|
-
try {
|
|
1521
|
-
const metadata = {
|
|
1522
|
-
epicNumber: story.epicNumber,
|
|
1523
|
-
filePath: storyFilePath,
|
|
1524
|
-
id: story.fullNumber,
|
|
1525
|
-
number: story.fullNumber,
|
|
1526
|
-
status: 'Draft',
|
|
1527
|
-
storyNumber: story.number,
|
|
1528
|
-
title: story.title,
|
|
1529
|
-
type: 'epic-based',
|
|
1530
|
-
};
|
|
1531
|
-
this.logger.debug({
|
|
1532
|
-
metadata,
|
|
2233
|
+
prefix,
|
|
2234
|
+
references: config.references,
|
|
2235
|
+
});
|
|
2236
|
+
const prompt = mcpPrefix ? `${mcpPrefix}\n\n${basePrompt}` : basePrompt;
|
|
2237
|
+
// Log prompt if verbose
|
|
2238
|
+
if (config.verbose) {
|
|
2239
|
+
this.logger.info({
|
|
2240
|
+
outputPath: storyFilePath,
|
|
2241
|
+
prompt,
|
|
1533
2242
|
storyNumber: story.fullNumber,
|
|
1534
|
-
|
|
1535
|
-
|
|
2243
|
+
storyTitle: story.title,
|
|
2244
|
+
}, 'Claude Prompt (Story)');
|
|
1536
2245
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
2246
|
+
// Step 4: Run Claude agent to populate content sections
|
|
2247
|
+
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
2248
|
+
agentType: 'sm',
|
|
2249
|
+
references: config.references,
|
|
2250
|
+
timeout: config.timeout ?? 2_700_000,
|
|
2251
|
+
}, {
|
|
2252
|
+
backoffMs: config.retryBackoffMs,
|
|
2253
|
+
logger: this.logger,
|
|
2254
|
+
maxRetries: config.maxRetries,
|
|
2255
|
+
});
|
|
2256
|
+
// Log output if verbose
|
|
2257
|
+
if (config.verbose) {
|
|
2258
|
+
this.logger.info({
|
|
2259
|
+
duration: result.duration,
|
|
2260
|
+
errors: result.errors,
|
|
2261
|
+
output: result.output,
|
|
2262
|
+
outputLength: result.output.length,
|
|
1540
2263
|
storyNumber: story.fullNumber,
|
|
1541
|
-
|
|
2264
|
+
success: result.success,
|
|
2265
|
+
}, 'Claude Response (Story)');
|
|
2266
|
+
}
|
|
2267
|
+
if (!result.success) {
|
|
2268
|
+
throw new Error(result.errors);
|
|
1542
2269
|
}
|
|
2270
|
+
// Step 5: Verify file was updated by Claude
|
|
2271
|
+
const updatedContent = await this.fileManager.readFile(storyFilePath);
|
|
2272
|
+
if (updatedContent === scaffoldedContent) {
|
|
2273
|
+
throw new Error(`Claude did not update the story file at ${storyFilePath}`);
|
|
2274
|
+
}
|
|
2275
|
+
// Invoke callback after successful story creation
|
|
2276
|
+
if (onStoryComplete) {
|
|
2277
|
+
try {
|
|
2278
|
+
const metadata = {
|
|
2279
|
+
epicNumber: story.epicNumber,
|
|
2280
|
+
filePath: storyFilePath,
|
|
2281
|
+
id: story.fullNumber,
|
|
2282
|
+
number: story.fullNumber,
|
|
2283
|
+
status: 'Draft',
|
|
2284
|
+
storyNumber: story.number,
|
|
2285
|
+
title: story.title,
|
|
2286
|
+
type: 'epic-based',
|
|
2287
|
+
};
|
|
2288
|
+
this.logger.debug({
|
|
2289
|
+
metadata,
|
|
2290
|
+
storyNumber: story.fullNumber,
|
|
2291
|
+
}, 'Invoking story completion callback');
|
|
2292
|
+
await onStoryComplete(metadata);
|
|
2293
|
+
}
|
|
2294
|
+
catch (error) {
|
|
2295
|
+
this.logger.error({
|
|
2296
|
+
error: error.message,
|
|
2297
|
+
storyNumber: story.fullNumber,
|
|
2298
|
+
}, 'Story completion callback failed, continuing story creation');
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
// Fire onSpawnComplete for success
|
|
2302
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2303
|
+
agentType: 'sm',
|
|
2304
|
+
duration: Date.now() - spawnStartTime,
|
|
2305
|
+
endTime: Date.now(),
|
|
2306
|
+
itemId: `story-${story.fullNumber}`,
|
|
2307
|
+
itemTitle: story.title,
|
|
2308
|
+
phaseName: 'story',
|
|
2309
|
+
spawnId,
|
|
2310
|
+
startTime: spawnStartTime,
|
|
2311
|
+
success: true,
|
|
2312
|
+
});
|
|
2313
|
+
return storyFilePath;
|
|
2314
|
+
}
|
|
2315
|
+
catch (spawnError) {
|
|
2316
|
+
// Fire onSpawnComplete for failure
|
|
2317
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2318
|
+
agentType: 'sm',
|
|
2319
|
+
duration: Date.now() - spawnStartTime,
|
|
2320
|
+
endTime: Date.now(),
|
|
2321
|
+
error: spawnError.message,
|
|
2322
|
+
itemId: `story-${story.fullNumber}`,
|
|
2323
|
+
itemTitle: story.title,
|
|
2324
|
+
phaseName: 'story',
|
|
2325
|
+
spawnId,
|
|
2326
|
+
startTime: spawnStartTime,
|
|
2327
|
+
success: false,
|
|
2328
|
+
});
|
|
2329
|
+
throw spawnError;
|
|
1543
2330
|
}
|
|
1544
|
-
return storyFilePath;
|
|
1545
2331
|
}, (info) => {
|
|
1546
2332
|
this.logger.info({
|
|
1547
2333
|
completedItems: info.completedItems,
|
|
@@ -1565,11 +2351,22 @@ Write output to: ${outputPath}`;
|
|
|
1565
2351
|
// Add existing stories to success count
|
|
1566
2352
|
successCount += existingStories.length;
|
|
1567
2353
|
const duration = Date.now() - startTime;
|
|
2354
|
+
const endTime = Date.now();
|
|
1568
2355
|
this.logger.info({
|
|
1569
2356
|
duration,
|
|
1570
2357
|
failures: failures.length,
|
|
1571
2358
|
success: successCount,
|
|
1572
2359
|
}, 'Story creation phase completed');
|
|
2360
|
+
// Invoke onPhaseComplete callback
|
|
2361
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2362
|
+
duration,
|
|
2363
|
+
endTime,
|
|
2364
|
+
failureCount: failures.length,
|
|
2365
|
+
itemCount,
|
|
2366
|
+
phaseName: 'story',
|
|
2367
|
+
startTime,
|
|
2368
|
+
successCount,
|
|
2369
|
+
});
|
|
1573
2370
|
return {
|
|
1574
2371
|
duration,
|
|
1575
2372
|
failures,
|
|
@@ -1579,9 +2376,28 @@ Write output to: ${outputPath}`;
|
|
|
1579
2376
|
};
|
|
1580
2377
|
}
|
|
1581
2378
|
catch (error) {
|
|
2379
|
+
const duration = Date.now() - startTime;
|
|
2380
|
+
const endTime = Date.now();
|
|
1582
2381
|
this.logger.error({ error: error.message }, 'Story phase failed');
|
|
2382
|
+
// Invoke onError callback
|
|
2383
|
+
this.invokeErrorCallback(error, {
|
|
2384
|
+
itemId: 'story-phase',
|
|
2385
|
+
phaseName: 'story',
|
|
2386
|
+
recoverable: false,
|
|
2387
|
+
});
|
|
2388
|
+
// Invoke onPhaseComplete callback even on error
|
|
2389
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2390
|
+
duration,
|
|
2391
|
+
endTime,
|
|
2392
|
+
failureCount: failures.length + 1,
|
|
2393
|
+
itemCount,
|
|
2394
|
+
metadata: { error: error.message },
|
|
2395
|
+
phaseName: 'story',
|
|
2396
|
+
startTime,
|
|
2397
|
+
successCount,
|
|
2398
|
+
});
|
|
1583
2399
|
return {
|
|
1584
|
-
duration
|
|
2400
|
+
duration,
|
|
1585
2401
|
failures: [
|
|
1586
2402
|
...failures,
|
|
1587
2403
|
{
|
|
@@ -1613,7 +2429,7 @@ Write output to: ${outputPath}`;
|
|
|
1613
2429
|
// In pipeline mode, stories must be created sequentially (one at a time)
|
|
1614
2430
|
// to ensure they are queued in the correct order for development
|
|
1615
2431
|
const isPipelineMode = Boolean(onStoryComplete);
|
|
1616
|
-
const effectiveParallel =
|
|
2432
|
+
const effectiveParallel = config.parallel;
|
|
1617
2433
|
this.logger.info({
|
|
1618
2434
|
epicFilePath,
|
|
1619
2435
|
mode: isPipelineMode ? 'sequential (pipeline)' : 'parallel',
|
|
@@ -1652,8 +2468,18 @@ Write output to: ${outputPath}`;
|
|
|
1652
2468
|
if (onStoryComplete) {
|
|
1653
2469
|
await this.enqueueExistingStoriesSequentially(allStories, storyDir, prefix, onStoryComplete);
|
|
1654
2470
|
}
|
|
2471
|
+
const duration = Date.now() - startTime;
|
|
2472
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2473
|
+
duration,
|
|
2474
|
+
endTime: Date.now(),
|
|
2475
|
+
failureCount: 0,
|
|
2476
|
+
itemCount: allStories.length,
|
|
2477
|
+
phaseName: 'story',
|
|
2478
|
+
startTime,
|
|
2479
|
+
successCount: allStories.length,
|
|
2480
|
+
});
|
|
1655
2481
|
return {
|
|
1656
|
-
duration
|
|
2482
|
+
duration,
|
|
1657
2483
|
failures: [],
|
|
1658
2484
|
phaseName: 'story',
|
|
1659
2485
|
skipped: false,
|
|
@@ -1673,106 +2499,160 @@ Write output to: ${outputPath}`;
|
|
|
1673
2499
|
: this.batchProcessor;
|
|
1674
2500
|
// Create story files using BatchProcessor and ClaudeAgentRunner
|
|
1675
2501
|
const results = await storyBatchProcessor.processBatch(storiesToCreate, async (story) => {
|
|
1676
|
-
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2502
|
+
const spawnStartTime = Date.now();
|
|
2503
|
+
const spawnId = `story-${story.fullNumber}`;
|
|
2504
|
+
// Fire onSpawnStart so the reporter shows which story is being created
|
|
2505
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
2506
|
+
agentType: 'sm',
|
|
2507
|
+
itemId: `story-${story.fullNumber}`,
|
|
2508
|
+
itemTitle: story.title,
|
|
2509
|
+
phaseName: 'story',
|
|
2510
|
+
spawnId,
|
|
2511
|
+
startTime: spawnStartTime,
|
|
2512
|
+
});
|
|
2513
|
+
try {
|
|
2514
|
+
// Generate story file path with prefix
|
|
2515
|
+
const storyFileName = this.generateStoryFileName(prefix, story.fullNumber);
|
|
2516
|
+
const storyFilePath = `${storyDir}/${storyFileName}`;
|
|
2517
|
+
// Check if file already exists (might have been created by parallel process)
|
|
2518
|
+
const fileExists = await this.fileManager.fileExists(storyFilePath);
|
|
2519
|
+
if (fileExists) {
|
|
2520
|
+
this.logger.info({
|
|
2521
|
+
filePath: storyFilePath,
|
|
2522
|
+
storyNumber: story.fullNumber,
|
|
2523
|
+
storyTitle: story.title,
|
|
2524
|
+
}, 'Story file already exists, skipping creation');
|
|
2525
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2526
|
+
agentType: 'sm',
|
|
2527
|
+
duration: Date.now() - spawnStartTime,
|
|
2528
|
+
endTime: Date.now(),
|
|
2529
|
+
itemId: `story-${story.fullNumber}`,
|
|
2530
|
+
itemTitle: story.title,
|
|
2531
|
+
phaseName: 'story',
|
|
2532
|
+
spawnId,
|
|
2533
|
+
startTime: spawnStartTime,
|
|
2534
|
+
success: true,
|
|
2535
|
+
});
|
|
2536
|
+
return storyFilePath;
|
|
2537
|
+
}
|
|
2538
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata
|
|
2539
|
+
const scaffoldedContent = this.fileScaffolder.scaffoldStory({
|
|
2540
|
+
epicNumber: story.epicNumber,
|
|
2541
|
+
storyNumber: story.number,
|
|
2542
|
+
storyTitle: story.title,
|
|
2543
|
+
});
|
|
2544
|
+
await this.fileManager.writeFile(storyFilePath, scaffoldedContent);
|
|
1682
2545
|
this.logger.info({
|
|
1683
2546
|
filePath: storyFilePath,
|
|
1684
2547
|
storyNumber: story.fullNumber,
|
|
1685
2548
|
storyTitle: story.title,
|
|
1686
|
-
}, 'Story file
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
storyNumber: story.number,
|
|
1693
|
-
storyTitle: story.title,
|
|
1694
|
-
});
|
|
1695
|
-
await this.fileManager.writeFile(storyFilePath, scaffoldedContent);
|
|
1696
|
-
this.logger.info({
|
|
1697
|
-
filePath: storyFilePath,
|
|
1698
|
-
storyNumber: story.fullNumber,
|
|
1699
|
-
storyTitle: story.title,
|
|
1700
|
-
}, 'Story scaffolded file created');
|
|
1701
|
-
// Step 2: Use the epic file path that was passed to this method
|
|
1702
|
-
// Step 3: Build Claude prompt to populate the scaffolded file
|
|
1703
|
-
const prompt = this.buildStoryPrompt(story, {
|
|
1704
|
-
cwd: config.cwd,
|
|
1705
|
-
epicPath: epicFilePath,
|
|
1706
|
-
outputPath: storyFilePath,
|
|
1707
|
-
prefix,
|
|
1708
|
-
references: config.references,
|
|
1709
|
-
});
|
|
1710
|
-
// Log prompt if verbose
|
|
1711
|
-
if (config.verbose) {
|
|
1712
|
-
this.logger.info({
|
|
2549
|
+
}, 'Story scaffolded file created');
|
|
2550
|
+
// Step 2: Use the epic file path that was passed to this method
|
|
2551
|
+
// Step 3: Build Claude prompt to populate the scaffolded file
|
|
2552
|
+
const mcpPrefix = await this.getMcpPromptPrefix('story', 'sm', config);
|
|
2553
|
+
const basePrompt = this.buildStoryPrompt(story, {
|
|
2554
|
+
cwd: config.cwd,
|
|
1713
2555
|
epicPath: epicFilePath,
|
|
1714
2556
|
outputPath: storyFilePath,
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
}, {
|
|
1726
|
-
backoffMs: config.retryBackoffMs,
|
|
1727
|
-
logger: this.logger,
|
|
1728
|
-
maxRetries: config.maxRetries,
|
|
1729
|
-
});
|
|
1730
|
-
// Log output if verbose
|
|
1731
|
-
if (config.verbose) {
|
|
1732
|
-
this.logger.info({
|
|
1733
|
-
duration: result.duration,
|
|
1734
|
-
errors: result.errors,
|
|
1735
|
-
output: result.output,
|
|
1736
|
-
outputLength: result.output.length,
|
|
1737
|
-
storyNumber: story.fullNumber,
|
|
1738
|
-
success: result.success,
|
|
1739
|
-
}, 'Claude Response (Story)');
|
|
1740
|
-
}
|
|
1741
|
-
if (!result.success) {
|
|
1742
|
-
throw new Error(result.errors);
|
|
1743
|
-
}
|
|
1744
|
-
// Step 5: Verify file was updated by Claude
|
|
1745
|
-
const updatedContent = await this.fileManager.readFile(storyFilePath);
|
|
1746
|
-
if (updatedContent === scaffoldedContent) {
|
|
1747
|
-
throw new Error(`Claude did not update the story file at ${storyFilePath}`);
|
|
1748
|
-
}
|
|
1749
|
-
// Invoke callback after successful story creation
|
|
1750
|
-
if (onStoryComplete) {
|
|
1751
|
-
try {
|
|
1752
|
-
const metadata = {
|
|
1753
|
-
epicNumber: story.epicNumber,
|
|
1754
|
-
filePath: storyFilePath,
|
|
1755
|
-
id: story.fullNumber,
|
|
1756
|
-
number: story.fullNumber,
|
|
1757
|
-
status: 'Draft',
|
|
1758
|
-
storyNumber: story.number,
|
|
1759
|
-
title: story.title,
|
|
1760
|
-
type: 'epic-based',
|
|
1761
|
-
};
|
|
1762
|
-
this.logger.debug({
|
|
1763
|
-
metadata,
|
|
2557
|
+
prefix,
|
|
2558
|
+
references: config.references,
|
|
2559
|
+
});
|
|
2560
|
+
const prompt = mcpPrefix ? `${mcpPrefix}\n\n${basePrompt}` : basePrompt;
|
|
2561
|
+
// Log prompt if verbose
|
|
2562
|
+
if (config.verbose) {
|
|
2563
|
+
this.logger.info({
|
|
2564
|
+
epicPath: epicFilePath,
|
|
2565
|
+
outputPath: storyFilePath,
|
|
2566
|
+
prompt,
|
|
1764
2567
|
storyNumber: story.fullNumber,
|
|
1765
|
-
|
|
1766
|
-
|
|
2568
|
+
storyTitle: story.title,
|
|
2569
|
+
}, 'Claude Prompt (Story)');
|
|
1767
2570
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2571
|
+
// Step 4: Run Claude agent to populate content sections
|
|
2572
|
+
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
2573
|
+
agentType: 'sm',
|
|
2574
|
+
references: config.references,
|
|
2575
|
+
timeout: config.timeout ?? 2_700_000,
|
|
2576
|
+
}, {
|
|
2577
|
+
backoffMs: config.retryBackoffMs,
|
|
2578
|
+
logger: this.logger,
|
|
2579
|
+
maxRetries: config.maxRetries,
|
|
2580
|
+
});
|
|
2581
|
+
// Log output if verbose
|
|
2582
|
+
if (config.verbose) {
|
|
2583
|
+
this.logger.info({
|
|
2584
|
+
duration: result.duration,
|
|
2585
|
+
errors: result.errors,
|
|
2586
|
+
output: result.output,
|
|
2587
|
+
outputLength: result.output.length,
|
|
1771
2588
|
storyNumber: story.fullNumber,
|
|
1772
|
-
|
|
2589
|
+
success: result.success,
|
|
2590
|
+
}, 'Claude Response (Story)');
|
|
2591
|
+
}
|
|
2592
|
+
if (!result.success) {
|
|
2593
|
+
throw new Error(result.errors);
|
|
2594
|
+
}
|
|
2595
|
+
// Step 5: Verify file was updated by Claude
|
|
2596
|
+
const updatedContent = await this.fileManager.readFile(storyFilePath);
|
|
2597
|
+
if (updatedContent === scaffoldedContent) {
|
|
2598
|
+
throw new Error(`Claude did not update the story file at ${storyFilePath}`);
|
|
1773
2599
|
}
|
|
2600
|
+
// Invoke callback after successful story creation
|
|
2601
|
+
if (onStoryComplete) {
|
|
2602
|
+
try {
|
|
2603
|
+
const metadata = {
|
|
2604
|
+
epicNumber: story.epicNumber,
|
|
2605
|
+
filePath: storyFilePath,
|
|
2606
|
+
id: story.fullNumber,
|
|
2607
|
+
number: story.fullNumber,
|
|
2608
|
+
status: 'Draft',
|
|
2609
|
+
storyNumber: story.number,
|
|
2610
|
+
title: story.title,
|
|
2611
|
+
type: 'epic-based',
|
|
2612
|
+
};
|
|
2613
|
+
this.logger.debug({
|
|
2614
|
+
metadata,
|
|
2615
|
+
storyNumber: story.fullNumber,
|
|
2616
|
+
}, 'Invoking story completion callback');
|
|
2617
|
+
await onStoryComplete(metadata);
|
|
2618
|
+
}
|
|
2619
|
+
catch (error) {
|
|
2620
|
+
this.logger.error({
|
|
2621
|
+
error: error.message,
|
|
2622
|
+
storyNumber: story.fullNumber,
|
|
2623
|
+
}, 'Story completion callback failed, continuing story creation');
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
// Fire onSpawnComplete for success
|
|
2627
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2628
|
+
agentType: 'sm',
|
|
2629
|
+
duration: Date.now() - spawnStartTime,
|
|
2630
|
+
endTime: Date.now(),
|
|
2631
|
+
itemId: `story-${story.fullNumber}`,
|
|
2632
|
+
itemTitle: story.title,
|
|
2633
|
+
phaseName: 'story',
|
|
2634
|
+
spawnId,
|
|
2635
|
+
startTime: spawnStartTime,
|
|
2636
|
+
success: true,
|
|
2637
|
+
});
|
|
2638
|
+
return storyFilePath;
|
|
2639
|
+
}
|
|
2640
|
+
catch (spawnError) {
|
|
2641
|
+
// Fire onSpawnComplete for failure
|
|
2642
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2643
|
+
agentType: 'sm',
|
|
2644
|
+
duration: Date.now() - spawnStartTime,
|
|
2645
|
+
endTime: Date.now(),
|
|
2646
|
+
error: spawnError.message,
|
|
2647
|
+
itemId: `story-${story.fullNumber}`,
|
|
2648
|
+
itemTitle: story.title,
|
|
2649
|
+
phaseName: 'story',
|
|
2650
|
+
spawnId,
|
|
2651
|
+
startTime: spawnStartTime,
|
|
2652
|
+
success: false,
|
|
2653
|
+
});
|
|
2654
|
+
throw spawnError;
|
|
1774
2655
|
}
|
|
1775
|
-
return storyFilePath;
|
|
1776
2656
|
}, (info) => {
|
|
1777
2657
|
this.logger.info({
|
|
1778
2658
|
completedItems: info.completedItems,
|