@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.
- package/LICENSE +21 -0
- package/README.md +1017 -0
- package/bin/dev +5 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/show.d.ts +34 -0
- package/dist/commands/config/show.js +108 -0
- package/dist/commands/config/validate.d.ts +29 -0
- package/dist/commands/config/validate.js +131 -0
- package/dist/commands/decompose.d.ts +79 -0
- package/dist/commands/decompose.js +327 -0
- package/dist/commands/demo.d.ts +18 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/epics/create.d.ts +123 -0
- package/dist/commands/epics/create.js +459 -0
- package/dist/commands/epics/list.d.ts +120 -0
- package/dist/commands/epics/list.js +280 -0
- package/dist/commands/hello/index.d.ts +12 -0
- package/dist/commands/hello/index.js +34 -0
- package/dist/commands/hello/world.d.ts +8 -0
- package/dist/commands/hello/world.js +24 -0
- package/dist/commands/prd/fix.d.ts +39 -0
- package/dist/commands/prd/fix.js +140 -0
- package/dist/commands/prd/validate.d.ts +112 -0
- package/dist/commands/prd/validate.js +302 -0
- package/dist/commands/stories/create.d.ts +95 -0
- package/dist/commands/stories/create.js +431 -0
- package/dist/commands/stories/develop.d.ts +91 -0
- package/dist/commands/stories/develop.js +460 -0
- package/dist/commands/stories/list.d.ts +84 -0
- package/dist/commands/stories/list.js +291 -0
- package/dist/commands/stories/move.d.ts +66 -0
- package/dist/commands/stories/move.js +273 -0
- package/dist/commands/stories/qa.d.ts +99 -0
- package/dist/commands/stories/qa.js +530 -0
- package/dist/commands/workflow.d.ts +97 -0
- package/dist/commands/workflow.js +390 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/agent-options.d.ts +50 -0
- package/dist/models/agent-options.js +1 -0
- package/dist/models/agent-result.d.ts +29 -0
- package/dist/models/agent-result.js +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +10 -0
- package/dist/models/phase-result.d.ts +65 -0
- package/dist/models/phase-result.js +7 -0
- package/dist/models/provider.d.ts +28 -0
- package/dist/models/provider.js +18 -0
- package/dist/models/story.d.ts +154 -0
- package/dist/models/story.js +18 -0
- package/dist/models/workflow-config.d.ts +148 -0
- package/dist/models/workflow-config.js +1 -0
- package/dist/models/workflow-result.d.ts +164 -0
- package/dist/models/workflow-result.js +7 -0
- package/dist/services/agents/agent-runner-factory.d.ts +31 -0
- package/dist/services/agents/agent-runner-factory.js +44 -0
- package/dist/services/agents/agent-runner.d.ts +46 -0
- package/dist/services/agents/agent-runner.js +29 -0
- package/dist/services/agents/claude-agent-runner.d.ts +81 -0
- package/dist/services/agents/claude-agent-runner.js +332 -0
- package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
- package/dist/services/agents/gemini-agent-runner.js +350 -0
- package/dist/services/agents/index.d.ts +7 -0
- package/dist/services/agents/index.js +7 -0
- package/dist/services/file-system/file-manager.d.ts +110 -0
- package/dist/services/file-system/file-manager.js +223 -0
- package/dist/services/file-system/glob-matcher.d.ts +75 -0
- package/dist/services/file-system/glob-matcher.js +126 -0
- package/dist/services/file-system/path-resolver.d.ts +183 -0
- package/dist/services/file-system/path-resolver.js +400 -0
- package/dist/services/logging/workflow-logger.d.ts +232 -0
- package/dist/services/logging/workflow-logger.js +552 -0
- package/dist/services/orchestration/batch-processor.d.ts +113 -0
- package/dist/services/orchestration/batch-processor.js +187 -0
- package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
- package/dist/services/orchestration/dependency-graph-executor.js +447 -0
- package/dist/services/orchestration/index.d.ts +10 -0
- package/dist/services/orchestration/index.js +8 -0
- package/dist/services/orchestration/input-detector.d.ts +125 -0
- package/dist/services/orchestration/input-detector.js +381 -0
- package/dist/services/orchestration/story-queue.d.ts +94 -0
- package/dist/services/orchestration/story-queue.js +170 -0
- package/dist/services/orchestration/story-type-detector.d.ts +80 -0
- package/dist/services/orchestration/story-type-detector.js +258 -0
- package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
- package/dist/services/orchestration/task-decomposition-service.js +607 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
- package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
- package/dist/services/parsers/epic-parser.d.ts +117 -0
- package/dist/services/parsers/epic-parser.js +264 -0
- package/dist/services/parsers/prd-fixer.d.ts +86 -0
- package/dist/services/parsers/prd-fixer.js +194 -0
- package/dist/services/parsers/prd-parser.d.ts +123 -0
- package/dist/services/parsers/prd-parser.js +286 -0
- package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
- package/dist/services/parsers/standalone-story-parser.js +255 -0
- package/dist/services/parsers/story-parser-factory.d.ts +81 -0
- package/dist/services/parsers/story-parser-factory.js +108 -0
- package/dist/services/parsers/story-parser.d.ts +122 -0
- package/dist/services/parsers/story-parser.js +262 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
- package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
- package/dist/services/scaffolding/file-scaffolder.js +314 -0
- package/dist/services/validation/config-validator.d.ts +88 -0
- package/dist/services/validation/config-validator.js +167 -0
- package/dist/types/task-graph.d.ts +142 -0
- package/dist/types/task-graph.js +5 -0
- package/dist/utils/colors.d.ts +49 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/error-formatter.d.ts +64 -0
- package/dist/utils/error-formatter.js +279 -0
- package/dist/utils/errors.d.ts +170 -0
- package/dist/utils/errors.js +233 -0
- package/dist/utils/formatters.d.ts +84 -0
- package/dist/utils/formatters.js +162 -0
- package/dist/utils/logger.d.ts +63 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/progress.d.ts +104 -0
- package/dist/utils/progress.js +161 -0
- package/dist/utils/retry.d.ts +114 -0
- package/dist/utils/retry.js +160 -0
- package/dist/utils/shared-flags.d.ts +28 -0
- package/dist/utils/shared-flags.js +43 -0
- 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
|
+
}
|