@hyperdrive.bot/bmad-workflow 1.0.2

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1017 -0
  3. package/bin/dev +5 -0
  4. package/bin/dev.cmd +3 -0
  5. package/bin/dev.js +5 -0
  6. package/bin/run +5 -0
  7. package/bin/run.cmd +3 -0
  8. package/bin/run.js +5 -0
  9. package/dist/commands/config/show.d.ts +34 -0
  10. package/dist/commands/config/show.js +108 -0
  11. package/dist/commands/config/validate.d.ts +29 -0
  12. package/dist/commands/config/validate.js +131 -0
  13. package/dist/commands/decompose.d.ts +79 -0
  14. package/dist/commands/decompose.js +327 -0
  15. package/dist/commands/demo.d.ts +18 -0
  16. package/dist/commands/demo.js +107 -0
  17. package/dist/commands/epics/create.d.ts +123 -0
  18. package/dist/commands/epics/create.js +459 -0
  19. package/dist/commands/epics/list.d.ts +120 -0
  20. package/dist/commands/epics/list.js +280 -0
  21. package/dist/commands/hello/index.d.ts +12 -0
  22. package/dist/commands/hello/index.js +34 -0
  23. package/dist/commands/hello/world.d.ts +8 -0
  24. package/dist/commands/hello/world.js +24 -0
  25. package/dist/commands/prd/fix.d.ts +39 -0
  26. package/dist/commands/prd/fix.js +140 -0
  27. package/dist/commands/prd/validate.d.ts +112 -0
  28. package/dist/commands/prd/validate.js +302 -0
  29. package/dist/commands/stories/create.d.ts +95 -0
  30. package/dist/commands/stories/create.js +431 -0
  31. package/dist/commands/stories/develop.d.ts +91 -0
  32. package/dist/commands/stories/develop.js +460 -0
  33. package/dist/commands/stories/list.d.ts +84 -0
  34. package/dist/commands/stories/list.js +291 -0
  35. package/dist/commands/stories/move.d.ts +66 -0
  36. package/dist/commands/stories/move.js +273 -0
  37. package/dist/commands/stories/qa.d.ts +99 -0
  38. package/dist/commands/stories/qa.js +530 -0
  39. package/dist/commands/workflow.d.ts +97 -0
  40. package/dist/commands/workflow.js +390 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +1 -0
  43. package/dist/models/agent-options.d.ts +50 -0
  44. package/dist/models/agent-options.js +1 -0
  45. package/dist/models/agent-result.d.ts +29 -0
  46. package/dist/models/agent-result.js +1 -0
  47. package/dist/models/index.d.ts +10 -0
  48. package/dist/models/index.js +10 -0
  49. package/dist/models/phase-result.d.ts +65 -0
  50. package/dist/models/phase-result.js +7 -0
  51. package/dist/models/provider.d.ts +28 -0
  52. package/dist/models/provider.js +18 -0
  53. package/dist/models/story.d.ts +154 -0
  54. package/dist/models/story.js +18 -0
  55. package/dist/models/workflow-config.d.ts +148 -0
  56. package/dist/models/workflow-config.js +1 -0
  57. package/dist/models/workflow-result.d.ts +164 -0
  58. package/dist/models/workflow-result.js +7 -0
  59. package/dist/services/agents/agent-runner-factory.d.ts +31 -0
  60. package/dist/services/agents/agent-runner-factory.js +44 -0
  61. package/dist/services/agents/agent-runner.d.ts +46 -0
  62. package/dist/services/agents/agent-runner.js +29 -0
  63. package/dist/services/agents/claude-agent-runner.d.ts +81 -0
  64. package/dist/services/agents/claude-agent-runner.js +332 -0
  65. package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
  66. package/dist/services/agents/gemini-agent-runner.js +350 -0
  67. package/dist/services/agents/index.d.ts +7 -0
  68. package/dist/services/agents/index.js +7 -0
  69. package/dist/services/file-system/file-manager.d.ts +110 -0
  70. package/dist/services/file-system/file-manager.js +223 -0
  71. package/dist/services/file-system/glob-matcher.d.ts +75 -0
  72. package/dist/services/file-system/glob-matcher.js +126 -0
  73. package/dist/services/file-system/path-resolver.d.ts +183 -0
  74. package/dist/services/file-system/path-resolver.js +400 -0
  75. package/dist/services/logging/workflow-logger.d.ts +232 -0
  76. package/dist/services/logging/workflow-logger.js +552 -0
  77. package/dist/services/orchestration/batch-processor.d.ts +113 -0
  78. package/dist/services/orchestration/batch-processor.js +187 -0
  79. package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
  80. package/dist/services/orchestration/dependency-graph-executor.js +447 -0
  81. package/dist/services/orchestration/index.d.ts +10 -0
  82. package/dist/services/orchestration/index.js +8 -0
  83. package/dist/services/orchestration/input-detector.d.ts +125 -0
  84. package/dist/services/orchestration/input-detector.js +381 -0
  85. package/dist/services/orchestration/story-queue.d.ts +94 -0
  86. package/dist/services/orchestration/story-queue.js +170 -0
  87. package/dist/services/orchestration/story-type-detector.d.ts +80 -0
  88. package/dist/services/orchestration/story-type-detector.js +258 -0
  89. package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
  90. package/dist/services/orchestration/task-decomposition-service.js +607 -0
  91. package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
  92. package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
  93. package/dist/services/parsers/epic-parser.d.ts +117 -0
  94. package/dist/services/parsers/epic-parser.js +264 -0
  95. package/dist/services/parsers/prd-fixer.d.ts +86 -0
  96. package/dist/services/parsers/prd-fixer.js +194 -0
  97. package/dist/services/parsers/prd-parser.d.ts +123 -0
  98. package/dist/services/parsers/prd-parser.js +286 -0
  99. package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
  100. package/dist/services/parsers/standalone-story-parser.js +255 -0
  101. package/dist/services/parsers/story-parser-factory.d.ts +81 -0
  102. package/dist/services/parsers/story-parser-factory.js +108 -0
  103. package/dist/services/parsers/story-parser.d.ts +122 -0
  104. package/dist/services/parsers/story-parser.js +262 -0
  105. package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
  106. package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
  107. package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
  108. package/dist/services/scaffolding/file-scaffolder.js +314 -0
  109. package/dist/services/validation/config-validator.d.ts +88 -0
  110. package/dist/services/validation/config-validator.js +167 -0
  111. package/dist/types/task-graph.d.ts +142 -0
  112. package/dist/types/task-graph.js +5 -0
  113. package/dist/utils/colors.d.ts +49 -0
  114. package/dist/utils/colors.js +50 -0
  115. package/dist/utils/error-formatter.d.ts +64 -0
  116. package/dist/utils/error-formatter.js +279 -0
  117. package/dist/utils/errors.d.ts +170 -0
  118. package/dist/utils/errors.js +233 -0
  119. package/dist/utils/formatters.d.ts +84 -0
  120. package/dist/utils/formatters.js +162 -0
  121. package/dist/utils/logger.d.ts +63 -0
  122. package/dist/utils/logger.js +78 -0
  123. package/dist/utils/progress.d.ts +104 -0
  124. package/dist/utils/progress.js +161 -0
  125. package/dist/utils/retry.d.ts +114 -0
  126. package/dist/utils/retry.js +160 -0
  127. package/dist/utils/shared-flags.d.ts +28 -0
  128. package/dist/utils/shared-flags.js +43 -0
  129. package/package.json +119 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Batch Processor Service
