@hyperdrive.bot/bmad-workflow 1.0.26 → 1.0.28
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/epics/create.d.ts +1 -0
- package/dist/commands/lock/acquire.d.ts +54 -0
- package/dist/commands/lock/acquire.js +193 -0
- package/dist/commands/lock/cleanup.d.ts +38 -0
- package/dist/commands/lock/cleanup.js +148 -0
- package/dist/commands/lock/list.d.ts +31 -0
- package/dist/commands/lock/list.js +123 -0
- package/dist/commands/lock/release.d.ts +42 -0
- package/dist/commands/lock/release.js +134 -0
- package/dist/commands/lock/status.d.ts +34 -0
- package/dist/commands/lock/status.js +109 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +4 -0
- package/dist/commands/stories/develop.js +55 -5
- package/dist/commands/stories/qa.d.ts +1 -0
- package/dist/commands/stories/qa.js +31 -0
- package/dist/commands/stories/review.d.ts +1 -0
- package/dist/commands/workflow.d.ts +11 -0
- package/dist/commands/workflow.js +120 -4
- package/dist/models/agent-options.d.ts +33 -0
- package/dist/models/agent-result.d.ts +10 -1
- package/dist/models/dispatch.d.ts +16 -0
- package/dist/models/dispatch.js +8 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +2 -0
- package/dist/models/lock.d.ts +80 -0
- package/dist/models/lock.js +69 -0
- package/dist/models/phase-result.d.ts +8 -0
- package/dist/models/provider.js +1 -1
- package/dist/models/workflow-callbacks.d.ts +37 -0
- package/dist/models/workflow-config.d.ts +50 -0
- package/dist/services/agents/agent-runner-factory.d.ts +14 -15
- package/dist/services/agents/agent-runner-factory.js +56 -15
- package/dist/services/agents/channel-agent-runner.d.ts +76 -0
- package/dist/services/agents/channel-agent-runner.js +246 -0
- package/dist/services/agents/channel-session-manager.d.ts +119 -0
- package/dist/services/agents/channel-session-manager.js +250 -0
- package/dist/services/agents/claude-agent-runner.d.ts +9 -50
- package/dist/services/agents/claude-agent-runner.js +221 -199
- package/dist/services/agents/gemini-agent-runner.js +3 -0
- package/dist/services/agents/index.d.ts +1 -0
- package/dist/services/agents/index.js +1 -0
- package/dist/services/agents/opencode-agent-runner.js +3 -0
- package/dist/services/file-system/file-manager.d.ts +11 -0
- package/dist/services/file-system/file-manager.js +26 -0
- package/dist/services/git/git-ops.d.ts +58 -0
- package/dist/services/git/git-ops.js +73 -0
- package/dist/services/git/index.d.ts +3 -0
- package/dist/services/git/index.js +2 -0
- package/dist/services/git/push-conflict-handler.d.ts +32 -0
- package/dist/services/git/push-conflict-handler.js +84 -0
- package/dist/services/lock/git-backed-lock-service.d.ts +76 -0
- package/dist/services/lock/git-backed-lock-service.js +173 -0
- package/dist/services/lock/lock-cleanup.d.ts +49 -0
- package/dist/services/lock/lock-cleanup.js +85 -0
- package/dist/services/lock/lock-service.d.ts +143 -0
- package/dist/services/lock/lock-service.js +290 -0
- package/dist/services/orchestration/locked-story-dispatcher.d.ts +40 -0
- package/dist/services/orchestration/locked-story-dispatcher.js +84 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +31 -0
- package/dist/services/orchestration/workflow-orchestrator.js +181 -31
- package/dist/services/review/ai-review-scanner.js +1 -0
- package/dist/services/review/review-phase-executor.js +3 -0
- package/dist/services/review/self-heal-loop.js +1 -0
- package/dist/services/review/types.d.ts +2 -0
- package/dist/utils/errors.d.ts +17 -1
- package/dist/utils/errors.js +18 -0
- package/dist/utils/session-naming.d.ts +23 -0
- package/dist/utils/session-naming.js +30 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +5 -0
- package/package.json +3 -2
|
@@ -43,13 +43,16 @@
|
|
|
43
43
|
* })
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
|
+
import { randomUUID } from 'node:crypto';
|
|
46
47
|
import { isEpicStory } from '../../models/story.js';
|
|
47
48
|
import { ParserError, ValidationError } from '../../utils/errors.js';
|
|
48
49
|
import { runAgentWithRetry } from '../../utils/retry.js';
|
|
50
|
+
import { derivePrefixForSessionName } from '../../utils/session-naming.js';
|
|
49
51
|
import { AssetResolver } from '../file-system/asset-resolver.js';
|
|
50
52
|
import { PrdFixer } from '../parsers/prd-fixer.js';
|
|
51
53
|
import { FileScaffolder } from '../scaffolding/file-scaffolder.js';
|
|
52
54
|
import { BatchProcessor } from './batch-processor.js';
|
|
55
|
+
import { LockedStoryDispatcher } from './locked-story-dispatcher.js';
|
|
53
56
|
import { StoryQueue } from './story-queue.js';
|
|
54
57
|
import { ReviewQueue } from '../review/review-queue.js';
|
|
55
58
|
/**
|
|
@@ -61,6 +64,7 @@ import { ReviewQueue } from '../review/review-queue.js';
|
|
|
61
64
|
*/
|
|
62
65
|
export class WorkflowOrchestrator {
|
|
63
66
|
agentRunner;
|
|
67
|
+
agentRunnerFactory;
|
|
64
68
|
assetResolver;
|
|
65
69
|
batchProcessor;
|
|
66
70
|
callbacks;
|
|
@@ -68,6 +72,7 @@ export class WorkflowOrchestrator {
|
|
|
68
72
|
fileManager;
|
|
69
73
|
fileScaffolder;
|
|
70
74
|
inputDetector;
|
|
75
|
+
lockService;
|
|
71
76
|
logger;
|
|
72
77
|
mcpContextInjector;
|
|
73
78
|
pathResolver;
|
|
@@ -87,12 +92,14 @@ export class WorkflowOrchestrator {
|
|
|
87
92
|
this.prdParser = config.prdParser;
|
|
88
93
|
this.epicParser = config.epicParser;
|
|
89
94
|
this.agentRunner = config.agentRunner;
|
|
95
|
+
this.agentRunnerFactory = config.agentRunnerFactory;
|
|
90
96
|
this.batchProcessor = config.batchProcessor;
|
|
91
97
|
this.fileManager = config.fileManager;
|
|
92
98
|
this.mcpContextInjector = config.mcpContextInjector;
|
|
93
99
|
this.pathResolver = config.pathResolver;
|
|
94
100
|
this.reviewPhaseExecutor = config.reviewPhaseExecutor;
|
|
95
101
|
this.storyTypeDetector = config.storyTypeDetector;
|
|
102
|
+
this.lockService = config.lockService;
|
|
96
103
|
this.logger = config.logger;
|
|
97
104
|
this.workflowLogger = config.workflowLogger;
|
|
98
105
|
this.callbacks = config.callbacks;
|
|
@@ -100,6 +107,75 @@ export class WorkflowOrchestrator {
|
|
|
100
107
|
this.prdFixer = new PrdFixer(config.agentRunner, config.fileManager, config.logger);
|
|
101
108
|
this.logger.debug('WorkflowOrchestrator initialized');
|
|
102
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Select the appropriate runner for the given agent type and config.
|
|
112
|
+
*
|
|
113
|
+
* When agentRunnerFactory is set and config.useChannels is true, delegates
|
|
114
|
+
* to the factory for discovery-based transport selection. Otherwise returns
|
|
115
|
+
* the default subprocess runner.
|
|
116
|
+
*/
|
|
117
|
+
getRunner(agentType, config) {
|
|
118
|
+
if (this.agentRunnerFactory && config.useChannels) {
|
|
119
|
+
const runner = this.agentRunnerFactory.create(agentType, config);
|
|
120
|
+
this.logger.debug({ agentType, transport: 'channel' }, 'Runner selected via factory');
|
|
121
|
+
return runner;
|
|
122
|
+
}
|
|
123
|
+
return this.agentRunner;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build stream callback options from workflow config.
|
|
127
|
+
* Returns onStream (spinner summary) and/or onStreamVerbose (raw passthrough)
|
|
128
|
+
* based on config.stream flag.
|
|
129
|
+
*/
|
|
130
|
+
getStreamCallbacks(config, label) {
|
|
131
|
+
if (!config.stream) {
|
|
132
|
+
return {
|
|
133
|
+
onStream: (summary) => {
|
|
134
|
+
this.logger.info({ phase: label }, summary);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Stream mode: pipe raw text to stdout with phase labels
|
|
139
|
+
process.stdout.write(`\n── [${label}] streaming ──\n`);
|
|
140
|
+
return {
|
|
141
|
+
onStreamVerbose: (text) => {
|
|
142
|
+
process.stdout.write(text + '\n');
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve session name template with placeholders.
|
|
148
|
+
*
|
|
149
|
+
* @param config - Workflow config containing the sessionName template
|
|
150
|
+
* @param phase - Current phase name (e.g., "dev", "epic", "story")
|
|
151
|
+
* @param storyId - Optional story/epic identifier for {story} placeholder
|
|
152
|
+
* @returns Resolved session name string, or undefined if no template
|
|
153
|
+
*/
|
|
154
|
+
resolveSessionName(config, phase, storyId) {
|
|
155
|
+
let template = config.sessionName;
|
|
156
|
+
// Auto-generate if no explicit template but inputPath is available
|
|
157
|
+
if (!template && config.inputPath) {
|
|
158
|
+
const prefix = derivePrefixForSessionName(config.inputPath);
|
|
159
|
+
template = `${prefix}-{phase}-{story}`;
|
|
160
|
+
}
|
|
161
|
+
if (!template)
|
|
162
|
+
return undefined;
|
|
163
|
+
// Replace {prefix} placeholder
|
|
164
|
+
template = template.replace(/\{prefix\}/g, config.inputPath ? derivePrefixForSessionName(config.inputPath) : '');
|
|
165
|
+
// Replace {phase}
|
|
166
|
+
template = template.replace(/\{phase\}/g, phase);
|
|
167
|
+
// Stutter normalization: strip phase word from storyId when it duplicates {phase}
|
|
168
|
+
if (storyId) {
|
|
169
|
+
if (phase === 'epic' && storyId.startsWith('epic-')) {
|
|
170
|
+
storyId = storyId.slice('epic-'.length);
|
|
171
|
+
}
|
|
172
|
+
else if (phase === 'story' && storyId.startsWith('story-')) {
|
|
173
|
+
storyId = storyId.slice('story-'.length);
|
|
174
|
+
}
|
|
175
|
+
template = template.replace(/\{story\}/g, storyId);
|
|
176
|
+
}
|
|
177
|
+
return template;
|
|
178
|
+
}
|
|
103
179
|
/**
|
|
104
180
|
* Execute the complete workflow
|
|
105
181
|
*
|
|
@@ -550,9 +626,12 @@ Write output to: ${outputPath}`;
|
|
|
550
626
|
* @returns Promise resolving to object with success count and failure array
|
|
551
627
|
* @private
|
|
552
628
|
*/
|
|
553
|
-
async devWorker(workerId, queue, config, reviewQueue) {
|
|
629
|
+
async devWorker(workerId, queue, config, dispatcher, reviewQueue) {
|
|
554
630
|
const workerLogger = this.logger.child({ workerId });
|
|
555
631
|
let successCount = 0;
|
|
632
|
+
let skippedCount = 0;
|
|
633
|
+
let processedCount = 0;
|
|
634
|
+
let failedCount = 0;
|
|
556
635
|
const failures = [];
|
|
557
636
|
let devPhaseStarted = false;
|
|
558
637
|
workerLogger.info('Worker started');
|
|
@@ -581,7 +660,8 @@ Write output to: ${outputPath}`;
|
|
|
581
660
|
storyNumber: story.fullNumber,
|
|
582
661
|
storyTitle: story.title,
|
|
583
662
|
}, 'Worker processing story');
|
|
584
|
-
|
|
663
|
+
// Dispatch through lock-aware wrapper (passthrough when locking disabled)
|
|
664
|
+
const dispatchResult = await dispatcher.dispatch(story, async () => {
|
|
585
665
|
// Use the actual file path from the story object
|
|
586
666
|
const storyFilePath = story.filePath || `${storyDir}/STORY-${story.fullNumber}.md`;
|
|
587
667
|
// Extract just the filename for QA folder (preserve the same filename)
|
|
@@ -595,7 +675,7 @@ Write output to: ${outputPath}`;
|
|
|
595
675
|
storyNumber: story.fullNumber,
|
|
596
676
|
}, 'Story already in QA folder, skipping development');
|
|
597
677
|
successCount++;
|
|
598
|
-
|
|
678
|
+
return;
|
|
599
679
|
}
|
|
600
680
|
const storyExists = await this.fileManager.fileExists(storyFilePath);
|
|
601
681
|
if (!storyExists) {
|
|
@@ -604,7 +684,7 @@ Write output to: ${outputPath}`;
|
|
|
604
684
|
error: 'Story file not found',
|
|
605
685
|
identifier: story.fullNumber,
|
|
606
686
|
});
|
|
607
|
-
|
|
687
|
+
return;
|
|
608
688
|
}
|
|
609
689
|
// Update story status to InProgress
|
|
610
690
|
await this.updateStoryStatus(storyFilePath, 'InProgress');
|
|
@@ -658,6 +738,9 @@ Write output to: ${outputPath}`;
|
|
|
658
738
|
prompt += '*yolo mode*\n';
|
|
659
739
|
// Generate unique spawn ID
|
|
660
740
|
const spawnId = `dev-${story.fullNumber}-${storyStartTime}`;
|
|
741
|
+
// Select runner via factory (Channel) or fallback (subprocess)
|
|
742
|
+
const runner = this.getRunner('dev', config);
|
|
743
|
+
const transport = runner === this.agentRunner ? 'subprocess' : 'channel';
|
|
661
744
|
// Invoke onSpawnStart callback
|
|
662
745
|
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
663
746
|
agentType: 'dev',
|
|
@@ -669,12 +752,16 @@ Write output to: ${outputPath}`;
|
|
|
669
752
|
spawnId,
|
|
670
753
|
startTime: storyStartTime,
|
|
671
754
|
workerId,
|
|
755
|
+
transport,
|
|
672
756
|
});
|
|
673
757
|
// Execute dev agent with retry on timeout/killed
|
|
674
|
-
const
|
|
758
|
+
const streamCbs = this.getStreamCallbacks(config, `dev ${story.fullNumber}`);
|
|
759
|
+
const result = await runAgentWithRetry(runner, prompt, {
|
|
675
760
|
agentType: 'dev',
|
|
676
761
|
cwd: config.cwd,
|
|
762
|
+
...streamCbs,
|
|
677
763
|
references: config.references,
|
|
764
|
+
sessionName: this.resolveSessionName(config, 'dev', story.fullNumber),
|
|
678
765
|
timeout: config.timeout ?? 2_700_000,
|
|
679
766
|
}, {
|
|
680
767
|
backoffMs: config.retryBackoffMs,
|
|
@@ -697,6 +784,7 @@ Write output to: ${outputPath}`;
|
|
|
697
784
|
spawnId,
|
|
698
785
|
startTime: storyStartTime,
|
|
699
786
|
success: true,
|
|
787
|
+
transport,
|
|
700
788
|
workerId,
|
|
701
789
|
});
|
|
702
790
|
// Update story status to Done
|
|
@@ -736,6 +824,7 @@ Write output to: ${outputPath}`;
|
|
|
736
824
|
spawnId,
|
|
737
825
|
startTime: storyStartTime,
|
|
738
826
|
success: false,
|
|
827
|
+
transport,
|
|
739
828
|
workerId,
|
|
740
829
|
});
|
|
741
830
|
// Invoke onError callback
|
|
@@ -765,23 +854,25 @@ Write output to: ${outputPath}`;
|
|
|
765
854
|
identifier: story.fullNumber,
|
|
766
855
|
});
|
|
767
856
|
}
|
|
857
|
+
});
|
|
858
|
+
// Track dispatch-level counters
|
|
859
|
+
if (dispatchResult.status === 'processed')
|
|
860
|
+
processedCount++;
|
|
861
|
+
else if (dispatchResult.status === 'skipped')
|
|
862
|
+
skippedCount++;
|
|
863
|
+
else if (dispatchResult.status === 'failed')
|
|
864
|
+
failedCount++;
|
|
865
|
+
// Handle dispatch result for lock-aware outcomes
|
|
866
|
+
if (dispatchResult.status === 'skipped') {
|
|
867
|
+
workerLogger.info({ reason: dispatchResult.reason, story: story.fullNumber }, 'Story locked, skipping');
|
|
868
|
+
continue;
|
|
768
869
|
}
|
|
769
|
-
|
|
770
|
-
// Invoke onError callback for unexpected errors
|
|
771
|
-
this.invokeErrorCallback(error, {
|
|
772
|
-
itemId: story.fullNumber,
|
|
773
|
-
phaseName: 'dev',
|
|
774
|
-
recoverable: true,
|
|
775
|
-
});
|
|
776
|
-
// Catch errors for individual stories to prevent worker crash
|
|
777
|
-
workerLogger.error({
|
|
778
|
-
error: error.message,
|
|
779
|
-
storyNumber: story.fullNumber,
|
|
780
|
-
}, 'Error processing story, continuing with next story');
|
|
870
|
+
if (dispatchResult.status === 'failed') {
|
|
781
871
|
failures.push({
|
|
782
|
-
error:
|
|
872
|
+
error: dispatchResult.reason || 'Dispatch failed',
|
|
783
873
|
identifier: story.fullNumber,
|
|
784
874
|
});
|
|
875
|
+
continue;
|
|
785
876
|
}
|
|
786
877
|
// Wait for configured interval before next story
|
|
787
878
|
if (config.storyInterval > 0) {
|
|
@@ -790,15 +881,18 @@ Write output to: ${outputPath}`;
|
|
|
790
881
|
}
|
|
791
882
|
}
|
|
792
883
|
workerLogger.info({
|
|
793
|
-
|
|
884
|
+
failed: failedCount,
|
|
885
|
+
processed: processedCount,
|
|
886
|
+
skipped: skippedCount,
|
|
794
887
|
successCount,
|
|
795
|
-
|
|
796
|
-
|
|
888
|
+
workerId,
|
|
889
|
+
}, `Worker ${workerId} complete: ${processedCount} processed, ${skippedCount} skipped, ${failedCount} failed`);
|
|
890
|
+
return { failed: failedCount, failures, processed: processedCount, skipped: skippedCount, success: successCount };
|
|
797
891
|
}
|
|
798
892
|
catch (error) {
|
|
799
893
|
workerLogger.error({ error: error.message }, 'Worker failed with unhandled error');
|
|
800
894
|
// Return partial results even on worker failure
|
|
801
|
-
return { failures, success: successCount };
|
|
895
|
+
return { failed: failedCount, failures, processed: processedCount, skipped: skippedCount, success: successCount };
|
|
802
896
|
}
|
|
803
897
|
}
|
|
804
898
|
/**
|
|
@@ -942,6 +1036,9 @@ Write output to: ${outputPath}`;
|
|
|
942
1036
|
// Generate unique spawn ID and timestamp
|
|
943
1037
|
const spawnStartTime = Date.now();
|
|
944
1038
|
const spawnId = `dev-${story.fullNumber}-${spawnStartTime}`;
|
|
1039
|
+
// Select runner via factory (Channel) or fallback (subprocess)
|
|
1040
|
+
const runner = this.getRunner('dev', config);
|
|
1041
|
+
const transport = runner === this.agentRunner ? 'subprocess' : 'channel';
|
|
945
1042
|
// Invoke onSpawnStart callback
|
|
946
1043
|
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
947
1044
|
agentType: 'dev',
|
|
@@ -952,11 +1049,15 @@ Write output to: ${outputPath}`;
|
|
|
952
1049
|
prompt,
|
|
953
1050
|
spawnId,
|
|
954
1051
|
startTime: spawnStartTime,
|
|
1052
|
+
transport,
|
|
955
1053
|
});
|
|
956
|
-
const
|
|
1054
|
+
const streamCbs2 = this.getStreamCallbacks(config, `dev ${story.fullNumber}`);
|
|
1055
|
+
const result = await runAgentWithRetry(runner, prompt, {
|
|
957
1056
|
agentType: 'dev',
|
|
958
1057
|
cwd: config.cwd,
|
|
1058
|
+
...streamCbs2,
|
|
959
1059
|
references: config.references,
|
|
1060
|
+
sessionName: this.resolveSessionName(config, 'dev', story.fullNumber),
|
|
960
1061
|
timeout: config.timeout ?? 2_700_000,
|
|
961
1062
|
}, {
|
|
962
1063
|
backoffMs: config.retryBackoffMs,
|
|
@@ -979,6 +1080,7 @@ Write output to: ${outputPath}`;
|
|
|
979
1080
|
spawnId,
|
|
980
1081
|
startTime: spawnStartTime,
|
|
981
1082
|
success: true,
|
|
1083
|
+
transport,
|
|
982
1084
|
});
|
|
983
1085
|
// Update story status to Done
|
|
984
1086
|
await this.updateStoryStatus(storyFilePath, 'Done');
|
|
@@ -1001,6 +1103,7 @@ Write output to: ${outputPath}`;
|
|
|
1001
1103
|
spawnId,
|
|
1002
1104
|
startTime: spawnStartTime,
|
|
1003
1105
|
success: false,
|
|
1106
|
+
transport,
|
|
1004
1107
|
});
|
|
1005
1108
|
// Invoke onError callback for spawn failure
|
|
1006
1109
|
this.invokeErrorCallback(new Error(result.errors), {
|
|
@@ -1202,6 +1305,9 @@ Write output to: ${outputPath}`;
|
|
|
1202
1305
|
const results = await this.batchProcessor.processBatch(epicsToCreate, async (epic) => {
|
|
1203
1306
|
const spawnStartTime = Date.now();
|
|
1204
1307
|
const spawnId = `epic-${epic.number}`;
|
|
1308
|
+
// Select runner via factory (Channel) or fallback (subprocess)
|
|
1309
|
+
const runner = this.getRunner('architect', config);
|
|
1310
|
+
const transport = runner === this.agentRunner ? 'subprocess' : 'channel';
|
|
1205
1311
|
// Fire onSpawnStart so the reporter shows which epic is being generated
|
|
1206
1312
|
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
1207
1313
|
agentType: 'architect',
|
|
@@ -1210,6 +1316,7 @@ Write output to: ${outputPath}`;
|
|
|
1210
1316
|
phaseName: 'epic',
|
|
1211
1317
|
spawnId,
|
|
1212
1318
|
startTime: spawnStartTime,
|
|
1319
|
+
transport,
|
|
1213
1320
|
});
|
|
1214
1321
|
try {
|
|
1215
1322
|
// Generate epic file path with prefix
|
|
@@ -1279,10 +1386,13 @@ Write output to: ${outputPath}`;
|
|
|
1279
1386
|
}, 'Claude Prompt (Epic)');
|
|
1280
1387
|
}
|
|
1281
1388
|
// Step 3: Run Claude agent to populate content sections
|
|
1282
|
-
const
|
|
1389
|
+
const epicStreamCbs = this.getStreamCallbacks(config, `epic ${epic.number}`);
|
|
1390
|
+
const result = await runAgentWithRetry(runner, prompt, {
|
|
1283
1391
|
agentType: 'architect',
|
|
1284
1392
|
cwd: config.cwd,
|
|
1393
|
+
...epicStreamCbs,
|
|
1285
1394
|
references: config.references,
|
|
1395
|
+
sessionName: this.resolveSessionName(config, 'epic', `epic-${epic.number}`),
|
|
1286
1396
|
timeout: config.timeout ?? 2_700_000,
|
|
1287
1397
|
}, {
|
|
1288
1398
|
backoffMs: config.retryBackoffMs,
|
|
@@ -1319,6 +1429,7 @@ Write output to: ${outputPath}`;
|
|
|
1319
1429
|
spawnId,
|
|
1320
1430
|
startTime: spawnStartTime,
|
|
1321
1431
|
success: true,
|
|
1432
|
+
transport,
|
|
1322
1433
|
});
|
|
1323
1434
|
return epicFilePath;
|
|
1324
1435
|
}
|
|
@@ -1335,6 +1446,7 @@ Write output to: ${outputPath}`;
|
|
|
1335
1446
|
spawnId,
|
|
1336
1447
|
startTime: spawnStartTime,
|
|
1337
1448
|
success: false,
|
|
1449
|
+
transport,
|
|
1338
1450
|
});
|
|
1339
1451
|
throw spawnError;
|
|
1340
1452
|
}
|
|
@@ -1480,15 +1592,21 @@ Write output to: ${outputPath}`;
|
|
|
1480
1592
|
// PIPELINE MODE: Use single worker for sequential story processing
|
|
1481
1593
|
// Stories are processed in order, one at a time as they become available
|
|
1482
1594
|
const workerCount = 1;
|
|
1595
|
+
// Instantiate lock-aware dispatcher shared by all workers
|
|
1596
|
+
const sessionId = randomUUID();
|
|
1597
|
+
const agentName = config.devAgent ?? 'pirlo';
|
|
1598
|
+
const dispatcher = new LockedStoryDispatcher(config.lockingEnabled ? (this.lockService ?? null) : null, this.logger, agentName, sessionId);
|
|
1483
1599
|
this.logger.info({
|
|
1484
1600
|
interval: config.storyInterval,
|
|
1601
|
+
lockingEnabled: !!config.lockingEnabled,
|
|
1485
1602
|
mode: 'sequential',
|
|
1486
1603
|
reviewEnabled: !!reviewQueue,
|
|
1604
|
+
sessionId,
|
|
1487
1605
|
workerCount,
|
|
1488
1606
|
}, 'Starting pipelined development phase (sequential processing)');
|
|
1489
1607
|
try {
|
|
1490
1608
|
// Create single worker for sequential processing, passing reviewQueue if review is enabled
|
|
1491
|
-
const workers = Array.from({ length: workerCount }, (_, i) => this.devWorker(i, queue, config, reviewQueue));
|
|
1609
|
+
const workers = Array.from({ length: workerCount }, (_, i) => this.devWorker(i, queue, config, dispatcher, reviewQueue));
|
|
1492
1610
|
// Wait for worker to complete
|
|
1493
1611
|
const workerResults = await Promise.all(workers);
|
|
1494
1612
|
// Close ReviewQueue after all dev workers finish (Task 3: close/drain semantics)
|
|
@@ -1498,23 +1616,34 @@ Write output to: ${outputPath}`;
|
|
|
1498
1616
|
}
|
|
1499
1617
|
// Aggregate results from all workers
|
|
1500
1618
|
let totalSuccess = 0;
|
|
1619
|
+
let totalSkipped = 0;
|
|
1620
|
+
let totalProcessed = 0;
|
|
1621
|
+
let totalFailed = 0;
|
|
1501
1622
|
const allFailures = [];
|
|
1502
1623
|
for (const result of workerResults) {
|
|
1503
1624
|
totalSuccess += result.success;
|
|
1625
|
+
totalSkipped += result.skipped;
|
|
1626
|
+
totalProcessed += result.processed;
|
|
1627
|
+
totalFailed += result.failed;
|
|
1504
1628
|
allFailures.push(...result.failures);
|
|
1505
1629
|
}
|
|
1506
1630
|
const duration = Date.now() - startTime;
|
|
1507
1631
|
this.logger.info({
|
|
1508
1632
|
duration,
|
|
1509
|
-
|
|
1510
|
-
|
|
1633
|
+
totalFailed,
|
|
1634
|
+
totalProcessed,
|
|
1635
|
+
totalSkipped,
|
|
1511
1636
|
workerCount,
|
|
1512
|
-
}, '
|
|
1637
|
+
}, 'Pipeline dev phase complete');
|
|
1638
|
+
// Warn when all stories were skipped (locked by other sessions)
|
|
1639
|
+
if (totalProcessed === 0 && totalSkipped > 0) {
|
|
1640
|
+
this.logger.warn({ totalSkipped }, 'All stories locked by other sessions — this pipeline had no work to do');
|
|
1641
|
+
}
|
|
1513
1642
|
this.invokeCallback('onPhaseComplete', this.callbacks?.onPhaseComplete, {
|
|
1514
1643
|
duration,
|
|
1515
1644
|
endTime: Date.now(),
|
|
1516
1645
|
failureCount: allFailures.length,
|
|
1517
|
-
itemCount: totalSuccess + allFailures.length,
|
|
1646
|
+
itemCount: totalSuccess + allFailures.length + totalSkipped,
|
|
1518
1647
|
phaseName: 'dev',
|
|
1519
1648
|
startTime,
|
|
1520
1649
|
successCount: totalSuccess,
|
|
@@ -1524,6 +1653,7 @@ Write output to: ${outputPath}`;
|
|
|
1524
1653
|
failures: allFailures,
|
|
1525
1654
|
phaseName: 'dev',
|
|
1526
1655
|
skipped: false,
|
|
1656
|
+
skippedCount: totalSkipped,
|
|
1527
1657
|
success: totalSuccess,
|
|
1528
1658
|
};
|
|
1529
1659
|
}
|
|
@@ -2202,6 +2332,9 @@ Write output to: ${outputPath}`;
|
|
|
2202
2332
|
const results = await storyBatchProcessor.processBatch(storiesToCreate, async (story) => {
|
|
2203
2333
|
const spawnStartTime = Date.now();
|
|
2204
2334
|
const spawnId = `story-${story.fullNumber}`;
|
|
2335
|
+
// Select runner via factory (Channel) or fallback (subprocess)
|
|
2336
|
+
const runner = this.getRunner('sm', config);
|
|
2337
|
+
const transport = runner === this.agentRunner ? 'subprocess' : 'channel';
|
|
2205
2338
|
// Fire onSpawnStart so the reporter shows which story is being created
|
|
2206
2339
|
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
2207
2340
|
agentType: 'sm',
|
|
@@ -2210,6 +2343,7 @@ Write output to: ${outputPath}`;
|
|
|
2210
2343
|
phaseName: 'story',
|
|
2211
2344
|
spawnId,
|
|
2212
2345
|
startTime: spawnStartTime,
|
|
2346
|
+
transport,
|
|
2213
2347
|
});
|
|
2214
2348
|
try {
|
|
2215
2349
|
// Generate story file path with prefix
|
|
@@ -2233,6 +2367,7 @@ Write output to: ${outputPath}`;
|
|
|
2233
2367
|
spawnId,
|
|
2234
2368
|
startTime: spawnStartTime,
|
|
2235
2369
|
success: true,
|
|
2370
|
+
transport,
|
|
2236
2371
|
});
|
|
2237
2372
|
return storyFilePath;
|
|
2238
2373
|
}
|
|
@@ -2272,10 +2407,13 @@ Write output to: ${outputPath}`;
|
|
|
2272
2407
|
}, 'Claude Prompt (Story)');
|
|
2273
2408
|
}
|
|
2274
2409
|
// Step 4: Run Claude agent to populate content sections
|
|
2275
|
-
const
|
|
2410
|
+
const storyStreamCbs = this.getStreamCallbacks(config, `story ${story.fullNumber}`);
|
|
2411
|
+
const result = await runAgentWithRetry(runner, prompt, {
|
|
2276
2412
|
agentType: 'sm',
|
|
2277
2413
|
cwd: config.cwd,
|
|
2414
|
+
...storyStreamCbs,
|
|
2278
2415
|
references: config.references,
|
|
2416
|
+
sessionName: this.resolveSessionName(config, 'story', story.fullNumber),
|
|
2279
2417
|
timeout: config.timeout ?? 2_700_000,
|
|
2280
2418
|
}, {
|
|
2281
2419
|
backoffMs: config.retryBackoffMs,
|
|
@@ -2338,6 +2476,7 @@ Write output to: ${outputPath}`;
|
|
|
2338
2476
|
spawnId,
|
|
2339
2477
|
startTime: spawnStartTime,
|
|
2340
2478
|
success: true,
|
|
2479
|
+
transport,
|
|
2341
2480
|
});
|
|
2342
2481
|
return storyFilePath;
|
|
2343
2482
|
}
|
|
@@ -2354,6 +2493,7 @@ Write output to: ${outputPath}`;
|
|
|
2354
2493
|
spawnId,
|
|
2355
2494
|
startTime: spawnStartTime,
|
|
2356
2495
|
success: false,
|
|
2496
|
+
transport,
|
|
2357
2497
|
});
|
|
2358
2498
|
throw spawnError;
|
|
2359
2499
|
}
|
|
@@ -2530,6 +2670,9 @@ Write output to: ${outputPath}`;
|
|
|
2530
2670
|
const results = await storyBatchProcessor.processBatch(storiesToCreate, async (story) => {
|
|
2531
2671
|
const spawnStartTime = Date.now();
|
|
2532
2672
|
const spawnId = `story-${story.fullNumber}`;
|
|
2673
|
+
// Select runner via factory (Channel) or fallback (subprocess)
|
|
2674
|
+
const runner = this.getRunner('sm', config);
|
|
2675
|
+
const transport = runner === this.agentRunner ? 'subprocess' : 'channel';
|
|
2533
2676
|
// Fire onSpawnStart so the reporter shows which story is being created
|
|
2534
2677
|
this.invokeCallback('onSpawnStart', this.callbacks?.onSpawnStart, {
|
|
2535
2678
|
agentType: 'sm',
|
|
@@ -2538,6 +2681,7 @@ Write output to: ${outputPath}`;
|
|
|
2538
2681
|
phaseName: 'story',
|
|
2539
2682
|
spawnId,
|
|
2540
2683
|
startTime: spawnStartTime,
|
|
2684
|
+
transport,
|
|
2541
2685
|
});
|
|
2542
2686
|
try {
|
|
2543
2687
|
// Generate story file path with prefix
|
|
@@ -2561,6 +2705,7 @@ Write output to: ${outputPath}`;
|
|
|
2561
2705
|
spawnId,
|
|
2562
2706
|
startTime: spawnStartTime,
|
|
2563
2707
|
success: true,
|
|
2708
|
+
transport,
|
|
2564
2709
|
});
|
|
2565
2710
|
return storyFilePath;
|
|
2566
2711
|
}
|
|
@@ -2599,10 +2744,13 @@ Write output to: ${outputPath}`;
|
|
|
2599
2744
|
}, 'Claude Prompt (Story)');
|
|
2600
2745
|
}
|
|
2601
2746
|
// Step 4: Run Claude agent to populate content sections
|
|
2602
|
-
const
|
|
2747
|
+
const storyStreamCbs = this.getStreamCallbacks(config, `story ${story.fullNumber}`);
|
|
2748
|
+
const result = await runAgentWithRetry(runner, prompt, {
|
|
2603
2749
|
agentType: 'sm',
|
|
2604
2750
|
cwd: config.cwd,
|
|
2751
|
+
...storyStreamCbs,
|
|
2605
2752
|
references: config.references,
|
|
2753
|
+
sessionName: this.resolveSessionName(config, 'story', story.fullNumber),
|
|
2606
2754
|
timeout: config.timeout ?? 2_700_000,
|
|
2607
2755
|
}, {
|
|
2608
2756
|
backoffMs: config.retryBackoffMs,
|
|
@@ -2665,6 +2813,7 @@ Write output to: ${outputPath}`;
|
|
|
2665
2813
|
spawnId,
|
|
2666
2814
|
startTime: spawnStartTime,
|
|
2667
2815
|
success: true,
|
|
2816
|
+
transport,
|
|
2668
2817
|
});
|
|
2669
2818
|
return storyFilePath;
|
|
2670
2819
|
}
|
|
@@ -2681,6 +2830,7 @@ Write output to: ${outputPath}`;
|
|
|
2681
2830
|
spawnId,
|
|
2682
2831
|
startTime: spawnStartTime,
|
|
2683
2832
|
success: false,
|
|
2833
|
+
transport,
|
|
2684
2834
|
});
|
|
2685
2835
|
throw spawnError;
|
|
2686
2836
|
}
|
|
@@ -55,6 +55,7 @@ export class AIReviewScanner {
|
|
|
55
55
|
agentType: 'qa',
|
|
56
56
|
references,
|
|
57
57
|
timeout: this.timeout,
|
|
58
|
+
sessionName: context.sessionName,
|
|
58
59
|
});
|
|
59
60
|
this.logger.info({ duration: result.duration, exitCode: result.exitCode, storyId: context.storyId, success: result.success }, 'AI review scan completed');
|
|
60
61
|
return this.parseAgentOutput(result);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* and pipelined review execution paths. The concrete implementation
|
|
10
10
|
* (DefaultReviewPhaseExecutor) composes SelfHealLoop and TechDebtTracker.
|
|
11
11
|
*/
|
|
12
|
+
import { derivePrefixForSessionName } from '../../utils/session-naming.js';
|
|
12
13
|
import { Severity } from './types.js';
|
|
13
14
|
/** Default severity threshold that blocks the pipeline */
|
|
14
15
|
const DEFAULT_BLOCK_ON = Severity.HIGH;
|
|
@@ -49,11 +50,13 @@ export class DefaultReviewPhaseExecutor {
|
|
|
49
50
|
const storyFile = story.filePath ?? '';
|
|
50
51
|
const blockOn = config.reviewBlockOn ?? DEFAULT_BLOCK_ON;
|
|
51
52
|
this.logger.info({ storyId, blockOn }, 'Reviewing story');
|
|
53
|
+
const prefix = config.inputPath ? derivePrefixForSessionName(config.inputPath) : undefined;
|
|
52
54
|
const context = {
|
|
53
55
|
baseBranch: 'main',
|
|
54
56
|
changedFiles: [],
|
|
55
57
|
projectRoot: config.cwd ?? process.cwd(),
|
|
56
58
|
referenceFiles: config.references ?? [],
|
|
59
|
+
sessionName: prefix ? `${prefix}-review-${storyId}` : undefined,
|
|
57
60
|
storyFile,
|
|
58
61
|
storyId,
|
|
59
62
|
};
|
|
@@ -120,6 +120,7 @@ export class SelfHealLoop {
|
|
|
120
120
|
const result = await this.agentRunner.runAgent(prompt, {
|
|
121
121
|
agentType: 'dev',
|
|
122
122
|
references: [context.storyFile, ...context.changedFiles],
|
|
123
|
+
sessionName: context.sessionName?.replace('-review-', '-fix-'),
|
|
123
124
|
timeout: this.config.fixTimeout,
|
|
124
125
|
});
|
|
125
126
|
return result;
|
|
@@ -40,6 +40,8 @@ export interface ReviewContext {
|
|
|
40
40
|
storyFile: string;
|
|
41
41
|
/** Story identifier (e.g., "PROJ-story-1.001") */
|
|
42
42
|
storyId: string;
|
|
43
|
+
/** Optional Claude session display name — forwarded to --name flag when set */
|
|
44
|
+
sessionName?: string;
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* Raw output from a scanner before classification
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -13,10 +13,12 @@ export declare const ERR_PARSER = "ERR_PARSER";
|
|
|
13
13
|
export declare const ERR_FILE_SYSTEM = "ERR_FILE_SYSTEM";
|
|
14
14
|
export declare const ERR_AGENT = "ERR_AGENT";
|
|
15
15
|
export declare const ERR_CONFIG = "ERR_CONFIG";
|
|
16
|
+
export declare const ERR_LOCK = "ERR_LOCK";
|
|
17
|
+
export declare const ERR_PUSH_REJECTED = "ERR_PUSH_REJECTED";
|
|
16
18
|
/**
|
|
17
19
|
* Type for error codes
|
|
18
20
|
*/
|
|
19
|
-
export type ErrorCode = typeof ERR_AGENT | typeof ERR_CONFIG | typeof ERR_FILE_SYSTEM | typeof ERR_PARSER | typeof ERR_VALIDATION;
|
|
21
|
+
export type ErrorCode = typeof ERR_AGENT | typeof ERR_CONFIG | typeof ERR_FILE_SYSTEM | typeof ERR_LOCK | typeof ERR_PARSER | typeof ERR_PUSH_REJECTED | typeof ERR_VALIDATION;
|
|
20
22
|
/**
|
|
21
23
|
* Context object for additional error information
|
|
22
24
|
*/
|
|
@@ -151,6 +153,20 @@ export declare class ParserError extends BaseError {
|
|
|
151
153
|
*/
|
|
152
154
|
constructor(message: string, context?: ErrorContext, suggestion?: string);
|
|
153
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Push rejected error for git push failures
|
|
158
|
+
*
|
|
159
|
+
* Used when a git push is rejected due to unexpected errors (not non-fast-forward).
|
|
160
|
+
*/
|
|
161
|
+
export declare class PushRejectedError extends BaseError {
|
|
162
|
+
/**
|
|
163
|
+
* Create a new PushRejectedError
|
|
164
|
+
*
|
|
165
|
+
* @param message - Human-readable error message
|
|
166
|
+
* @param stderr - stderr output from the git push command
|
|
167
|
+
*/
|
|
168
|
+
constructor(message: string, stderr: string);
|
|
169
|
+
}
|
|
154
170
|
/**
|
|
155
171
|
* Handle fatal errors by logging and exiting the process
|
|
156
172
|
*
|
package/dist/utils/errors.js
CHANGED
|
@@ -12,6 +12,8 @@ export const ERR_PARSER = 'ERR_PARSER';
|
|
|
12
12
|
export const ERR_FILE_SYSTEM = 'ERR_FILE_SYSTEM';
|
|
13
13
|
export const ERR_AGENT = 'ERR_AGENT';
|
|
14
14
|
export const ERR_CONFIG = 'ERR_CONFIG';
|
|
15
|
+
export const ERR_LOCK = 'ERR_LOCK';
|
|
16
|
+
export const ERR_PUSH_REJECTED = 'ERR_PUSH_REJECTED';
|
|
15
17
|
/**
|
|
16
18
|
* Base error class with error codes and context
|
|
17
19
|
*
|
|
@@ -196,6 +198,22 @@ export class ParserError extends BaseError {
|
|
|
196
198
|
super(message, ERR_PARSER, context, suggestion || defaultSuggestion);
|
|
197
199
|
}
|
|
198
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Push rejected error for git push failures
|
|
203
|
+
*
|
|
204
|
+
* Used when a git push is rejected due to unexpected errors (not non-fast-forward).
|
|
205
|
+
*/
|
|
206
|
+
export class PushRejectedError extends BaseError {
|
|
207
|
+
/**
|
|
208
|
+
* Create a new PushRejectedError
|
|
209
|
+
*
|
|
210
|
+
* @param message - Human-readable error message
|
|
211
|
+
* @param stderr - stderr output from the git push command
|
|
212
|
+
*/
|
|
213
|
+
constructor(message, stderr) {
|
|
214
|
+
super(message, ERR_PUSH_REJECTED, { stderr }, 'A git push was rejected. This may indicate a concurrent operation. Pull and retry.');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
199
217
|
/**
|
|
200
218
|
* Handle fatal errors by logging and exiting the process
|
|
201
219
|
*
|