@hyperdrive.bot/bmad-workflow 1.0.16 → 1.0.18
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/stories/qa.js +29 -73
- package/dist/commands/workflow.d.ts +81 -0
- package/dist/commands/workflow.js +386 -13
- 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/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +6 -1
- package/dist/services/orchestration/workflow-orchestrator.d.ts +33 -1
- package/dist/services/orchestration/workflow-orchestrator.js +869 -275
- package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- package/dist/utils/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/package.json +3 -2
|
@@ -60,6 +60,7 @@ import { StoryQueue } from './story-queue.js';
|
|
|
60
60
|
export class WorkflowOrchestrator {
|
|
61
61
|
agentRunner;
|
|
62
62
|
batchProcessor;
|
|
63
|
+
callbacks;
|
|
63
64
|
epicParser;
|
|
64
65
|
fileManager;
|
|
65
66
|
fileScaffolder;
|
|
@@ -86,6 +87,7 @@ export class WorkflowOrchestrator {
|
|
|
86
87
|
this.storyTypeDetector = config.storyTypeDetector;
|
|
87
88
|
this.logger = config.logger;
|
|
88
89
|
this.workflowLogger = config.workflowLogger;
|
|
90
|
+
this.callbacks = config.callbacks;
|
|
89
91
|
this.fileScaffolder = new FileScaffolder(config.logger);
|
|
90
92
|
this.prdFixer = new PrdFixer(config.agentRunner, config.fileManager, config.logger);
|
|
91
93
|
this.logger.debug('WorkflowOrchestrator initialized');
|
|
@@ -222,6 +224,83 @@ Write output to: ${outputPath}`;
|
|
|
222
224
|
totalFilesProcessed,
|
|
223
225
|
};
|
|
224
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Invoke a callback safely, catching and logging any errors
|
|
229
|
+
*
|
|
230
|
+
* All callback invocations are wrapped in try-catch to prevent
|
|
231
|
+
* callback errors from interrupting workflow execution.
|
|
232
|
+
*
|
|
233
|
+
* @param callbackName - Name of the callback for logging
|
|
234
|
+
* @param callback - The callback function to invoke
|
|
235
|
+
* @param context - The context to pass to the callback
|
|
236
|
+
* @private
|
|
237
|
+
*/
|
|
238
|
+
invokeCallback(callbackName, callback, context) {
|
|
239
|
+
if (!callback)
|
|
240
|
+
return;
|
|
241
|
+
try {
|
|
242
|
+
callback(context);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
this.logger.warn({
|
|
246
|
+
callbackName,
|
|
247
|
+
error: error.message,
|
|
248
|
+
}, 'Callback error (ignored to prevent workflow interruption)');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Invoke the onSpawnOutput callback safely
|
|
253
|
+
*
|
|
254
|
+
* @param callback - The callback function to invoke
|
|
255
|
+
* @param context - The spawn context
|
|
256
|
+
* @param output - The output text
|
|
257
|
+
* @private
|
|
258
|
+
*/
|
|
259
|
+
invokeSpawnOutputCallback(callback, context, output) {
|
|
260
|
+
if (!callback)
|
|
261
|
+
return;
|
|
262
|
+
try {
|
|
263
|
+
callback(context, output);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
this.logger.warn({
|
|
267
|
+
callbackName: 'onSpawnOutput',
|
|
268
|
+
error: error.message,
|
|
269
|
+
}, 'Callback error (ignored to prevent workflow interruption)');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Invoke the onError callback safely
|
|
274
|
+
*
|
|
275
|
+
* @param error - The error that occurred
|
|
276
|
+
* @param options - Additional context for the error
|
|
277
|
+
* @private
|
|
278
|
+
*/
|
|
279
|
+
invokeErrorCallback(error, options = {}) {
|
|
280
|
+
const callback = this.callbacks?.onError;
|
|
281
|
+
if (!callback)
|
|
282
|
+
return;
|
|
283
|
+
const context = {
|
|
284
|
+
error,
|
|
285
|
+
itemId: options.itemId,
|
|
286
|
+
message: error.message,
|
|
287
|
+
metadata: options.metadata,
|
|
288
|
+
phaseName: options.phaseName,
|
|
289
|
+
recoverable: options.recoverable ?? true,
|
|
290
|
+
spawnId: options.spawnId,
|
|
291
|
+
stack: error.stack,
|
|
292
|
+
timestamp: Date.now(),
|
|
293
|
+
};
|
|
294
|
+
try {
|
|
295
|
+
callback(context);
|
|
296
|
+
}
|
|
297
|
+
catch (callbackError) {
|
|
298
|
+
this.logger.warn({
|
|
299
|
+
callbackName: 'onError',
|
|
300
|
+
error: callbackError.message,
|
|
301
|
+
}, 'Callback error (ignored to prevent workflow interruption)');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
225
304
|
/**
|
|
226
305
|
* Check which files already exist in a directory
|
|
227
306
|
*
|
|
@@ -297,7 +376,7 @@ Write output to: ${outputPath}`;
|
|
|
297
376
|
const content = await this.fileManager.readFile(filePath);
|
|
298
377
|
const isScaffolded = content.includes('_[AI Agent will populate');
|
|
299
378
|
if (isScaffolded) {
|
|
300
|
-
this.logger.
|
|
379
|
+
this.logger.info({
|
|
301
380
|
fileName,
|
|
302
381
|
filePath,
|
|
303
382
|
}, 'Epic file exists but is only scaffolded (will be re-populated)');
|
|
@@ -418,6 +497,7 @@ Write output to: ${outputPath}`;
|
|
|
418
497
|
const workerLogger = this.logger.child({ workerId });
|
|
419
498
|
let successCount = 0;
|
|
420
499
|
const failures = [];
|
|
500
|
+
let devPhaseStarted = false;
|
|
421
501
|
workerLogger.info('Worker started');
|
|
422
502
|
try {
|
|
423
503
|
const storyDir = await this.pathResolver.getStoryDir();
|
|
@@ -430,6 +510,16 @@ Write output to: ${outputPath}`;
|
|
|
430
510
|
workerLogger.info('Queue closed and empty, worker terminating');
|
|
431
511
|
break;
|
|
432
512
|
}
|
|
513
|
+
// Fire onPhaseStart on first dequeue (after story phase is done creating)
|
|
514
|
+
if (!devPhaseStarted) {
|
|
515
|
+
devPhaseStarted = true;
|
|
516
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
517
|
+
itemCount: 0,
|
|
518
|
+
metadata: { mode: 'pipeline', workerId },
|
|
519
|
+
phaseName: 'dev',
|
|
520
|
+
startTime: Date.now(),
|
|
521
|
+
});
|
|
522
|
+
}
|
|
433
523
|
workerLogger.info({
|
|
434
524
|
storyNumber: story.fullNumber,
|
|
435
525
|
storyTitle: story.title,
|
|
@@ -500,6 +590,20 @@ Write output to: ${outputPath}`;
|
|
|
500
590
|
prompt += '\n';
|
|
501
591
|
}
|
|
502
592
|
prompt += '*yolo mode*\n';
|
|
593
|
+
// Generate unique spawn ID
|
|
594
|
+
const spawnId = `dev-${story.fullNumber}-${storyStartTime}`;
|
|
595
|
+
// Invoke onSpawnStart callback
|
|
596
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
597
|
+
agentType: 'dev',
|
|
598
|
+
itemId: story.fullNumber,
|
|
599
|
+
itemTitle: story.title,
|
|
600
|
+
outputPath: storyFilePath,
|
|
601
|
+
phaseName: 'dev',
|
|
602
|
+
prompt,
|
|
603
|
+
spawnId,
|
|
604
|
+
startTime: storyStartTime,
|
|
605
|
+
workerId,
|
|
606
|
+
});
|
|
503
607
|
// Execute dev agent with retry on timeout/killed
|
|
504
608
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
505
609
|
agentType: 'dev',
|
|
@@ -510,7 +614,24 @@ Write output to: ${outputPath}`;
|
|
|
510
614
|
logger: this.logger,
|
|
511
615
|
maxRetries: config.maxRetries,
|
|
512
616
|
});
|
|
617
|
+
const spawnEndTime = Date.now();
|
|
618
|
+
const spawnDuration = spawnEndTime - storyStartTime;
|
|
513
619
|
if (result.success) {
|
|
620
|
+
// Invoke onSpawnComplete callback for success
|
|
621
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
622
|
+
agentType: 'dev',
|
|
623
|
+
duration: spawnDuration,
|
|
624
|
+
endTime: spawnEndTime,
|
|
625
|
+
itemId: story.fullNumber,
|
|
626
|
+
itemTitle: story.title,
|
|
627
|
+
output: result.output,
|
|
628
|
+
outputPath: storyFilePath,
|
|
629
|
+
phaseName: 'dev',
|
|
630
|
+
spawnId,
|
|
631
|
+
startTime: storyStartTime,
|
|
632
|
+
success: true,
|
|
633
|
+
workerId,
|
|
634
|
+
});
|
|
514
635
|
// Update story status to Done
|
|
515
636
|
await this.updateStoryStatus(storyFilePath, 'Done');
|
|
516
637
|
// Move to QA folder
|
|
@@ -530,6 +651,28 @@ Write output to: ${outputPath}`;
|
|
|
530
651
|
workerLogger.info({ storyNumber: story.fullNumber }, 'Story development completed successfully');
|
|
531
652
|
}
|
|
532
653
|
else {
|
|
654
|
+
// Invoke onSpawnComplete callback for failure
|
|
655
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
656
|
+
agentType: 'dev',
|
|
657
|
+
duration: spawnDuration,
|
|
658
|
+
endTime: spawnEndTime,
|
|
659
|
+
error: result.errors,
|
|
660
|
+
itemId: story.fullNumber,
|
|
661
|
+
itemTitle: story.title,
|
|
662
|
+
outputPath: storyFilePath,
|
|
663
|
+
phaseName: 'dev',
|
|
664
|
+
spawnId,
|
|
665
|
+
startTime: storyStartTime,
|
|
666
|
+
success: false,
|
|
667
|
+
workerId,
|
|
668
|
+
});
|
|
669
|
+
// Invoke onError callback
|
|
670
|
+
this.invokeErrorCallback(new Error(result.errors), {
|
|
671
|
+
itemId: story.fullNumber,
|
|
672
|
+
phaseName: 'dev',
|
|
673
|
+
recoverable: true,
|
|
674
|
+
spawnId,
|
|
675
|
+
});
|
|
533
676
|
// Log story failed in pipeline mode
|
|
534
677
|
const storyDuration = Date.now() - storyStartTime;
|
|
535
678
|
if (this.workflowLogger) {
|
|
@@ -552,6 +695,12 @@ Write output to: ${outputPath}`;
|
|
|
552
695
|
}
|
|
553
696
|
}
|
|
554
697
|
catch (error) {
|
|
698
|
+
// Invoke onError callback for unexpected errors
|
|
699
|
+
this.invokeErrorCallback(error, {
|
|
700
|
+
itemId: story.fullNumber,
|
|
701
|
+
phaseName: 'dev',
|
|
702
|
+
recoverable: true,
|
|
703
|
+
});
|
|
555
704
|
// Catch errors for individual stories to prevent worker crash
|
|
556
705
|
workerLogger.error({
|
|
557
706
|
error: error.message,
|
|
@@ -636,10 +785,18 @@ Write output to: ${outputPath}`;
|
|
|
636
785
|
const startTime = Date.now();
|
|
637
786
|
const failures = [];
|
|
638
787
|
let successCount = 0;
|
|
788
|
+
const itemCount = stories.length;
|
|
639
789
|
this.logger.info({
|
|
640
790
|
interval: config.storyInterval,
|
|
641
791
|
storyCount: stories.length,
|
|
642
792
|
}, 'Starting development phase');
|
|
793
|
+
// Invoke onPhaseStart callback
|
|
794
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
795
|
+
itemCount,
|
|
796
|
+
metadata: { interval: config.storyInterval },
|
|
797
|
+
phaseName: 'dev',
|
|
798
|
+
startTime,
|
|
799
|
+
});
|
|
643
800
|
try {
|
|
644
801
|
const storyDir = await this.pathResolver.getStoryDir();
|
|
645
802
|
const qaStoryDir = await this.pathResolver.getQaStoryDir();
|
|
@@ -701,6 +858,20 @@ Write output to: ${outputPath}`;
|
|
|
701
858
|
prompt += '\n';
|
|
702
859
|
}
|
|
703
860
|
prompt += '*yolo mode*\n';
|
|
861
|
+
// Generate unique spawn ID and timestamp
|
|
862
|
+
const spawnStartTime = Date.now();
|
|
863
|
+
const spawnId = `dev-${story.fullNumber}-${spawnStartTime}`;
|
|
864
|
+
// Invoke onSpawnStart callback
|
|
865
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
866
|
+
agentType: 'dev',
|
|
867
|
+
itemId: story.fullNumber,
|
|
868
|
+
itemTitle: story.title,
|
|
869
|
+
outputPath: storyFilePath,
|
|
870
|
+
phaseName: 'dev',
|
|
871
|
+
prompt,
|
|
872
|
+
spawnId,
|
|
873
|
+
startTime: spawnStartTime,
|
|
874
|
+
});
|
|
704
875
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
705
876
|
agentType: 'dev',
|
|
706
877
|
references: config.references,
|
|
@@ -710,7 +881,23 @@ Write output to: ${outputPath}`;
|
|
|
710
881
|
logger: this.logger,
|
|
711
882
|
maxRetries: config.maxRetries,
|
|
712
883
|
});
|
|
884
|
+
const spawnEndTime = Date.now();
|
|
885
|
+
const spawnDuration = spawnEndTime - spawnStartTime;
|
|
713
886
|
if (result.success) {
|
|
887
|
+
// Invoke onSpawnComplete callback for success
|
|
888
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
889
|
+
agentType: 'dev',
|
|
890
|
+
duration: spawnDuration,
|
|
891
|
+
endTime: spawnEndTime,
|
|
892
|
+
itemId: story.fullNumber,
|
|
893
|
+
itemTitle: story.title,
|
|
894
|
+
output: result.output,
|
|
895
|
+
outputPath: storyFilePath,
|
|
896
|
+
phaseName: 'dev',
|
|
897
|
+
spawnId,
|
|
898
|
+
startTime: spawnStartTime,
|
|
899
|
+
success: true,
|
|
900
|
+
});
|
|
714
901
|
// Update story status to Done
|
|
715
902
|
await this.updateStoryStatus(storyFilePath, 'Done');
|
|
716
903
|
// Move to QA folder (qaFilePath already defined above with correct filename)
|
|
@@ -719,6 +906,27 @@ Write output to: ${outputPath}`;
|
|
|
719
906
|
this.logger.info({ storyNumber: story.fullNumber }, 'Story development completed successfully');
|
|
720
907
|
}
|
|
721
908
|
else {
|
|
909
|
+
// Invoke onSpawnComplete callback for failure
|
|
910
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
911
|
+
agentType: 'dev',
|
|
912
|
+
duration: spawnDuration,
|
|
913
|
+
endTime: spawnEndTime,
|
|
914
|
+
error: result.errors,
|
|
915
|
+
itemId: story.fullNumber,
|
|
916
|
+
itemTitle: story.title,
|
|
917
|
+
outputPath: storyFilePath,
|
|
918
|
+
phaseName: 'dev',
|
|
919
|
+
spawnId,
|
|
920
|
+
startTime: spawnStartTime,
|
|
921
|
+
success: false,
|
|
922
|
+
});
|
|
923
|
+
// Invoke onError callback for spawn failure
|
|
924
|
+
this.invokeErrorCallback(new Error(result.errors), {
|
|
925
|
+
itemId: story.fullNumber,
|
|
926
|
+
phaseName: 'dev',
|
|
927
|
+
recoverable: true,
|
|
928
|
+
spawnId,
|
|
929
|
+
});
|
|
722
930
|
this.logger.error({
|
|
723
931
|
error: result.errors,
|
|
724
932
|
storyNumber: story.fullNumber,
|
|
@@ -735,11 +943,22 @@ Write output to: ${outputPath}`;
|
|
|
735
943
|
}
|
|
736
944
|
}
|
|
737
945
|
const duration = Date.now() - startTime;
|
|
946
|
+
const endTime = Date.now();
|
|
738
947
|
this.logger.info({
|
|
739
948
|
duration,
|
|
740
949
|
failures: failures.length,
|
|
741
950
|
success: successCount,
|
|
742
951
|
}, 'Development phase completed');
|
|
952
|
+
// Invoke onPhaseComplete callback
|
|
953
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
954
|
+
duration,
|
|
955
|
+
endTime,
|
|
956
|
+
failureCount: failures.length,
|
|
957
|
+
itemCount,
|
|
958
|
+
phaseName: 'dev',
|
|
959
|
+
startTime,
|
|
960
|
+
successCount,
|
|
961
|
+
});
|
|
743
962
|
return {
|
|
744
963
|
duration,
|
|
745
964
|
failures,
|
|
@@ -749,9 +968,28 @@ Write output to: ${outputPath}`;
|
|
|
749
968
|
};
|
|
750
969
|
}
|
|
751
970
|
catch (error) {
|
|
971
|
+
const duration = Date.now() - startTime;
|
|
972
|
+
const endTime = Date.now();
|
|
752
973
|
this.logger.error({ error: error.message }, 'Development phase failed');
|
|
974
|
+
// Invoke onError callback
|
|
975
|
+
this.invokeErrorCallback(error, {
|
|
976
|
+
itemId: 'dev-phase',
|
|
977
|
+
phaseName: 'dev',
|
|
978
|
+
recoverable: false,
|
|
979
|
+
});
|
|
980
|
+
// Invoke onPhaseComplete callback even on error
|
|
981
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
982
|
+
duration,
|
|
983
|
+
endTime,
|
|
984
|
+
failureCount: failures.length + 1,
|
|
985
|
+
itemCount,
|
|
986
|
+
metadata: { error: error.message },
|
|
987
|
+
phaseName: 'dev',
|
|
988
|
+
startTime,
|
|
989
|
+
successCount,
|
|
990
|
+
});
|
|
753
991
|
return {
|
|
754
|
-
duration
|
|
992
|
+
duration,
|
|
755
993
|
failures: [
|
|
756
994
|
...failures,
|
|
757
995
|
{
|
|
@@ -780,11 +1018,19 @@ Write output to: ${outputPath}`;
|
|
|
780
1018
|
const startTime = Date.now();
|
|
781
1019
|
const failures = [];
|
|
782
1020
|
let successCount = 0;
|
|
1021
|
+
let itemCount = 0;
|
|
783
1022
|
this.logger.info({
|
|
784
1023
|
interval: config.prdInterval,
|
|
785
1024
|
parallel: config.parallel,
|
|
786
1025
|
prdFile: prdFilePath,
|
|
787
1026
|
}, 'Starting epic creation phase');
|
|
1027
|
+
// Invoke onPhaseStart callback (itemCount will be updated once epics are parsed)
|
|
1028
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
1029
|
+
itemCount: 0,
|
|
1030
|
+
metadata: { parallel: config.parallel, prdFilePath },
|
|
1031
|
+
phaseName: 'epic',
|
|
1032
|
+
startTime,
|
|
1033
|
+
});
|
|
788
1034
|
try {
|
|
789
1035
|
// Read PRD file
|
|
790
1036
|
let prdContent = await this.fileManager.readFile(prdFilePath);
|
|
@@ -819,8 +1065,19 @@ Write output to: ${outputPath}`;
|
|
|
819
1065
|
}
|
|
820
1066
|
}
|
|
821
1067
|
this.logger.info({ epicCount: epics.length }, 'Epics extracted from PRD');
|
|
1068
|
+
itemCount = epics.length;
|
|
822
1069
|
if (epics.length === 0) {
|
|
823
1070
|
this.logger.warn('No epics found in PRD file');
|
|
1071
|
+
// Invoke onPhaseComplete for empty phase
|
|
1072
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1073
|
+
duration: Date.now() - startTime,
|
|
1074
|
+
endTime: Date.now(),
|
|
1075
|
+
failureCount: 0,
|
|
1076
|
+
itemCount: 0,
|
|
1077
|
+
phaseName: 'epic',
|
|
1078
|
+
startTime,
|
|
1079
|
+
successCount: 0,
|
|
1080
|
+
});
|
|
824
1081
|
return {
|
|
825
1082
|
duration: Date.now() - startTime,
|
|
826
1083
|
failures: [],
|
|
@@ -837,8 +1094,18 @@ Write output to: ${outputPath}`;
|
|
|
837
1094
|
const epicsToCreate = epics.filter((epic) => !properlyPopulatedEpics.includes(this.generateEpicFileName(prefix, epic.number)));
|
|
838
1095
|
if (epicsToCreate.length === 0) {
|
|
839
1096
|
this.logger.info('All epics already exist and are properly populated, skipping creation');
|
|
1097
|
+
const duration = Date.now() - startTime;
|
|
1098
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1099
|
+
duration,
|
|
1100
|
+
endTime: Date.now(),
|
|
1101
|
+
failureCount: 0,
|
|
1102
|
+
itemCount: epics.length,
|
|
1103
|
+
phaseName: 'epic',
|
|
1104
|
+
startTime,
|
|
1105
|
+
successCount: epics.length,
|
|
1106
|
+
});
|
|
840
1107
|
return {
|
|
841
|
-
duration
|
|
1108
|
+
duration,
|
|
842
1109
|
failures: [],
|
|
843
1110
|
phaseName: 'epic',
|
|
844
1111
|
skipped: false,
|
|
@@ -851,99 +1118,140 @@ Write output to: ${outputPath}`;
|
|
|
851
1118
|
}, 'Creating/re-populating epic files');
|
|
852
1119
|
// Create epic files using BatchProcessor and ClaudeAgentRunner
|
|
853
1120
|
const results = await this.batchProcessor.processBatch(epicsToCreate, async (epic) => {
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1121
|
+
const spawnStartTime = Date.now();
|
|
1122
|
+
const spawnId = `epic-${epic.number}`;
|
|
1123
|
+
// Fire onSpawnStart so the reporter shows which epic is being generated
|
|
1124
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
1125
|
+
agentType: 'architect',
|
|
1126
|
+
itemId: `epic-${epic.number}`,
|
|
1127
|
+
itemTitle: epic.title,
|
|
1128
|
+
phaseName: 'epic',
|
|
1129
|
+
spawnId,
|
|
1130
|
+
startTime: spawnStartTime,
|
|
1131
|
+
});
|
|
1132
|
+
try {
|
|
1133
|
+
// Generate epic file path with prefix
|
|
1134
|
+
const epicFileName = this.generateEpicFileName(prefix, epic.number);
|
|
1135
|
+
const epicFilePath = `${epicDir}/${epicFileName}`;
|
|
1136
|
+
// Check if file already exists and is properly populated
|
|
1137
|
+
const fileExists = await this.fileManager.fileExists(epicFilePath);
|
|
1138
|
+
if (fileExists) {
|
|
1139
|
+
// Check if the file is properly populated (has actual story entries, not placeholder text)
|
|
1140
|
+
const existingContent = await this.fileManager.readFile(epicFilePath);
|
|
1141
|
+
const isScaffolded = existingContent.includes('_[AI Agent will populate');
|
|
1142
|
+
if (!isScaffolded) {
|
|
1143
|
+
// File is properly populated, skip recreation
|
|
1144
|
+
this.logger.info({
|
|
1145
|
+
epicNumber: epic.number,
|
|
1146
|
+
epicTitle: epic.title,
|
|
1147
|
+
filePath: epicFilePath,
|
|
1148
|
+
}, 'Epic file already exists and is properly populated, skipping creation');
|
|
1149
|
+
return epicFilePath;
|
|
1150
|
+
}
|
|
1151
|
+
// File exists but is only scaffolded (incomplete) - will re-populate it
|
|
865
1152
|
this.logger.info({
|
|
866
1153
|
epicNumber: epic.number,
|
|
867
1154
|
epicTitle: epic.title,
|
|
868
1155
|
filePath: epicFilePath,
|
|
869
|
-
}, 'Epic file
|
|
870
|
-
return epicFilePath;
|
|
1156
|
+
}, 'Epic file exists but is only scaffolded (incomplete), re-populating with Claude agent');
|
|
871
1157
|
}
|
|
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({
|
|
1158
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata (if not exists)
|
|
1159
|
+
const scaffoldedContent = this.fileScaffolder.scaffoldEpic({
|
|
911
1160
|
epicNumber: epic.number,
|
|
912
1161
|
epicTitle: epic.title,
|
|
1162
|
+
prefix,
|
|
1163
|
+
});
|
|
1164
|
+
if (fileExists) {
|
|
1165
|
+
this.logger.info({
|
|
1166
|
+
epicNumber: epic.number,
|
|
1167
|
+
epicTitle: epic.title,
|
|
1168
|
+
filePath: epicFilePath,
|
|
1169
|
+
}, 'Using existing scaffolded epic file');
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
await this.fileManager.writeFile(epicFilePath, scaffoldedContent);
|
|
1173
|
+
this.logger.info({
|
|
1174
|
+
epicNumber: epic.number,
|
|
1175
|
+
epicTitle: epic.title,
|
|
1176
|
+
filePath: epicFilePath,
|
|
1177
|
+
}, 'Epic scaffolded file created');
|
|
1178
|
+
}
|
|
1179
|
+
// Step 2: Build Claude prompt to populate the scaffolded file
|
|
1180
|
+
const prompt = this.buildEpicPrompt(epic, {
|
|
1181
|
+
cwd: config.cwd,
|
|
913
1182
|
outputPath: epicFilePath,
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1183
|
+
prdPath: prdFilePath,
|
|
1184
|
+
prefix,
|
|
1185
|
+
references: config.references,
|
|
1186
|
+
});
|
|
1187
|
+
// Log prompt if verbose
|
|
1188
|
+
if (config.verbose) {
|
|
1189
|
+
this.logger.info({
|
|
1190
|
+
epicNumber: epic.number,
|
|
1191
|
+
epicTitle: epic.title,
|
|
1192
|
+
outputPath: epicFilePath,
|
|
1193
|
+
prompt,
|
|
1194
|
+
}, 'Claude Prompt (Epic)');
|
|
1195
|
+
}
|
|
1196
|
+
// Step 3: Run Claude agent to populate content sections
|
|
1197
|
+
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
1198
|
+
agentType: 'architect',
|
|
1199
|
+
references: config.references,
|
|
1200
|
+
timeout: config.timeout ?? 2_700_000,
|
|
1201
|
+
}, {
|
|
1202
|
+
backoffMs: config.retryBackoffMs,
|
|
1203
|
+
logger: this.logger,
|
|
1204
|
+
maxRetries: config.maxRetries,
|
|
1205
|
+
});
|
|
1206
|
+
// Log output if verbose
|
|
1207
|
+
if (config.verbose) {
|
|
1208
|
+
this.logger.info({
|
|
1209
|
+
duration: result.duration,
|
|
1210
|
+
epicNumber: epic.number,
|
|
1211
|
+
errors: result.errors,
|
|
1212
|
+
output: result.output,
|
|
1213
|
+
outputLength: result.output.length,
|
|
1214
|
+
success: result.success,
|
|
1215
|
+
}, 'Claude Response (Epic)');
|
|
1216
|
+
}
|
|
1217
|
+
if (!result.success) {
|
|
1218
|
+
throw new Error(result.errors);
|
|
1219
|
+
}
|
|
1220
|
+
// Step 4: Verify file was updated by Claude
|
|
1221
|
+
const updatedContent = await this.fileManager.readFile(epicFilePath);
|
|
1222
|
+
if (updatedContent === scaffoldedContent) {
|
|
1223
|
+
throw new Error(`Claude did not update the epic file at ${epicFilePath}`);
|
|
1224
|
+
}
|
|
1225
|
+
// Fire onSpawnComplete for success
|
|
1226
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
1227
|
+
agentType: 'architect',
|
|
1228
|
+
duration: Date.now() - spawnStartTime,
|
|
1229
|
+
endTime: Date.now(),
|
|
1230
|
+
itemId: `epic-${epic.number}`,
|
|
1231
|
+
itemTitle: epic.title,
|
|
1232
|
+
phaseName: 'epic',
|
|
1233
|
+
spawnId,
|
|
1234
|
+
startTime: spawnStartTime,
|
|
1235
|
+
success: true,
|
|
1236
|
+
});
|
|
1237
|
+
return epicFilePath;
|
|
940
1238
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1239
|
+
catch (spawnError) {
|
|
1240
|
+
// Fire onSpawnComplete for failure
|
|
1241
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
1242
|
+
agentType: 'architect',
|
|
1243
|
+
duration: Date.now() - spawnStartTime,
|
|
1244
|
+
endTime: Date.now(),
|
|
1245
|
+
error: spawnError.message,
|
|
1246
|
+
itemId: `epic-${epic.number}`,
|
|
1247
|
+
itemTitle: epic.title,
|
|
1248
|
+
phaseName: 'epic',
|
|
1249
|
+
spawnId,
|
|
1250
|
+
startTime: spawnStartTime,
|
|
1251
|
+
success: false,
|
|
1252
|
+
});
|
|
1253
|
+
throw spawnError;
|
|
945
1254
|
}
|
|
946
|
-
return epicFilePath;
|
|
947
1255
|
}, (info) => {
|
|
948
1256
|
this.logger.info({
|
|
949
1257
|
completedItems: info.completedItems,
|
|
@@ -967,11 +1275,23 @@ Write output to: ${outputPath}`;
|
|
|
967
1275
|
// Add properly populated epics to success count
|
|
968
1276
|
successCount += properlyPopulatedEpics.length;
|
|
969
1277
|
const duration = Date.now() - startTime;
|
|
1278
|
+
const endTime = Date.now();
|
|
970
1279
|
this.logger.info({
|
|
971
1280
|
duration,
|
|
972
1281
|
failures: failures.length,
|
|
973
1282
|
success: successCount,
|
|
974
1283
|
}, 'Epic creation phase completed');
|
|
1284
|
+
// Invoke onPhaseComplete callback
|
|
1285
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1286
|
+
duration,
|
|
1287
|
+
endTime,
|
|
1288
|
+
failureCount: failures.length,
|
|
1289
|
+
itemCount,
|
|
1290
|
+
metadata: { parallel: config.parallel, prdFilePath },
|
|
1291
|
+
phaseName: 'epic',
|
|
1292
|
+
startTime,
|
|
1293
|
+
successCount,
|
|
1294
|
+
});
|
|
975
1295
|
return {
|
|
976
1296
|
duration,
|
|
977
1297
|
failures,
|
|
@@ -981,9 +1301,28 @@ Write output to: ${outputPath}`;
|
|
|
981
1301
|
};
|
|
982
1302
|
}
|
|
983
1303
|
catch (error) {
|
|
1304
|
+
const duration = Date.now() - startTime;
|
|
1305
|
+
const endTime = Date.now();
|
|
984
1306
|
this.logger.error({ error: error.message }, 'Epic phase failed');
|
|
1307
|
+
// Invoke onError callback
|
|
1308
|
+
this.invokeErrorCallback(error, {
|
|
1309
|
+
itemId: 'epic-phase',
|
|
1310
|
+
phaseName: 'epic',
|
|
1311
|
+
recoverable: false,
|
|
1312
|
+
});
|
|
1313
|
+
// Invoke onPhaseComplete callback even on error
|
|
1314
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1315
|
+
duration,
|
|
1316
|
+
endTime,
|
|
1317
|
+
failureCount: failures.length + 1,
|
|
1318
|
+
itemCount,
|
|
1319
|
+
metadata: { error: error.message },
|
|
1320
|
+
phaseName: 'epic',
|
|
1321
|
+
startTime,
|
|
1322
|
+
successCount,
|
|
1323
|
+
});
|
|
985
1324
|
return {
|
|
986
|
-
duration
|
|
1325
|
+
duration,
|
|
987
1326
|
failures: [
|
|
988
1327
|
...failures,
|
|
989
1328
|
{
|
|
@@ -1079,6 +1418,15 @@ Write output to: ${outputPath}`;
|
|
|
1079
1418
|
success: totalSuccess,
|
|
1080
1419
|
workerCount,
|
|
1081
1420
|
}, 'Pipelined development phase completed');
|
|
1421
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1422
|
+
duration,
|
|
1423
|
+
endTime: Date.now(),
|
|
1424
|
+
failureCount: allFailures.length,
|
|
1425
|
+
itemCount: totalSuccess + allFailures.length,
|
|
1426
|
+
phaseName: 'dev',
|
|
1427
|
+
startTime,
|
|
1428
|
+
successCount: totalSuccess,
|
|
1429
|
+
});
|
|
1082
1430
|
return {
|
|
1083
1431
|
duration,
|
|
1084
1432
|
failures: allFailures,
|
|
@@ -1089,6 +1437,15 @@ Write output to: ${outputPath}`;
|
|
|
1089
1437
|
}
|
|
1090
1438
|
catch (error) {
|
|
1091
1439
|
this.logger.error({ error: error.message }, 'Pipelined development phase failed');
|
|
1440
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1441
|
+
duration: Date.now() - startTime,
|
|
1442
|
+
endTime: Date.now(),
|
|
1443
|
+
failureCount: 1,
|
|
1444
|
+
itemCount: 1,
|
|
1445
|
+
phaseName: 'dev',
|
|
1446
|
+
startTime,
|
|
1447
|
+
successCount: 0,
|
|
1448
|
+
});
|
|
1092
1449
|
return {
|
|
1093
1450
|
duration: Date.now() - startTime,
|
|
1094
1451
|
failures: [
|
|
@@ -1146,9 +1503,17 @@ Write output to: ${outputPath}`;
|
|
|
1146
1503
|
const startTime = Date.now();
|
|
1147
1504
|
const failures = [];
|
|
1148
1505
|
let successCount = 0;
|
|
1506
|
+
let itemCount = 0;
|
|
1149
1507
|
this.logger.info({
|
|
1150
1508
|
qaRetries: config.qaRetries ?? 2,
|
|
1151
1509
|
}, 'Starting QA phase');
|
|
1510
|
+
// Invoke onPhaseStart callback
|
|
1511
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
1512
|
+
itemCount: 0,
|
|
1513
|
+
metadata: { qaRetries: config.qaRetries ?? 2 },
|
|
1514
|
+
phaseName: 'qa',
|
|
1515
|
+
startTime,
|
|
1516
|
+
});
|
|
1152
1517
|
try {
|
|
1153
1518
|
// Get QA story directory
|
|
1154
1519
|
const qaStoryDir = await this.pathResolver.getQaStoryDir();
|
|
@@ -1158,6 +1523,17 @@ Write output to: ${outputPath}`;
|
|
|
1158
1523
|
const storyFiles = await this.fileManager.listFiles(qaStoryDir, storyPattern);
|
|
1159
1524
|
if (storyFiles.length === 0) {
|
|
1160
1525
|
this.logger.info('No stories found in QA folder, skipping QA phase');
|
|
1526
|
+
// Invoke onPhaseComplete for skipped phase
|
|
1527
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1528
|
+
duration: Date.now() - startTime,
|
|
1529
|
+
endTime: Date.now(),
|
|
1530
|
+
failureCount: 0,
|
|
1531
|
+
itemCount: 0,
|
|
1532
|
+
phaseName: 'qa',
|
|
1533
|
+
skipped: true,
|
|
1534
|
+
startTime,
|
|
1535
|
+
successCount: 0,
|
|
1536
|
+
});
|
|
1161
1537
|
return {
|
|
1162
1538
|
duration: Date.now() - startTime,
|
|
1163
1539
|
failures: [],
|
|
@@ -1166,6 +1542,7 @@ Write output to: ${outputPath}`;
|
|
|
1166
1542
|
success: 0,
|
|
1167
1543
|
};
|
|
1168
1544
|
}
|
|
1545
|
+
itemCount = storyFiles.length;
|
|
1169
1546
|
this.logger.info({ storyCount: storyFiles.length }, 'Found stories for QA phase');
|
|
1170
1547
|
// Dynamically import QA command to avoid circular dependencies
|
|
1171
1548
|
const { default: StoriesQaCommand } = await import('../../commands/stories/qa.js');
|
|
@@ -1190,8 +1567,22 @@ Write output to: ${outputPath}`;
|
|
|
1190
1567
|
qaFlags.push(`--reference=${ref}`);
|
|
1191
1568
|
}
|
|
1192
1569
|
}
|
|
1193
|
-
//
|
|
1194
|
-
|
|
1570
|
+
// Suppress QA command's pino logger in non-verbose mode
|
|
1571
|
+
const prevLogLevel = process.env.LOG_LEVEL;
|
|
1572
|
+
if (!config.verbose) {
|
|
1573
|
+
process.env.LOG_LEVEL = 'silent';
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
await StoriesQaCommand.run([...qaArgs, ...qaFlags]);
|
|
1577
|
+
}
|
|
1578
|
+
finally {
|
|
1579
|
+
if (prevLogLevel === undefined) {
|
|
1580
|
+
delete process.env.LOG_LEVEL;
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
process.env.LOG_LEVEL = prevLogLevel;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1195
1586
|
successCount++;
|
|
1196
1587
|
this.logger.info({ storyPath }, 'QA workflow completed successfully for story');
|
|
1197
1588
|
}
|
|
@@ -1205,11 +1596,22 @@ Write output to: ${outputPath}`;
|
|
|
1205
1596
|
}
|
|
1206
1597
|
}
|
|
1207
1598
|
const duration = Date.now() - startTime;
|
|
1599
|
+
const endTime = Date.now();
|
|
1208
1600
|
this.logger.info({
|
|
1209
1601
|
duration,
|
|
1210
1602
|
failures: failures.length,
|
|
1211
1603
|
success: successCount,
|
|
1212
1604
|
}, 'QA phase completed');
|
|
1605
|
+
// Invoke onPhaseComplete callback
|
|
1606
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1607
|
+
duration,
|
|
1608
|
+
endTime,
|
|
1609
|
+
failureCount: failures.length,
|
|
1610
|
+
itemCount,
|
|
1611
|
+
phaseName: 'qa',
|
|
1612
|
+
startTime,
|
|
1613
|
+
successCount,
|
|
1614
|
+
});
|
|
1213
1615
|
return {
|
|
1214
1616
|
duration,
|
|
1215
1617
|
failures,
|
|
@@ -1219,9 +1621,28 @@ Write output to: ${outputPath}`;
|
|
|
1219
1621
|
};
|
|
1220
1622
|
}
|
|
1221
1623
|
catch (error) {
|
|
1624
|
+
const duration = Date.now() - startTime;
|
|
1625
|
+
const endTime = Date.now();
|
|
1222
1626
|
this.logger.error({ error: error.message }, 'QA phase failed');
|
|
1627
|
+
// Invoke onError callback
|
|
1628
|
+
this.invokeErrorCallback(error, {
|
|
1629
|
+
itemId: 'qa-phase',
|
|
1630
|
+
phaseName: 'qa',
|
|
1631
|
+
recoverable: false,
|
|
1632
|
+
});
|
|
1633
|
+
// Invoke onPhaseComplete callback even on error
|
|
1634
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1635
|
+
duration,
|
|
1636
|
+
endTime,
|
|
1637
|
+
failureCount: failures.length + 1,
|
|
1638
|
+
itemCount,
|
|
1639
|
+
metadata: { error: error.message },
|
|
1640
|
+
phaseName: 'qa',
|
|
1641
|
+
startTime,
|
|
1642
|
+
successCount,
|
|
1643
|
+
});
|
|
1223
1644
|
return {
|
|
1224
|
-
duration
|
|
1645
|
+
duration,
|
|
1225
1646
|
failures: [
|
|
1226
1647
|
...failures,
|
|
1227
1648
|
{
|
|
@@ -1355,16 +1776,24 @@ Write output to: ${outputPath}`;
|
|
|
1355
1776
|
const startTime = Date.now();
|
|
1356
1777
|
const failures = [];
|
|
1357
1778
|
let successCount = 0;
|
|
1779
|
+
let itemCount = 0;
|
|
1358
1780
|
// In pipeline mode, stories must be created sequentially (one at a time)
|
|
1359
1781
|
// to ensure they are queued in the correct order for development
|
|
1360
1782
|
const isPipelineMode = Boolean(onStoryComplete);
|
|
1361
|
-
const effectiveParallel =
|
|
1783
|
+
const effectiveParallel = config.parallel;
|
|
1362
1784
|
this.logger.info({
|
|
1363
1785
|
epicCount: epics.length,
|
|
1364
1786
|
interval: config.epicInterval,
|
|
1365
1787
|
mode: isPipelineMode ? 'sequential (pipeline)' : 'parallel',
|
|
1366
1788
|
parallel: effectiveParallel,
|
|
1367
1789
|
}, 'Starting story creation phase');
|
|
1790
|
+
// Invoke onPhaseStart callback
|
|
1791
|
+
this.invokeCallback('onPhaseStart', this.callbacks?.onPhaseStart, {
|
|
1792
|
+
itemCount: 0,
|
|
1793
|
+
metadata: { epicCount: epics.length, parallel: effectiveParallel, pipelineMode: isPipelineMode },
|
|
1794
|
+
phaseName: 'story',
|
|
1795
|
+
startTime,
|
|
1796
|
+
});
|
|
1368
1797
|
try {
|
|
1369
1798
|
const allStories = [];
|
|
1370
1799
|
// Get epic directory
|
|
@@ -1397,8 +1826,19 @@ Write output to: ${outputPath}`;
|
|
|
1397
1826
|
// Flatten the array of story arrays
|
|
1398
1827
|
allStories.push(...epicStories.flat());
|
|
1399
1828
|
this.logger.info({ storyCount: allStories.length }, 'Stories extracted from epics');
|
|
1829
|
+
itemCount = allStories.length;
|
|
1400
1830
|
if (allStories.length === 0) {
|
|
1401
1831
|
this.logger.warn('No stories found in epic files');
|
|
1832
|
+
// Invoke onPhaseComplete for empty phase
|
|
1833
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1834
|
+
duration: Date.now() - startTime,
|
|
1835
|
+
endTime: Date.now(),
|
|
1836
|
+
failureCount: 0,
|
|
1837
|
+
itemCount: 0,
|
|
1838
|
+
phaseName: 'story',
|
|
1839
|
+
startTime,
|
|
1840
|
+
successCount: 0,
|
|
1841
|
+
});
|
|
1402
1842
|
return {
|
|
1403
1843
|
duration: Date.now() - startTime,
|
|
1404
1844
|
failures: [],
|
|
@@ -1420,8 +1860,18 @@ Write output to: ${outputPath}`;
|
|
|
1420
1860
|
if (onStoryComplete) {
|
|
1421
1861
|
await this.enqueueExistingStoriesSequentially(allStories, storyDir, prefix, onStoryComplete);
|
|
1422
1862
|
}
|
|
1863
|
+
const duration = Date.now() - startTime;
|
|
1864
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1865
|
+
duration,
|
|
1866
|
+
endTime: Date.now(),
|
|
1867
|
+
failureCount: 0,
|
|
1868
|
+
itemCount: allStories.length,
|
|
1869
|
+
phaseName: 'story',
|
|
1870
|
+
startTime,
|
|
1871
|
+
successCount: allStories.length,
|
|
1872
|
+
});
|
|
1423
1873
|
return {
|
|
1424
|
-
duration
|
|
1874
|
+
duration,
|
|
1425
1875
|
failures: [],
|
|
1426
1876
|
phaseName: 'story',
|
|
1427
1877
|
skipped: false,
|
|
@@ -1441,107 +1891,159 @@ Write output to: ${outputPath}`;
|
|
|
1441
1891
|
: this.batchProcessor;
|
|
1442
1892
|
// Create story files using BatchProcessor and ClaudeAgentRunner
|
|
1443
1893
|
const results = await storyBatchProcessor.processBatch(storiesToCreate, async (story) => {
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1894
|
+
const spawnStartTime = Date.now();
|
|
1895
|
+
const spawnId = `story-${story.fullNumber}`;
|
|
1896
|
+
// Fire onSpawnStart so the reporter shows which story is being created
|
|
1897
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
1898
|
+
agentType: 'sm',
|
|
1899
|
+
itemId: `story-${story.fullNumber}`,
|
|
1900
|
+
itemTitle: story.title,
|
|
1901
|
+
phaseName: 'story',
|
|
1902
|
+
spawnId,
|
|
1903
|
+
startTime: spawnStartTime,
|
|
1904
|
+
});
|
|
1905
|
+
try {
|
|
1906
|
+
// Generate story file path with prefix
|
|
1907
|
+
const storyFileName = this.generateStoryFileName(prefix, story.fullNumber);
|
|
1908
|
+
const storyFilePath = `${storyDir}/${storyFileName}`;
|
|
1909
|
+
// Check if file already exists (might have been created by parallel process)
|
|
1910
|
+
const fileExists = await this.fileManager.fileExists(storyFilePath);
|
|
1911
|
+
if (fileExists) {
|
|
1912
|
+
this.logger.info({
|
|
1913
|
+
filePath: storyFilePath,
|
|
1914
|
+
storyNumber: story.fullNumber,
|
|
1915
|
+
storyTitle: story.title,
|
|
1916
|
+
}, 'Story file already exists, skipping creation');
|
|
1917
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
1918
|
+
agentType: 'sm',
|
|
1919
|
+
duration: Date.now() - spawnStartTime,
|
|
1920
|
+
endTime: Date.now(),
|
|
1921
|
+
itemId: `story-${story.fullNumber}`,
|
|
1922
|
+
itemTitle: story.title,
|
|
1923
|
+
phaseName: 'story',
|
|
1924
|
+
spawnId,
|
|
1925
|
+
startTime: spawnStartTime,
|
|
1926
|
+
success: true,
|
|
1927
|
+
});
|
|
1928
|
+
return storyFilePath;
|
|
1929
|
+
}
|
|
1930
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata
|
|
1931
|
+
const scaffoldedContent = this.fileScaffolder.scaffoldStory({
|
|
1932
|
+
epicNumber: story.epicNumber,
|
|
1933
|
+
storyNumber: story.number,
|
|
1934
|
+
storyTitle: story.title,
|
|
1935
|
+
});
|
|
1936
|
+
await this.fileManager.writeFile(storyFilePath, scaffoldedContent);
|
|
1450
1937
|
this.logger.info({
|
|
1451
1938
|
filePath: storyFilePath,
|
|
1452
1939
|
storyNumber: story.fullNumber,
|
|
1453
1940
|
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({
|
|
1941
|
+
}, 'Story scaffolded file created');
|
|
1942
|
+
// Step 2: Generate epic file path for this story
|
|
1943
|
+
const epicFileName = this.generateEpicFileName(prefix, story.epicNumber);
|
|
1944
|
+
const epicFilePath = `${epicDir}/${epicFileName}`;
|
|
1945
|
+
// Step 3: Build Claude prompt to populate the scaffolded file
|
|
1946
|
+
const prompt = this.buildStoryPrompt(story, {
|
|
1947
|
+
cwd: config.cwd,
|
|
1948
|
+
epicPath: epicFilePath,
|
|
1483
1949
|
outputPath: storyFilePath,
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
references: config.references,
|
|
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,
|
|
1950
|
+
prefix,
|
|
1951
|
+
references: config.references,
|
|
1952
|
+
});
|
|
1953
|
+
// Log prompt if verbose
|
|
1954
|
+
if (config.verbose) {
|
|
1955
|
+
this.logger.info({
|
|
1956
|
+
outputPath: storyFilePath,
|
|
1957
|
+
prompt,
|
|
1533
1958
|
storyNumber: story.fullNumber,
|
|
1534
|
-
|
|
1535
|
-
|
|
1959
|
+
storyTitle: story.title,
|
|
1960
|
+
}, 'Claude Prompt (Story)');
|
|
1536
1961
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1962
|
+
// Step 4: Run Claude agent to populate content sections
|
|
1963
|
+
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
1964
|
+
agentType: 'sm',
|
|
1965
|
+
references: config.references,
|
|
1966
|
+
timeout: config.timeout ?? 2_700_000,
|
|
1967
|
+
}, {
|
|
1968
|
+
backoffMs: config.retryBackoffMs,
|
|
1969
|
+
logger: this.logger,
|
|
1970
|
+
maxRetries: config.maxRetries,
|
|
1971
|
+
});
|
|
1972
|
+
// Log output if verbose
|
|
1973
|
+
if (config.verbose) {
|
|
1974
|
+
this.logger.info({
|
|
1975
|
+
duration: result.duration,
|
|
1976
|
+
errors: result.errors,
|
|
1977
|
+
output: result.output,
|
|
1978
|
+
outputLength: result.output.length,
|
|
1540
1979
|
storyNumber: story.fullNumber,
|
|
1541
|
-
|
|
1980
|
+
success: result.success,
|
|
1981
|
+
}, 'Claude Response (Story)');
|
|
1982
|
+
}
|
|
1983
|
+
if (!result.success) {
|
|
1984
|
+
throw new Error(result.errors);
|
|
1542
1985
|
}
|
|
1986
|
+
// Step 5: Verify file was updated by Claude
|
|
1987
|
+
const updatedContent = await this.fileManager.readFile(storyFilePath);
|
|
1988
|
+
if (updatedContent === scaffoldedContent) {
|
|
1989
|
+
throw new Error(`Claude did not update the story file at ${storyFilePath}`);
|
|
1990
|
+
}
|
|
1991
|
+
// Invoke callback after successful story creation
|
|
1992
|
+
if (onStoryComplete) {
|
|
1993
|
+
try {
|
|
1994
|
+
const metadata = {
|
|
1995
|
+
epicNumber: story.epicNumber,
|
|
1996
|
+
filePath: storyFilePath,
|
|
1997
|
+
id: story.fullNumber,
|
|
1998
|
+
number: story.fullNumber,
|
|
1999
|
+
status: 'Draft',
|
|
2000
|
+
storyNumber: story.number,
|
|
2001
|
+
title: story.title,
|
|
2002
|
+
type: 'epic-based',
|
|
2003
|
+
};
|
|
2004
|
+
this.logger.debug({
|
|
2005
|
+
metadata,
|
|
2006
|
+
storyNumber: story.fullNumber,
|
|
2007
|
+
}, 'Invoking story completion callback');
|
|
2008
|
+
await onStoryComplete(metadata);
|
|
2009
|
+
}
|
|
2010
|
+
catch (error) {
|
|
2011
|
+
this.logger.error({
|
|
2012
|
+
error: error.message,
|
|
2013
|
+
storyNumber: story.fullNumber,
|
|
2014
|
+
}, 'Story completion callback failed, continuing story creation');
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
// Fire onSpawnComplete for success
|
|
2018
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2019
|
+
agentType: 'sm',
|
|
2020
|
+
duration: Date.now() - spawnStartTime,
|
|
2021
|
+
endTime: Date.now(),
|
|
2022
|
+
itemId: `story-${story.fullNumber}`,
|
|
2023
|
+
itemTitle: story.title,
|
|
2024
|
+
phaseName: 'story',
|
|
2025
|
+
spawnId,
|
|
2026
|
+
startTime: spawnStartTime,
|
|
2027
|
+
success: true,
|
|
2028
|
+
});
|
|
2029
|
+
return storyFilePath;
|
|
2030
|
+
}
|
|
2031
|
+
catch (spawnError) {
|
|
2032
|
+
// Fire onSpawnComplete for failure
|
|
2033
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2034
|
+
agentType: 'sm',
|
|
2035
|
+
duration: Date.now() - spawnStartTime,
|
|
2036
|
+
endTime: Date.now(),
|
|
2037
|
+
error: spawnError.message,
|
|
2038
|
+
itemId: `story-${story.fullNumber}`,
|
|
2039
|
+
itemTitle: story.title,
|
|
2040
|
+
phaseName: 'story',
|
|
2041
|
+
spawnId,
|
|
2042
|
+
startTime: spawnStartTime,
|
|
2043
|
+
success: false,
|
|
2044
|
+
});
|
|
2045
|
+
throw spawnError;
|
|
1543
2046
|
}
|
|
1544
|
-
return storyFilePath;
|
|
1545
2047
|
}, (info) => {
|
|
1546
2048
|
this.logger.info({
|
|
1547
2049
|
completedItems: info.completedItems,
|
|
@@ -1565,11 +2067,22 @@ Write output to: ${outputPath}`;
|
|
|
1565
2067
|
// Add existing stories to success count
|
|
1566
2068
|
successCount += existingStories.length;
|
|
1567
2069
|
const duration = Date.now() - startTime;
|
|
2070
|
+
const endTime = Date.now();
|
|
1568
2071
|
this.logger.info({
|
|
1569
2072
|
duration,
|
|
1570
2073
|
failures: failures.length,
|
|
1571
2074
|
success: successCount,
|
|
1572
2075
|
}, 'Story creation phase completed');
|
|
2076
|
+
// Invoke onPhaseComplete callback
|
|
2077
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2078
|
+
duration,
|
|
2079
|
+
endTime,
|
|
2080
|
+
failureCount: failures.length,
|
|
2081
|
+
itemCount,
|
|
2082
|
+
phaseName: 'story',
|
|
2083
|
+
startTime,
|
|
2084
|
+
successCount,
|
|
2085
|
+
});
|
|
1573
2086
|
return {
|
|
1574
2087
|
duration,
|
|
1575
2088
|
failures,
|
|
@@ -1579,9 +2092,28 @@ Write output to: ${outputPath}`;
|
|
|
1579
2092
|
};
|
|
1580
2093
|
}
|
|
1581
2094
|
catch (error) {
|
|
2095
|
+
const duration = Date.now() - startTime;
|
|
2096
|
+
const endTime = Date.now();
|
|
1582
2097
|
this.logger.error({ error: error.message }, 'Story phase failed');
|
|
2098
|
+
// Invoke onError callback
|
|
2099
|
+
this.invokeErrorCallback(error, {
|
|
2100
|
+
itemId: 'story-phase',
|
|
2101
|
+
phaseName: 'story',
|
|
2102
|
+
recoverable: false,
|
|
2103
|
+
});
|
|
2104
|
+
// Invoke onPhaseComplete callback even on error
|
|
2105
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2106
|
+
duration,
|
|
2107
|
+
endTime,
|
|
2108
|
+
failureCount: failures.length + 1,
|
|
2109
|
+
itemCount,
|
|
2110
|
+
metadata: { error: error.message },
|
|
2111
|
+
phaseName: 'story',
|
|
2112
|
+
startTime,
|
|
2113
|
+
successCount,
|
|
2114
|
+
});
|
|
1583
2115
|
return {
|
|
1584
|
-
duration
|
|
2116
|
+
duration,
|
|
1585
2117
|
failures: [
|
|
1586
2118
|
...failures,
|
|
1587
2119
|
{
|
|
@@ -1613,7 +2145,7 @@ Write output to: ${outputPath}`;
|
|
|
1613
2145
|
// In pipeline mode, stories must be created sequentially (one at a time)
|
|
1614
2146
|
// to ensure they are queued in the correct order for development
|
|
1615
2147
|
const isPipelineMode = Boolean(onStoryComplete);
|
|
1616
|
-
const effectiveParallel =
|
|
2148
|
+
const effectiveParallel = config.parallel;
|
|
1617
2149
|
this.logger.info({
|
|
1618
2150
|
epicFilePath,
|
|
1619
2151
|
mode: isPipelineMode ? 'sequential (pipeline)' : 'parallel',
|
|
@@ -1652,8 +2184,18 @@ Write output to: ${outputPath}`;
|
|
|
1652
2184
|
if (onStoryComplete) {
|
|
1653
2185
|
await this.enqueueExistingStoriesSequentially(allStories, storyDir, prefix, onStoryComplete);
|
|
1654
2186
|
}
|
|
2187
|
+
const duration = Date.now() - startTime;
|
|
2188
|
+
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
2189
|
+
duration,
|
|
2190
|
+
endTime: Date.now(),
|
|
2191
|
+
failureCount: 0,
|
|
2192
|
+
itemCount: allStories.length,
|
|
2193
|
+
phaseName: 'story',
|
|
2194
|
+
startTime,
|
|
2195
|
+
successCount: allStories.length,
|
|
2196
|
+
});
|
|
1655
2197
|
return {
|
|
1656
|
-
duration
|
|
2198
|
+
duration,
|
|
1657
2199
|
failures: [],
|
|
1658
2200
|
phaseName: 'story',
|
|
1659
2201
|
skipped: false,
|
|
@@ -1673,106 +2215,158 @@ Write output to: ${outputPath}`;
|
|
|
1673
2215
|
: this.batchProcessor;
|
|
1674
2216
|
// Create story files using BatchProcessor and ClaudeAgentRunner
|
|
1675
2217
|
const results = await storyBatchProcessor.processBatch(storiesToCreate, async (story) => {
|
|
1676
|
-
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2218
|
+
const spawnStartTime = Date.now();
|
|
2219
|
+
const spawnId = `story-${story.fullNumber}`;
|
|
2220
|
+
// Fire onSpawnStart so the reporter shows which story is being created
|
|
2221
|
+
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
2222
|
+
agentType: 'sm',
|
|
2223
|
+
itemId: `story-${story.fullNumber}`,
|
|
2224
|
+
itemTitle: story.title,
|
|
2225
|
+
phaseName: 'story',
|
|
2226
|
+
spawnId,
|
|
2227
|
+
startTime: spawnStartTime,
|
|
2228
|
+
});
|
|
2229
|
+
try {
|
|
2230
|
+
// Generate story file path with prefix
|
|
2231
|
+
const storyFileName = this.generateStoryFileName(prefix, story.fullNumber);
|
|
2232
|
+
const storyFilePath = `${storyDir}/${storyFileName}`;
|
|
2233
|
+
// Check if file already exists (might have been created by parallel process)
|
|
2234
|
+
const fileExists = await this.fileManager.fileExists(storyFilePath);
|
|
2235
|
+
if (fileExists) {
|
|
2236
|
+
this.logger.info({
|
|
2237
|
+
filePath: storyFilePath,
|
|
2238
|
+
storyNumber: story.fullNumber,
|
|
2239
|
+
storyTitle: story.title,
|
|
2240
|
+
}, 'Story file already exists, skipping creation');
|
|
2241
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2242
|
+
agentType: 'sm',
|
|
2243
|
+
duration: Date.now() - spawnStartTime,
|
|
2244
|
+
endTime: Date.now(),
|
|
2245
|
+
itemId: `story-${story.fullNumber}`,
|
|
2246
|
+
itemTitle: story.title,
|
|
2247
|
+
phaseName: 'story',
|
|
2248
|
+
spawnId,
|
|
2249
|
+
startTime: spawnStartTime,
|
|
2250
|
+
success: true,
|
|
2251
|
+
});
|
|
2252
|
+
return storyFilePath;
|
|
2253
|
+
}
|
|
2254
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata
|
|
2255
|
+
const scaffoldedContent = this.fileScaffolder.scaffoldStory({
|
|
2256
|
+
epicNumber: story.epicNumber,
|
|
2257
|
+
storyNumber: story.number,
|
|
2258
|
+
storyTitle: story.title,
|
|
2259
|
+
});
|
|
2260
|
+
await this.fileManager.writeFile(storyFilePath, scaffoldedContent);
|
|
1682
2261
|
this.logger.info({
|
|
1683
2262
|
filePath: storyFilePath,
|
|
1684
2263
|
storyNumber: story.fullNumber,
|
|
1685
2264
|
storyTitle: story.title,
|
|
1686
|
-
}, 'Story file
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
epicNumber: story.epicNumber,
|
|
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({
|
|
2265
|
+
}, 'Story scaffolded file created');
|
|
2266
|
+
// Step 2: Use the epic file path that was passed to this method
|
|
2267
|
+
// Step 3: Build Claude prompt to populate the scaffolded file
|
|
2268
|
+
const prompt = this.buildStoryPrompt(story, {
|
|
2269
|
+
cwd: config.cwd,
|
|
1713
2270
|
epicPath: epicFilePath,
|
|
1714
2271
|
outputPath: storyFilePath,
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
timeout: config.timeout ?? 2_700_000,
|
|
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,
|
|
2272
|
+
prefix,
|
|
2273
|
+
references: config.references,
|
|
2274
|
+
});
|
|
2275
|
+
// Log prompt if verbose
|
|
2276
|
+
if (config.verbose) {
|
|
2277
|
+
this.logger.info({
|
|
2278
|
+
epicPath: epicFilePath,
|
|
2279
|
+
outputPath: storyFilePath,
|
|
2280
|
+
prompt,
|
|
1764
2281
|
storyNumber: story.fullNumber,
|
|
1765
|
-
|
|
1766
|
-
|
|
2282
|
+
storyTitle: story.title,
|
|
2283
|
+
}, 'Claude Prompt (Story)');
|
|
1767
2284
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2285
|
+
// Step 4: Run Claude agent to populate content sections
|
|
2286
|
+
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
2287
|
+
agentType: 'sm',
|
|
2288
|
+
references: config.references,
|
|
2289
|
+
timeout: config.timeout ?? 2_700_000,
|
|
2290
|
+
}, {
|
|
2291
|
+
backoffMs: config.retryBackoffMs,
|
|
2292
|
+
logger: this.logger,
|
|
2293
|
+
maxRetries: config.maxRetries,
|
|
2294
|
+
});
|
|
2295
|
+
// Log output if verbose
|
|
2296
|
+
if (config.verbose) {
|
|
2297
|
+
this.logger.info({
|
|
2298
|
+
duration: result.duration,
|
|
2299
|
+
errors: result.errors,
|
|
2300
|
+
output: result.output,
|
|
2301
|
+
outputLength: result.output.length,
|
|
1771
2302
|
storyNumber: story.fullNumber,
|
|
1772
|
-
|
|
2303
|
+
success: result.success,
|
|
2304
|
+
}, 'Claude Response (Story)');
|
|
2305
|
+
}
|
|
2306
|
+
if (!result.success) {
|
|
2307
|
+
throw new Error(result.errors);
|
|
2308
|
+
}
|
|
2309
|
+
// Step 5: Verify file was updated by Claude
|
|
2310
|
+
const updatedContent = await this.fileManager.readFile(storyFilePath);
|
|
2311
|
+
if (updatedContent === scaffoldedContent) {
|
|
2312
|
+
throw new Error(`Claude did not update the story file at ${storyFilePath}`);
|
|
2313
|
+
}
|
|
2314
|
+
// Invoke callback after successful story creation
|
|
2315
|
+
if (onStoryComplete) {
|
|
2316
|
+
try {
|
|
2317
|
+
const metadata = {
|
|
2318
|
+
epicNumber: story.epicNumber,
|
|
2319
|
+
filePath: storyFilePath,
|
|
2320
|
+
id: story.fullNumber,
|
|
2321
|
+
number: story.fullNumber,
|
|
2322
|
+
status: 'Draft',
|
|
2323
|
+
storyNumber: story.number,
|
|
2324
|
+
title: story.title,
|
|
2325
|
+
type: 'epic-based',
|
|
2326
|
+
};
|
|
2327
|
+
this.logger.debug({
|
|
2328
|
+
metadata,
|
|
2329
|
+
storyNumber: story.fullNumber,
|
|
2330
|
+
}, 'Invoking story completion callback');
|
|
2331
|
+
await onStoryComplete(metadata);
|
|
2332
|
+
}
|
|
2333
|
+
catch (error) {
|
|
2334
|
+
this.logger.error({
|
|
2335
|
+
error: error.message,
|
|
2336
|
+
storyNumber: story.fullNumber,
|
|
2337
|
+
}, 'Story completion callback failed, continuing story creation');
|
|
2338
|
+
}
|
|
1773
2339
|
}
|
|
2340
|
+
// Fire onSpawnComplete for success
|
|
2341
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2342
|
+
agentType: 'sm',
|
|
2343
|
+
duration: Date.now() - spawnStartTime,
|
|
2344
|
+
endTime: Date.now(),
|
|
2345
|
+
itemId: `story-${story.fullNumber}`,
|
|
2346
|
+
itemTitle: story.title,
|
|
2347
|
+
phaseName: 'story',
|
|
2348
|
+
spawnId,
|
|
2349
|
+
startTime: spawnStartTime,
|
|
2350
|
+
success: true,
|
|
2351
|
+
});
|
|
2352
|
+
return storyFilePath;
|
|
2353
|
+
}
|
|
2354
|
+
catch (spawnError) {
|
|
2355
|
+
// Fire onSpawnComplete for failure
|
|
2356
|
+
this.invokeCallback('onSpawnComplete', this.callbacks?.onSpawnComplete, {
|
|
2357
|
+
agentType: 'sm',
|
|
2358
|
+
duration: Date.now() - spawnStartTime,
|
|
2359
|
+
endTime: Date.now(),
|
|
2360
|
+
error: spawnError.message,
|
|
2361
|
+
itemId: `story-${story.fullNumber}`,
|
|
2362
|
+
itemTitle: story.title,
|
|
2363
|
+
phaseName: 'story',
|
|
2364
|
+
spawnId,
|
|
2365
|
+
startTime: spawnStartTime,
|
|
2366
|
+
success: false,
|
|
2367
|
+
});
|
|
2368
|
+
throw spawnError;
|
|
1774
2369
|
}
|
|
1775
|
-
return storyFilePath;
|
|
1776
2370
|
}, (info) => {
|
|
1777
2371
|
this.logger.info({
|
|
1778
2372
|
completedItems: info.completedItems,
|