3
+ *
4
+ * Handles parallel execution of tasks with configurable concurrency,
5
+ * intervals between batches, and partial failure handling.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const logger = createLogger({ namespace: 'batch' })
10
+ * const processor = new BatchProcessor(5, 1000, logger)
11
+ * const results = await processor.processBatch(items, async (item) => {
12
+ * return await someAsyncOperation(item)
13
+ * }, (info) => {
14
+ * console.log(`Processing batch ${info.currentBatch}/${info.totalBatches}`)
15
+ * })
16
+ * ```
17
+ */
18
+ import { ValidationError } from '../../utils/errors.js';
19
+ /**
20
+ * BatchProcessor - Handles parallel batch processing with configurable concurrency
21
+ *
22
+ * Processes items in batches with configurable concurrency limits and intervals
23
+ * between batches. Continues processing even if individual items fail, collecting
24
+ * both successes and failures.
25
+ *
26
+ * @example
27
+ * const processor = new BatchProcessor(5, 1000, logger)
28
+ * const results = await processor.processBatch(items, async (item) => {
29
+ * return await processItem(item)
30
+ * })
31
+ */
32
+ export class BatchProcessor {
33
+ intervalMs;
34
+ logger;
35
+ maxConcurrency;
36
+ /**
37
+ * Create a new BatchProcessor instance
38
+ *
39
+ * @param maxConcurrency - Maximum number of concurrent operations (must be positive)
40
+ * @param intervalMs - Milliseconds to wait between batches (must be non-negative)
41
+ * @param logger - Logger instance for structured logging
42
+ * @throws {ValidationError} If maxConcurrency is not positive or intervalMs is negative
43
+ */
44
+ constructor(maxConcurrency, intervalMs, logger) {
45
+ // Validate parameters
46
+ if (!Number.isInteger(maxConcurrency) || maxConcurrency <= 0) {
47
+ const error = new ValidationError('maxConcurrency must be a positive integer');
48
+ logger.error({ maxConcurrency }, error.message);
49
+ throw error;
50
+ }
51
+ if (typeof intervalMs !== 'number' || intervalMs < 0) {
52
+ const error = new ValidationError('intervalMs must be a non-negative number');
53
+ logger.error({ intervalMs }, error.message);
54
+ throw error;
55
+ }
56
+ this.maxConcurrency = maxConcurrency;
57
+ this.intervalMs = intervalMs;
58
+ this.logger = logger;
59
+ this.logger.info({ intervalMs: this.intervalMs, maxConcurrency: this.maxConcurrency }, 'BatchProcessor initialized');
60
+ }
61
+ /**
62
+ * Process items in parallel batches with partial failure handling
63
+ *
64
+ * Divides items into batches of size maxConcurrency, processes each batch
65
+ * with Promise.allSettled, waits intervalMs between batches, and collects
66
+ * all results including both successes and failures.
67
+ *
68
+ * @param items - Array of items to process
69
+ * @param processor - Async function to process each item
70
+ * @param onProgress - Optional callback for progress updates
71
+ * @returns Array of BatchResult objects in original item order
72
+ *
73
+ * @example
74
+ * const results = await processor.processBatch(items, async (item) => {
75
+ * return await processItem(item)
76
+ * }, (info) => {
77
+ * console.log(`Batch ${info.currentBatch}/${info.totalBatches}`)
78
+ * })
79
+ */
80
+ async processBatch(items, processor, onProgress) {
81
+ const results = [];
82
+ // Handle empty array edge case
83
+ if (items.length === 0) {
84
+ this.logger.info('No items to process');
85
+ return results;
86
+ }
87
+ // Divide items into batches
88
+ const batches = this.splitIntoBatches(items, this.maxConcurrency);
89
+ const totalBatches = batches.length;
90
+ this.logger.info({ batchSize: this.maxConcurrency, totalBatches, totalItems: items.length }, 'Starting batch processing');
91
+ let completedItems = 0;
92
+ // Process batches sequentially to control concurrency and rate limiting
93
+ for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
94
+ const batch = batches[batchIndex];
95
+ const currentBatch = batchIndex + 1;
96
+ this.logger.debug({ currentBatch, itemsInBatch: batch.length, totalBatches }, 'Processing batch');
97
+ // Call progress callback at start of batch if provided
98
+ if (onProgress) {
99
+ try {
100
+ onProgress({
101
+ completedItems,
102
+ currentBatch,
103
+ currentItem: batchIndex * this.maxConcurrency + 1,
104
+ totalBatches,
105
+ totalItems: items.length,
106
+ });
107
+ }
108
+ catch (error) {
109
+ this.logger.error({ error }, 'Progress callback threw error');
110
+ }
111
+ }
112
+ // Process batch in parallel with Promise.allSettled to handle partial failures
113
+ const batchPromises = batch.map((item, itemIndex) => {
114
+ const globalIndex = batchIndex * this.maxConcurrency + itemIndex;
115
+ return processor(item)
116
+ .then((result) => {
117
+ this.logger.debug({ index: globalIndex }, 'Item processed successfully');
118
+ return { result, success: true };
119
+ })
120
+ .catch((error) => {
121
+ const err = error;
122
+ this.logger.error({ error: err.message, index: globalIndex, itemIndex }, 'Item processing failed');
123
+ return { error: err, success: false };
124
+ });
125
+ });
126
+ // eslint-disable-next-line no-await-in-loop -- batches processed sequentially by design
127
+ const batchResults = await Promise.all(batchPromises);
128
+ // Collect results
129
+ for (const result of batchResults) {
130
+ results.push(result);
131
+ if (result.success) {
132
+ completedItems++;
133
+ }
134
+ // Call progress callback after each item completes
135
+ if (onProgress) {
136
+ try {
137
+ onProgress({
138
+ completedItems,
139
+ currentBatch,
140
+ currentItem: results.length,
141
+ totalBatches,
142
+ totalItems: items.length,
143
+ });
144
+ }
145
+ catch (error) {
146
+ this.logger.error({ error }, 'Progress callback threw error');
147
+ }
148
+ }
149
+ }
150
+ // Wait between batches (except after last batch) to control rate limiting
151
+ if (batchIndex < batches.length - 1 && this.intervalMs > 0) {
152
+ this.logger.debug({ intervalMs: this.intervalMs }, 'Waiting between batches');
153
+ // eslint-disable-next-line no-await-in-loop -- intentional delay between batches for rate limiting
154
+ await this.delay(this.intervalMs);
155
+ }
156
+ }
157
+ const successCount = results.filter((r) => r.success).length;
158
+ const errorCount = results.filter((r) => !r.success).length;
159
+ this.logger.info({ errorCount, successCount }, 'Batch processing complete');
160
+ return results;
161
+ }
162
+ /**
163
+ * Delay execution for specified milliseconds
164
+ *
165
+ * @param ms - Milliseconds to wait
166
+ * @returns Promise that resolves after delay
167
+ */
168
+ async delay(ms) {
169
+ return new Promise((resolve) => {
170
+ setTimeout(resolve, ms);
171
+ });
172
+ }
173
+ /**
174
+ * Split array into batches of specified size
175
+ *
176
+ * @param items - Array of items to split
177
+ * @param batchSize - Maximum items per batch
178
+ * @returns Array of batches
179
+ */
180
+ splitIntoBatches(items, batchSize) {
181
+ const batches = [];
182
+ for (let i = 0; i < items.length; i += batchSize) {
183
+ batches.push(items.slice(i, i + batchSize));
184
+ }
185
+ return batches;
186
+ }
187
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * DependencyGraphExecutor
3
+ *
4
+ * Executes task graphs with dependency-aware parallel execution.
5
+ * Uses layer-based execution where each layer runs in parallel,
6
+ * but layers execute sequentially to respect dependencies.
7
+ */
8
+ import type pino from 'pino';
9
+ import type { GraphExecutionResult, TaskGraph } from '../../types/task-graph.js';
10
+ import type { AIProviderRunner } from '../agents/agent-runner.js';
11
+ import type { FileManager } from '../file-system/file-manager.js';
12
+ import type { BatchProcessor } from './batch-processor.js';
13
+ /**
14
+ * Progress callback for layer execution
15
+ */
16
+ export type LayerProgressCallback = (layerIndex: number, totalLayers: number, layerSize: number) => void;
17
+ /**
18
+ * Progress callback for task execution
19
+ */
20
+ export type TaskProgressCallback = (taskId: string, layerIndex: number, taskIndex: number, totalTasks: number) => void;
21
+ /**
22
+ * Executor for dependency graph task execution
23
+ */
24
+ export declare class DependencyGraphExecutor {
25
+ private readonly agentRunner;
26
+ private readonly batchProcessor;
27
+ private readonly cwd?;
28
+ private readonly fileManager;
29
+ private readonly logger;
30
+ private readonly taskGraph;
31
+ constructor(taskGraph: TaskGraph, agentRunner: AIProviderRunner, batchProcessor: BatchProcessor, fileManager: FileManager, logger: pino.Logger, cwd?: string);
32
+ /**
33
+ * Execute the entire task graph
34
+ *
35
+ * @param onLayerStart - Callback when a layer starts
36
+ * @param onTaskStart - Callback when a task starts
37
+ * @returns Execution result with success status and task results
38
+ */
39
+ execute(onLayerStart?: LayerProgressCallback, onTaskStart?: TaskProgressCallback): Promise<GraphExecutionResult>;
40
+ /**
41
+ * Build the full prompt for a task
42
+ */
43
+ private buildTaskPrompt;
44
+ /**
45
+ * Count how many target files were modified by comparing before/after content
46
+ */
47
+ private countModifiedFiles;
48
+ /**
49
+ * Execute a single task
50
+ */
51
+ private executeTask;
52
+ /**
53
+ * Scaffold a story file with pre-populated structure
54
+ */
55
+ private scaffoldStoryFile;
56
+ /**
57
+ * Check if a layer should be skipped due to failed dependencies
58
+ */
59
+ private shouldSkipLayer;
60
+ }
@@ -0,0 +1,447 @@
1
+ /**
2
+ * DependencyGraphExecutor
3
+ *
4
+ * Executes task graphs with dependency-aware parallel execution.
5
+ * Uses layer-based execution where each layer runs in parallel,
6
+ * but layers execute sequentially to respect dependencies.
7
+ */
8
+ /**
9
+ * Executor for dependency graph task execution
10
+ */
11
+ export class DependencyGraphExecutor {
12
+ agentRunner;
13
+ batchProcessor;
14
+ cwd;
15
+ fileManager;
16
+ logger;
17
+ taskGraph;
18
+ // eslint-disable-next-line max-params -- Constructor dependencies will be refactored to config object in future
19
+ constructor(taskGraph, agentRunner, batchProcessor, fileManager, logger, cwd) {
20
+ this.taskGraph = taskGraph;
21
+ this.agentRunner = agentRunner;
22
+ this.batchProcessor = batchProcessor;
23
+ this.fileManager = fileManager;
24
+ this.logger = logger;
25
+ this.cwd = cwd;
26
+ }
27
+ /**
28
+ * Execute the entire task graph
29
+ *
30
+ * @param onLayerStart - Callback when a layer starts
31
+ * @param onTaskStart - Callback when a task starts
32
+ * @returns Execution result with success status and task results
33
+ */
34
+ async execute(onLayerStart, onTaskStart) {
35
+ this.logger.info({
36
+ goal: this.taskGraph.goal,
37
+ layers: this.taskGraph.metadata.executionLayers.length,
38
+ totalTasks: this.taskGraph.metadata.totalTasks,
39
+ }, 'Starting task graph execution');
40
+ const startTime = Date.now();
41
+ const taskResults = [];
42
+ const layerResults = [];
43
+ const taskMap = new Map(this.taskGraph.tasks.map((t) => [t.id, t]));
44
+ let completedTasks = 0;
45
+ let failedTasks = 0;
46
+ let skippedTasks = 0;
47
+ // Execute layer by layer - layers must be processed sequentially to respect dependencies
48
+ for (let layerIndex = 0; layerIndex < this.taskGraph.metadata.executionLayers.length; layerIndex++) {
49
+ const layer = this.taskGraph.metadata.executionLayers[layerIndex];
50
+ const layerStartTime = Date.now();
51
+ this.logger.info({
52
+ layerIndex,
53
+ layerSize: layer.length,
54
+ totalLayers: this.taskGraph.metadata.executionLayers.length,
55
+ }, `Starting layer ${layerIndex + 1}/${this.taskGraph.metadata.executionLayers.length}`);
56
+ // Notify layer start
57
+ if (onLayerStart) {
58
+ onLayerStart(layerIndex, this.taskGraph.metadata.executionLayers.length, layer.length);
59
+ }
60
+ // Check if any dependencies failed (if so, skip this layer)
61
+ const shouldSkipLayer = this.shouldSkipLayer(layer, taskMap, taskResults);
62
+ if (shouldSkipLayer) {
63
+ this.logger.warn({ layerIndex, layerSize: layer.length }, 'Skipping layer due to failed dependencies');
64
+ // Mark all tasks in this layer as skipped
65
+ for (const taskId of layer) {
66
+ const task = taskMap.get(taskId);
67
+ if (!task)
68
+ continue;
69
+ const skippedResult = {
70
+ duration: 0,
71
+ errors: 'Skipped due to failed dependencies',
72
+ exitCode: -1,
73
+ output: '',
74
+ success: false,
75
+ taskId,
76
+ };
77
+ taskResults.push(skippedResult);
78
+ skippedTasks++;
79
+ }
80
+ layerResults.push({
81
+ duration: Date.now() - layerStartTime,
82
+ layerIndex,
83
+ parallelTasksExecuted: 0,
84
+ success: false,
85
+ taskIds: layer,
86
+ });
87
+ continue;
88
+ }
89
+ // Execute all tasks in this layer in parallel
90
+ const layerTasks = layer.map((taskId) => taskMap.get(taskId)).filter((t) => t !== undefined);
91
+ // eslint-disable-next-line no-await-in-loop -- layers must complete sequentially
92
+ const results = await this.batchProcessor.processBatch(layerTasks, async (task) => {
93
+ // Notify task start
94
+ if (onTaskStart) {
95
+ const index = layerTasks.indexOf(task);
96
+ onTaskStart(task.id, layerIndex, index, layerTasks.length);
97
+ }
98
+ return this.executeTask(task);
99
+ }, (info) => {
100
+ this.logger.debug({ current: info.currentItem, layerIndex, total: info.totalItems }, `Layer ${layerIndex + 1} progress: ${info.currentItem}/${info.totalItems} tasks`);
101
+ });
102
+ // Process results
103
+ let layerSuccess = true;
104
+ for (const result of results) {
105
+ if (result.success && result.result) {
106
+ taskResults.push(result.result);
107
+ completedTasks++;
108
+ }
109
+ else {
110
+ // Task failed
111
+ const taskId = layerTasks[results.indexOf(result)]?.id ?? 'unknown';
112
+ const failedResult = {
113
+ duration: 0,
114
+ errors: result.error?.message ?? 'Unknown error',
115
+ exitCode: -1,
116
+ output: '',
117
+ success: false,
118
+ taskId,
119
+ };
120
+ taskResults.push(failedResult);
121
+ failedTasks++;
122
+ layerSuccess = false;
123
+ }
124
+ }
125
+ layerResults.push({
126
+ duration: Date.now() - layerStartTime,
127
+ layerIndex,
128
+ parallelTasksExecuted: layer.length,
129
+ success: layerSuccess,
130
+ taskIds: layer,
131
+ });
132
+ this.logger.info({
133
+ duration: Date.now() - layerStartTime,
134
+ layerIndex,
135
+ layerSuccess,
136
+ }, `Completed layer ${layerIndex + 1}/${this.taskGraph.metadata.executionLayers.length}`);
137
+ }
138
+ const totalDuration = Date.now() - startTime;
139
+ const result = {
140
+ completedTasks,
141
+ executionSummary: {
142
+ layerResults,
143
+ },
144
+ failedTasks,
145
+ skippedTasks,
146
+ success: failedTasks === 0 && skippedTasks === 0,
147
+ taskResults,
148
+ totalDuration,
149
+ totalTasks: this.taskGraph.metadata.totalTasks,
150
+ };
151
+ this.logger.info({
152
+ completedTasks,
153
+ failedTasks,
154
+ skippedTasks,
155
+ success: result.success,
156
+ totalDuration,
157
+ }, 'Task graph execution complete');
158
+ return result;
159
+ }
160
+ /**
161
+ * Build the full prompt for a task
162
+ */
163
+ buildTaskPrompt(task) {
164
+ let prompt = `${this.taskGraph.masterPrompt}
165
+
166
+ ---
167
+
168
+ ## TASK: ${task.title}
169
+
170
+ ${task.description}
171
+
172
+ ${task.prompt}
173
+
174
+ ## CRITICAL REQUIREMENT: MAKE ACTUAL CODE CHANGES
175
+
176
+ **YOU MUST MODIFY SOURCE CODE FILES.** Analysis-only completion is NOT acceptable.
177
+
178
+ Before marking this task complete, you MUST:
179
+ 1. Make actual changes to source files (not just the story/output file)
180
+ 2. Run verification (lint, tests, type-check) to prove changes work
181
+ 3. If issues are "already fixed", find OTHER improvements to make (refactoring, better types, cleanup)
182
+
183
+ DO NOT:
184
+ - Complete with "no changes needed" or "already correct"
185
+ - Only document findings without making fixes
186
+ - Write analysis without modifying source code
187
+
188
+ If you cannot find issues to fix, you MUST:
189
+ - Add eslint-disable comments with justifications for intentional warnings
190
+ - Refactor for better readability
191
+ - Improve type safety
192
+ - Add missing documentation
193
+ - Extract helper functions to reduce complexity
194
+
195
+ **ZERO source file modifications = TASK FAILURE**
196
+
197
+ `;
198
+ // Add target files if specified
199
+ if (task.targetFiles && task.targetFiles.length > 0) {
200
+ prompt += `\n## TARGET FILES\n`;
201
+ for (const file of task.targetFiles) {
202
+ prompt += `@${file}\n`;
203
+ }
204
+ prompt += '\n';
205
+ }
206
+ // Add output file instruction (conditional based on story format)
207
+ // eslint-disable-next-line unicorn/prefer-ternary
208
+ if (this.taskGraph.metadata.storyFormat) {
209
+ // Reference the scaffolded file
210
+ prompt += `## OUTPUT - STORY DOCUMENT UPDATE
211
+
212
+ Target story file: @${task.outputFile}
213
+
214
+ IMPORTANT: The target file has been pre-scaffolded with story structure and metadata.
215
+
216
+ Your task is to UPDATE the existing story file by:
217
+ - Changing Status from "Draft" to "Completed"
218
+ - Filling in sections marked with [AI Agent will populate]
219
+ - Checking off completed tasks/subtasks ([ ] → [x])
220
+ - Adding your findings to "## Dev Notes" section
221
+ - Adding completion entry to "## Change Log" table
222
+
223
+ DO NOT:
224
+ - Modify the story title or ID
225
+ - Change the original "## Story" user story format
226
+ - Change the "## Acceptance Criteria" list
227
+ - Remove any sections or change section headers
228
+ - Write conversational summaries outside the story structure
229
+
230
+ The file @${task.outputFile} already exists with the proper structure - just populate the empty sections.
231
+
232
+ `;
233
+ }
234
+ else {
235
+ prompt += `## OUTPUT
236
+ Write your results and any notes to: ${task.outputFile}
237
+
238
+ Use the file at the path above to document:
239
+ - What you did
240
+ - What worked
241
+ - Any issues encountered
242
+ - Next steps or recommendations
243
+
244
+ `;
245
+ }
246
+ return prompt;
247
+ }
248
+ /**
249
+ * Count how many target files were modified by comparing before/after content
250
+ */
251
+ async countModifiedFiles(targetFiles, contentsBefore) {
252
+ let filesModified = 0;
253
+ for (const filePath of targetFiles) {
254
+ try {
255
+ // eslint-disable-next-line no-await-in-loop -- must check each file sequentially for modification verification
256
+ const contentAfter = await this.fileManager.readFile(filePath);
257
+ const contentBefore = contentsBefore.get(filePath) ?? '';
258
+ if (contentAfter !== contentBefore) {
259
+ filesModified++;
260
+ }
261
+ }
262
+ catch {
263
+ // File read failed, skip
264
+ }
265
+ }
266
+ return filesModified;
267
+ }
268
+ /**
269
+ * Execute a single task
270
+ */
271
+ async executeTask(task) {
272
+ this.logger.info({ estimatedMinutes: task.estimatedMinutes, taskId: task.id, title: task.title }, 'Executing task');
273
+ const startTime = Date.now();
274
+ try {
275
+ // Ensure output directory exists
276
+ const outputDir = task.outputFile.slice(0, Math.max(0, task.outputFile.lastIndexOf('/')));
277
+ await this.fileManager.createDirectory(outputDir);
278
+ // If story format, write scaffolded file FIRST
279
+ if (this.taskGraph.metadata.storyFormat) {
280
+ const scaffoldedContent = this.scaffoldStoryFile(task);
281
+ await this.fileManager.writeFile(task.outputFile, scaffoldedContent);
282
+ this.logger.debug({ outputFile: task.outputFile, taskId: task.id }, 'Scaffolded story file created');
283
+ }
284
+ // Build full prompt with master prompt + task-specific prompt
285
+ const fullPrompt = this.buildTaskPrompt(task);
286
+ // Capture target file contents BEFORE execution for verification
287
+ const targetFileContentsBefore = new Map();
288
+ if (task.targetFiles && task.targetFiles.length > 0) {
289
+ for (const filePath of task.targetFiles) {
290
+ try {
291
+ // eslint-disable-next-line no-await-in-loop -- must capture each file sequentially for before/after comparison
292
+ const content = await this.fileManager.readFile(filePath);
293
+ targetFileContentsBefore.set(filePath, content);
294
+ }
295
+ catch {
296
+ // File might not exist yet, that's ok
297
+ targetFileContentsBefore.set(filePath, '');
298
+ }
299
+ }
300
+ }
301
+ // Map agent type to valid values
302
+ const validAgentTypes = ['analyst', 'architect', 'dev', 'pm', 'prd-fixer', 'quick-flow-solo-dev', 'sm', 'tea', 'tech-writer', 'ux-designer'];
303
+ const agentType = validAgentTypes.includes(task.agentType)
304
+ ? task.agentType
305
+ : 'dev';
306
+ // Execute agent
307
+ const result = await this.agentRunner.runAgent(fullPrompt, {
308
+ agentType,
309
+ references: task.targetFiles, // Pass target files as references
310
+ timeout: task.estimatedMinutes * 60 * 1000 * 1.5, // 1.5x estimated time as buffer
311
+ });
312
+ // Verify target files were actually modified
313
+ const filesModified = await this.countModifiedFiles(task.targetFiles ?? [], targetFileContentsBefore);
314
+ if (task.targetFiles && task.targetFiles.length > 0) {
315
+ if (filesModified === 0) {
316
+ this.logger.warn({
317
+ targetFiles: task.targetFiles.length,
318
+ taskId: task.id,
319
+ }, 'WARNING: Task completed but NO target source files were modified. This may indicate an analysis-only completion.');
320
+ }
321
+ else {
322
+ this.logger.info({
323
+ filesModified,
324
+ targetFiles: task.targetFiles.length,
325
+ taskId: task.id,
326
+ }, 'Target source files were modified');
327
+ }
328
+ }
329
+ // Write output to file (or verify it was updated in story format mode)
330
+ if (this.taskGraph.metadata.storyFormat) {
331
+ // In story format, Claude should have updated the file via @ reference
332
+ // Verify the file was actually modified
333
+ const updatedContent = await this.fileManager.readFile(task.outputFile);
334
+ const scaffoldedContent = this.scaffoldStoryFile(task);
335
+ if (updatedContent === scaffoldedContent) {
336
+ this.logger.warn({ taskId: task.id }, 'Story file was not updated by agent');
337
+ // Write agent output as fallback
338
+ await this.fileManager.writeFile(task.outputFile, result.output);
339
+ }
340
+ }
341
+ else {
342
+ // Regular mode: write agent output directly
343
+ await this.fileManager.writeFile(task.outputFile, result.output);
344
+ }
345
+ const duration = Date.now() - startTime;
346
+ this.logger.info({
347
+ duration,
348
+ exitCode: result.exitCode,
349
+ success: result.success,
350
+ taskId: task.id,
351
+ }, 'Task execution complete');
352
+ return {
353
+ duration,
354
+ errors: result.errors,
355
+ exitCode: result.exitCode,
356
+ output: result.output,
357
+ success: result.success,
358
+ taskId: task.id,
359
+ };
360
+ }
361
+ catch (error) {
362
+ const duration = Date.now() - startTime;
363
+ const err = error;
364
+ this.logger.error({
365
+ duration,
366
+ error: err.message,
367
+ taskId: task.id,
368
+ }, 'Task execution failed');
369
+ return {
370
+ duration,
371
+ errors: err.message,
372
+ exitCode: -1,
373
+ output: '',
374
+ success: false,
375
+ taskId: task.id,
376
+ };
377
+ }
378
+ }
379
+ /**
380
+ * Scaffold a story file with pre-populated structure
381
+ */
382
+ scaffoldStoryFile(task) {
383
+ const currentDate = new Date().toISOString().split('T')[0];
384
+ return `# Story ${task.id}: ${task.title}
385
+
386
+ <!-- Powered by BMAD™ Core -->
387
+
388
+ ## Status
389
+
390
+ Draft
391
+
392
+ ---
393
+
394
+ ## Story
395
+
396
+ ${task.prompt.includes('**As a**') ? task.prompt.match(/## Story\n([\s\S]*?)(?=\n##|$)/)?.[1]?.trim() || '_[AI Agent will populate]_' : '_[AI Agent will populate]_'}
397
+
398
+ ---
399
+
400
+ ## Acceptance Criteria
401
+
402
+ ${task.prompt.includes('## Acceptance Criteria') ? task.prompt.match(/## Acceptance Criteria\n([\s\S]*?)(?=\n##|$)/)?.[1]?.trim() || '_[AI Agent will populate]_' : '_[AI Agent will populate]_'}
403
+
404
+ ---
405
+
406
+ ## Tasks / Subtasks
407
+
408
+ ${task.prompt.includes('## Tasks / Subtasks') ? task.prompt.match(/## Tasks \/ Subtasks\n([\s\S]*?)(?=\n##|$)/)?.[1]?.trim() || '_[AI Agent will populate]_' : '_[AI Agent will populate]_'}
409
+
410
+ ---
411
+
412
+ ## Dev Notes
413
+
414
+ _[AI Agent will populate: Your findings, what you fixed/changed, issues encountered]_
415
+
416
+ ### Testing
417
+
418
+ ${task.prompt.includes('### Testing') ? task.prompt.match(/### Testing\n([\s\S]*?)(?=\n##|$)/)?.[1]?.trim() || '_[AI Agent will populate]_' : '_[AI Agent will populate]_'}
419
+
420
+ ---
421
+
422
+ ## Change Log
423
+
424
+ | Date | Version | Description | Author |
425
+ |------|---------|-------------|--------|
426
+ | ${currentDate} | 1.0 | Story created | AI Agent |
427
+ `;
428
+ }
429
+ /**
430
+ * Check if a layer should be skipped due to failed dependencies
431
+ */
432
+ shouldSkipLayer(layer, taskMap, taskResults) {
433
+ const completedTaskIds = new Set(taskResults.filter((r) => r.success).map((r) => r.taskId));
434
+ for (const taskId of layer) {
435
+ const task = taskMap.get(taskId);
436
+ if (!task)
437
+ continue;
438
+ // Check if all dependencies succeeded
439
+ for (const depId of task.dependencies) {
440
+ if (!completedTaskIds.has(depId)) {
441
+ return true; // Dependency not completed successfully
442
+ }
443
+ }
444
+ }
445
+ return false;
446
+ }
447
+ }