@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.
Files changed (72) hide show
  1. package/dist/commands/epics/create.d.ts +1 -0
  2. package/dist/commands/lock/acquire.d.ts +54 -0
  3. package/dist/commands/lock/acquire.js +193 -0
  4. package/dist/commands/lock/cleanup.d.ts +38 -0
  5. package/dist/commands/lock/cleanup.js +148 -0
  6. package/dist/commands/lock/list.d.ts +31 -0
  7. package/dist/commands/lock/list.js +123 -0
  8. package/dist/commands/lock/release.d.ts +42 -0
  9. package/dist/commands/lock/release.js +134 -0
  10. package/dist/commands/lock/status.d.ts +34 -0
  11. package/dist/commands/lock/status.js +109 -0
  12. package/dist/commands/stories/create.d.ts +1 -0
  13. package/dist/commands/stories/develop.d.ts +4 -0
  14. package/dist/commands/stories/develop.js +55 -5
  15. package/dist/commands/stories/qa.d.ts +1 -0
  16. package/dist/commands/stories/qa.js +31 -0
  17. package/dist/commands/stories/review.d.ts +1 -0
  18. package/dist/commands/workflow.d.ts +11 -0
  19. package/dist/commands/workflow.js +120 -4
  20. package/dist/models/agent-options.d.ts +33 -0
  21. package/dist/models/agent-result.d.ts +10 -1
  22. package/dist/models/dispatch.d.ts +16 -0
  23. package/dist/models/dispatch.js +8 -0
  24. package/dist/models/index.d.ts +3 -0
  25. package/dist/models/index.js +2 -0
  26. package/dist/models/lock.d.ts +80 -0
  27. package/dist/models/lock.js +69 -0
  28. package/dist/models/phase-result.d.ts +8 -0
  29. package/dist/models/provider.js +1 -1
  30. package/dist/models/workflow-callbacks.d.ts +37 -0
  31. package/dist/models/workflow-config.d.ts +50 -0
  32. package/dist/services/agents/agent-runner-factory.d.ts +14 -15
  33. package/dist/services/agents/agent-runner-factory.js +56 -15
  34. package/dist/services/agents/channel-agent-runner.d.ts +76 -0
  35. package/dist/services/agents/channel-agent-runner.js +246 -0
  36. package/dist/services/agents/channel-session-manager.d.ts +119 -0
  37. package/dist/services/agents/channel-session-manager.js +250 -0
  38. package/dist/services/agents/claude-agent-runner.d.ts +9 -50
  39. package/dist/services/agents/claude-agent-runner.js +221 -199
  40. package/dist/services/agents/gemini-agent-runner.js +3 -0
  41. package/dist/services/agents/index.d.ts +1 -0
  42. package/dist/services/agents/index.js +1 -0
  43. package/dist/services/agents/opencode-agent-runner.js +3 -0
  44. package/dist/services/file-system/file-manager.d.ts +11 -0
  45. package/dist/services/file-system/file-manager.js +26 -0
  46. package/dist/services/git/git-ops.d.ts +58 -0
  47. package/dist/services/git/git-ops.js +73 -0
  48. package/dist/services/git/index.d.ts +3 -0
  49. package/dist/services/git/index.js +2 -0
  50. package/dist/services/git/push-conflict-handler.d.ts +32 -0
  51. package/dist/services/git/push-conflict-handler.js +84 -0
  52. package/dist/services/lock/git-backed-lock-service.d.ts +76 -0
  53. package/dist/services/lock/git-backed-lock-service.js +173 -0
  54. package/dist/services/lock/lock-cleanup.d.ts +49 -0
  55. package/dist/services/lock/lock-cleanup.js +85 -0
  56. package/dist/services/lock/lock-service.d.ts +143 -0
  57. package/dist/services/lock/lock-service.js +290 -0
  58. package/dist/services/orchestration/locked-story-dispatcher.d.ts +40 -0
  59. package/dist/services/orchestration/locked-story-dispatcher.js +84 -0
  60. package/dist/services/orchestration/workflow-orchestrator.d.ts +31 -0
  61. package/dist/services/orchestration/workflow-orchestrator.js +181 -31
  62. package/dist/services/review/ai-review-scanner.js +1 -0
  63. package/dist/services/review/review-phase-executor.js +3 -0
  64. package/dist/services/review/self-heal-loop.js +1 -0
  65. package/dist/services/review/types.d.ts +2 -0
  66. package/dist/utils/errors.d.ts +17 -1
  67. package/dist/utils/errors.js +18 -0
  68. package/dist/utils/session-naming.d.ts +23 -0
  69. package/dist/utils/session-naming.js +30 -0
  70. package/dist/utils/shared-flags.d.ts +1 -0
  71. package/dist/utils/shared-flags.js +5 -0
  72. 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
- try {
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
- continue;
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
- continue;
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 result = await runAgentWithRetry(this.agentRunner, prompt, {
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
- catch (error) {
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: error.message,
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
- failureCount: failures.length,
884
+ failed: failedCount,
885
+ processed: processedCount,
886
+ skipped: skippedCount,
794
887
  successCount,
795
- }, 'Worker completed');
796
- return { failures, success: successCount };
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 result = await runAgentWithRetry(this.agentRunner, prompt, {
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 result = await runAgentWithRetry(this.agentRunner, prompt, {
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
- failures: allFailures.length,
1510
- success: totalSuccess,
1633
+ totalFailed,
1634
+ totalProcessed,
1635
+ totalSkipped,
1511
1636
  workerCount,
1512
- }, 'Pipelined development phase completed');
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 result = await runAgentWithRetry(this.agentRunner, prompt, {
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 result = await runAgentWithRetry(this.agentRunner, prompt, {
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
@@ -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
  *
@@ -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
  *