@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,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskDecompositionService
|
|
3
|
+
*
|
|
4
|
+
* Decomposes large goals into executable task graphs with dependency management.
|
|
5
|
+
* Leverages Claude AI to intelligently break down complex objectives into
|
|
6
|
+
* small, actionable tasks with proper sequencing and parallelization.
|
|
7
|
+
*/
|
|
8
|
+
import * as yaml from 'js-yaml';
|
|
9
|
+
/**
|
|
10
|
+
* Service for decomposing goals into task graphs
|
|
11
|
+
*/
|
|
12
|
+
export class TaskDecompositionService {
|
|
13
|
+
agentRunner;
|
|
14
|
+
fileManager;
|
|
15
|
+
globMatcher;
|
|
16
|
+
logger;
|
|
17
|
+
constructor(agentRunner, fileManager, globMatcher, logger) {
|
|
18
|
+
this.agentRunner = agentRunner;
|
|
19
|
+
this.fileManager = fileManager;
|
|
20
|
+
this.globMatcher = globMatcher;
|
|
21
|
+
this.logger = logger;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decompose a goal into an executable task graph
|
|
25
|
+
*
|
|
26
|
+
* @param options - Decomposition options including goal, context, and mode
|
|
27
|
+
* @param sessionDir - Directory where session outputs will be stored
|
|
28
|
+
* @returns Complete task graph with dependencies
|
|
29
|
+
*/
|
|
30
|
+
async decomposeGoal(options, sessionDir) {
|
|
31
|
+
this.logger.info({ goal: options.goal, perFile: options.perFile }, 'Starting goal decomposition');
|
|
32
|
+
// If per-file mode is enabled, gather target files first
|
|
33
|
+
let targetFiles = [];
|
|
34
|
+
if (options.perFile && options.filePattern) {
|
|
35
|
+
targetFiles = await this.gatherTargetFiles(options.filePattern);
|
|
36
|
+
this.logger.info({ fileCount: targetFiles.length, pattern: options.filePattern }, 'Gathered target files');
|
|
37
|
+
}
|
|
38
|
+
// Build decomposition prompt
|
|
39
|
+
const prompt = this.buildDecompositionPrompt(options, targetFiles, sessionDir);
|
|
40
|
+
this.logger.debug({ promptLength: prompt.length }, 'Built decomposition prompt');
|
|
41
|
+
// Execute Claude agent to generate task graph
|
|
42
|
+
const result = await this.agentRunner.runAgent(prompt, {
|
|
43
|
+
agentType: (options.agent ?? 'architect'),
|
|
44
|
+
references: options.contextFiles,
|
|
45
|
+
timeout: options.taskTimeout ?? 600_000, // 10 min default for planning
|
|
46
|
+
});
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
throw new Error(`Failed to decompose goal: ${result.errors}`);
|
|
49
|
+
}
|
|
50
|
+
// Save raw output for debugging
|
|
51
|
+
try {
|
|
52
|
+
const debugOutputPath = `${sessionDir}/raw-claude-output.txt`;
|
|
53
|
+
await this.fileManager.writeFile(debugOutputPath, result.output);
|
|
54
|
+
this.logger.debug({ debugOutputPath }, 'Saved raw Claude output for debugging');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.logger.warn({ error: error.message }, 'Failed to save debug output');
|
|
58
|
+
}
|
|
59
|
+
// Validate and fix YAML before parsing
|
|
60
|
+
const validatedOutput = await this.validateAndFixYaml(result.output, options, sessionDir);
|
|
61
|
+
// Parse YAML output into task graph
|
|
62
|
+
const taskGraph = this.parseTaskGraph(validatedOutput, options, sessionDir, targetFiles);
|
|
63
|
+
this.logger.info({
|
|
64
|
+
estimatedDuration: taskGraph.metadata.estimatedDuration,
|
|
65
|
+
layers: taskGraph.metadata.executionLayers.length,
|
|
66
|
+
totalTasks: taskGraph.metadata.totalTasks,
|
|
67
|
+
}, 'Goal decomposition complete');
|
|
68
|
+
return taskGraph;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the decomposition prompt for Claude
|
|
72
|
+
*/
|
|
73
|
+
buildDecompositionPrompt(options, targetFiles, sessionDir) {
|
|
74
|
+
let prompt = `You are an expert system architect specializing in task decomposition.
|
|
75
|
+
|
|
76
|
+
## GOAL
|
|
77
|
+
${options.goal}
|
|
78
|
+
|
|
79
|
+
## YOUR MISSION
|
|
80
|
+
Break this goal into small, executable tasks that can be completed by Claude AI agents in approximately 10 minutes each.
|
|
81
|
+
|
|
82
|
+
## CONSTRAINTS
|
|
83
|
+
- Each task must be atomic and independently executable
|
|
84
|
+
- Tasks should have clear dependencies (what must complete before this task)
|
|
85
|
+
- Identify opportunities for parallel execution
|
|
86
|
+
- Maximum parallel tasks: ${options.maxParallel ?? 3}
|
|
87
|
+
- Tasks must be actionable with specific prompts
|
|
88
|
+
|
|
89
|
+
## AGENT ASSIGNMENT
|
|
90
|
+
For each task, you MUST choose the most appropriate agent type based on what the task actually does:
|
|
91
|
+
- "dev" - Code implementation, refactoring, bug fixes, feature development
|
|
92
|
+
- "architect" - System design, technical decisions, infrastructure, API design
|
|
93
|
+
- "analyst" - Requirements gathering, data analysis, research, documentation
|
|
94
|
+
- "tea" - Test architecture, testing strategy, test automation, quality validation
|
|
95
|
+
- "pm" - Project planning, coordination, stakeholder communication
|
|
96
|
+
- "sm" - Sprint/process management, team coordination, agile ceremonies
|
|
97
|
+
- "tech-writer" - Technical documentation, API docs, user guides
|
|
98
|
+
- "ux-designer" - UI/UX design, user flows, wireframes, design systems
|
|
99
|
+
- "quick-flow-solo-dev" - Rapid prototyping, solo development, quick iterations
|
|
100
|
+
|
|
101
|
+
Assign agentType based on the task's nature, NOT the overall goal. A refactoring goal may have architect tasks for planning, dev tasks for implementation, and tea tasks for test validation.
|
|
102
|
+
|
|
103
|
+
## CRITICAL: TASKS MUST MAKE CODE CHANGES
|
|
104
|
+
**ANALYSIS-ONLY TASKS ARE NOT ACCEPTABLE.**
|
|
105
|
+
|
|
106
|
+
Every task MUST result in actual source code modifications. Do NOT create tasks that:
|
|
107
|
+
- Only analyze or investigate without making changes
|
|
108
|
+
- Document findings without fixing the issues found
|
|
109
|
+
- Report "no changes needed" without attempting fixes
|
|
110
|
+
- Write summaries instead of making code changes
|
|
111
|
+
|
|
112
|
+
Each task prompt MUST explicitly specify:
|
|
113
|
+
1. What SOURCE FILES will be MODIFIED (not just read)
|
|
114
|
+
2. What specific CHANGES will be made
|
|
115
|
+
3. How to VERIFY the changes work (run tests, lint, etc.)
|
|
116
|
+
|
|
117
|
+
If a task cannot make changes (e.g., all issues already fixed), the agent should:
|
|
118
|
+
1. Verify with evidence (show lint output, test results)
|
|
119
|
+
2. Proceed to make any IMPROVEMENTS possible (refactoring, better types, cleaner code)
|
|
120
|
+
3. Only mark complete after exhausting ALL improvement opportunities
|
|
121
|
+
|
|
122
|
+
`;
|
|
123
|
+
// Add per-file mode instructions
|
|
124
|
+
if (options.perFile && targetFiles.length > 0) {
|
|
125
|
+
// Show a small sample of files for context
|
|
126
|
+
const sampleSize = Math.min(10, targetFiles.length);
|
|
127
|
+
const sample = targetFiles.slice(0, sampleSize);
|
|
128
|
+
prompt += `## PER-FILE MODE ENABLED
|
|
129
|
+
You MUST create one task per file for the main work. This is critical for file-heavy operations like migrations.
|
|
130
|
+
|
|
131
|
+
**Total Files to Process:** ${targetFiles.length}
|
|
132
|
+
|
|
133
|
+
**Sample Files (showing ${sampleSize} of ${targetFiles.length}):**
|
|
134
|
+
${sample.join('\n')}
|
|
135
|
+
${targetFiles.length > sampleSize ? `\n... (${targetFiles.length - sampleSize} more files not shown - you'll create tasks for ALL ${targetFiles.length} files)` : ''}
|
|
136
|
+
|
|
137
|
+
**Task Structure:**
|
|
138
|
+
1. Create analysis/planning tasks first (1-3 tasks)
|
|
139
|
+
2. Create ONE task per file for the core work (${targetFiles.length} tasks total)
|
|
140
|
+
3. Create verification/cleanup tasks last (1-2 tasks)
|
|
141
|
+
|
|
142
|
+
**CRITICAL:** Each file task should:
|
|
143
|
+
- Have sequential IDs (task-001, task-002, ... task-${targetFiles.length + 3})
|
|
144
|
+
- Reference the specific file in "targetFiles" array
|
|
145
|
+
- Include the file path in the title
|
|
146
|
+
- Have appropriate dependencies on planning tasks
|
|
147
|
+
- Be parallelizable (set parallelizable: true for file tasks)
|
|
148
|
+
|
|
149
|
+
You don't need to see all file paths - just know there are ${targetFiles.length} files total.
|
|
150
|
+
The system will provide the correct file path for each task in the targetFiles array.
|
|
151
|
+
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
// Add context files
|
|
155
|
+
if (options.contextFiles && options.contextFiles.length > 0) {
|
|
156
|
+
prompt += `## CONTEXT FILES
|
|
157
|
+
${options.contextFiles.map((f) => `- ${f}`).join('\n')}
|
|
158
|
+
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
// Add output format instructions (varies based on story format mode)
|
|
162
|
+
prompt += options.storyFormat ? `## OUTPUT FORMAT - STORY MODE
|
|
163
|
+
You MUST output tasks as BMAD-formatted STORIES with proper structure.
|
|
164
|
+
Output ONLY valid YAML. DO NOT include markdown code fences or any other text.
|
|
165
|
+
|
|
166
|
+
Each task will become a full story with:
|
|
167
|
+
- Story ID: ${options.storyPrefix}-<number> (e.g., ${options.storyPrefix}-001, ${options.storyPrefix}-002)
|
|
168
|
+
- User story format: "As a... I want... so that..."
|
|
169
|
+
- Acceptance Criteria
|
|
170
|
+
- Tasks/Subtasks breakdown
|
|
171
|
+
- Dev Notes with testing info
|
|
172
|
+
|
|
173
|
+
\`\`\`yaml
|
|
174
|
+
masterPrompt: |
|
|
175
|
+
A reusable prompt template that applies to all stories.
|
|
176
|
+
Include best practices, coding standards, and common guidelines for implementing these stories.
|
|
177
|
+
|
|
178
|
+
tasks:
|
|
179
|
+
- id: ${options.storyPrefix}-001
|
|
180
|
+
title: "Clear, specific story title"
|
|
181
|
+
description: "High-level description of what this story delivers"
|
|
182
|
+
estimatedMinutes: 10
|
|
183
|
+
dependencies: [] # Array of story IDs that must complete first
|
|
184
|
+
parallelizable: true
|
|
185
|
+
agentType: "dev"
|
|
186
|
+
targetFiles: # Optional: specific files this story operates on
|
|
187
|
+
- "path/to/file.js"
|
|
188
|
+
prompt: |
|
|
189
|
+
Create a story following BMAD story format:
|
|
190
|
+
|
|
191
|
+
# Story ${options.storyPrefix}-001: [Title]
|
|
192
|
+
|
|
193
|
+
## Status
|
|
194
|
+
Draft
|
|
195
|
+
|
|
196
|
+
## Story
|
|
197
|
+
**As a** [role/persona],
|
|
198
|
+
**I want** [capability/feature],
|
|
199
|
+
**so that** [benefit/value]
|
|
200
|
+
|
|
201
|
+
## Acceptance Criteria
|
|
202
|
+
1. [Specific, testable criterion]
|
|
203
|
+
2. [Another criterion]
|
|
204
|
+
3. [Another criterion]
|
|
205
|
+
|
|
206
|
+
## Tasks / Subtasks
|
|
207
|
+
- [ ] Task 1: [Description] (AC: #1)
|
|
208
|
+
- [ ] Subtask 1.1: [Specific action]
|
|
209
|
+
- [ ] Subtask 1.2: [Specific action]
|
|
210
|
+
- [ ] Task 2: [Description] (AC: #2)
|
|
211
|
+
- [ ] Subtask 2.1: [Specific action]
|
|
212
|
+
|
|
213
|
+
## Dev Notes
|
|
214
|
+
[Relevant architecture info, integration points, file locations]
|
|
215
|
+
|
|
216
|
+
### Testing
|
|
217
|
+
- Test file location: [path]
|
|
218
|
+
- Test standards: [requirements]
|
|
219
|
+
- Testing frameworks: [tools being used]
|
|
220
|
+
- Specific requirements: [any special test needs]
|
|
221
|
+
|
|
222
|
+
## Change Log
|
|
223
|
+
| Date | Version | Description | Author |
|
|
224
|
+
|------|---------|-------------|--------|
|
|
225
|
+
| [Date] | 1.0 | Story created | AI Agent |
|
|
226
|
+
outputFile: "${sessionDir}/stories/${options.storyPrefix}-001.md"
|
|
227
|
+
|
|
228
|
+
- id: ${options.storyPrefix}-002
|
|
229
|
+
title: "Next story"
|
|
230
|
+
description: "Story description"
|
|
231
|
+
estimatedMinutes: 5
|
|
232
|
+
dependencies: ["${options.storyPrefix}-001"]
|
|
233
|
+
parallelizable: false
|
|
234
|
+
agentType: "dev"
|
|
235
|
+
prompt: |
|
|
236
|
+
[Story-formatted prompt as above]
|
|
237
|
+
outputFile: "${sessionDir}/stories/${options.storyPrefix}-002.md"
|
|
238
|
+
|
|
239
|
+
# ... more tasks (as stories)
|
|
240
|
+
\`\`\`
|
|
241
|
+
|
|
242
|
+
**CRITICAL FOR STORY MODE:**
|
|
243
|
+
- Task IDs MUST be story IDs: ${options.storyPrefix}-001, ${options.storyPrefix}-002, etc.
|
|
244
|
+
- Output files MUST go to stories/ directory
|
|
245
|
+
- Prompts MUST generate full story structure (not simple task outputs)
|
|
246
|
+
- Each story must have user story format, acceptance criteria, and tasks breakdown
|
|
247
|
+
|
|
248
|
+
## IMPORTANT RULES
|
|
249
|
+
1. Each story ID must be unique
|
|
250
|
+
2. Dependencies must reference valid story IDs
|
|
251
|
+
3. Circular dependencies are not allowed
|
|
252
|
+
4. Stories with no dependencies can run immediately
|
|
253
|
+
5. Stories with the same dependencies can run in parallel (if parallelizable: true)
|
|
254
|
+
6. All file paths in outputFile should use the session directory: ${sessionDir}/stories/
|
|
255
|
+
${options.perFile ? '7. **CRITICAL**: Create one story per file for the main work (per-file mode is ON)' : ''}
|
|
256
|
+
|
|
257
|
+
## EXAMPLE DEPENDENCY PATTERNS
|
|
258
|
+
|
|
259
|
+
Sequential:
|
|
260
|
+
${options.storyPrefix}-001 (deps: []) → ${options.storyPrefix}-002 (deps: [${options.storyPrefix}-001])
|
|
261
|
+
|
|
262
|
+
Parallel then join:
|
|
263
|
+
${options.storyPrefix}-001 (deps: []) → [${options.storyPrefix}-002, ${options.storyPrefix}-003] → ${options.storyPrefix}-004
|
|
264
|
+
|
|
265
|
+
Now, decompose the goal into stories. Output ONLY the YAML structure.
|
|
266
|
+
` : `## OUTPUT FORMAT
|
|
267
|
+
You MUST output ONLY valid YAML in the following structure. DO NOT include markdown code fences or any other text.
|
|
268
|
+
|
|
269
|
+
\`\`\`yaml
|
|
270
|
+
masterPrompt: |
|
|
271
|
+
A reusable prompt template that applies to all tasks.
|
|
272
|
+
Include best practices, coding standards, and common guidelines.
|
|
273
|
+
|
|
274
|
+
tasks:
|
|
275
|
+
- id: task-001
|
|
276
|
+
title: "Clear, specific task title"
|
|
277
|
+
description: "Detailed description of what this task accomplishes"
|
|
278
|
+
estimatedMinutes: 10
|
|
279
|
+
dependencies: [] # Array of task IDs that must complete first
|
|
280
|
+
parallelizable: true # Can this run in parallel with other tasks?
|
|
281
|
+
agentType: "dev" # Agent type: dev, architect, qa, etc.
|
|
282
|
+
targetFiles: # Optional: specific files this task operates on
|
|
283
|
+
- "path/to/file.js"
|
|
284
|
+
prompt: |
|
|
285
|
+
Specific prompt for the agent to execute this task.
|
|
286
|
+
Include:
|
|
287
|
+
- What to do
|
|
288
|
+
- Where to do it (file paths)
|
|
289
|
+
- Expected outcome
|
|
290
|
+
- Any validation steps
|
|
291
|
+
outputFile: "${sessionDir}/outputs/task-001-output.md"
|
|
292
|
+
|
|
293
|
+
- id: task-002
|
|
294
|
+
title: "Next task"
|
|
295
|
+
description: "Task description"
|
|
296
|
+
estimatedMinutes: 5
|
|
297
|
+
dependencies: ["task-001"] # Must wait for task-001
|
|
298
|
+
parallelizable: false
|
|
299
|
+
agentType: "dev"
|
|
300
|
+
prompt: |
|
|
301
|
+
Task-specific prompt here
|
|
302
|
+
outputFile: "${sessionDir}/outputs/task-002-output.md"
|
|
303
|
+
|
|
304
|
+
# ... more tasks
|
|
305
|
+
\`\`\`
|
|
306
|
+
|
|
307
|
+
## IMPORTANT RULES
|
|
308
|
+
1. Each task ID must be unique
|
|
309
|
+
2. Dependencies must reference valid task IDs
|
|
310
|
+
3. Circular dependencies are not allowed
|
|
311
|
+
4. Tasks with no dependencies can run immediately
|
|
312
|
+
5. Tasks with the same dependencies can run in parallel (if parallelizable: true)
|
|
313
|
+
6. All file paths in outputFile should use the session directory: ${sessionDir}/outputs/
|
|
314
|
+
${options.perFile ? '7. **CRITICAL**: Create one task per file for the main work (per-file mode is ON)' : ''}
|
|
315
|
+
|
|
316
|
+
## EXAMPLE DEPENDENCY PATTERNS
|
|
317
|
+
|
|
318
|
+
Sequential:
|
|
319
|
+
task-001 (deps: []) → task-002 (deps: [task-001]) → task-003 (deps: [task-002])
|
|
320
|
+
|
|
321
|
+
Parallel then join:
|
|
322
|
+
task-001 (deps: []) → [task-002, task-003] (deps: [task-001]) → task-004 (deps: [task-002, task-003])
|
|
323
|
+
|
|
324
|
+
Now, decompose the goal into tasks. Output ONLY the YAML structure.
|
|
325
|
+
`;
|
|
326
|
+
return prompt;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Build execution layers using topological sort (Kahn's algorithm)
|
|
330
|
+
* Each layer contains tasks that can run in parallel
|
|
331
|
+
*/
|
|
332
|
+
buildExecutionLayers(tasks) {
|
|
333
|
+
const layers = [];
|
|
334
|
+
const inDegree = new Map();
|
|
335
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
336
|
+
// Calculate in-degree for each task
|
|
337
|
+
for (const task of tasks) {
|
|
338
|
+
if (!inDegree.has(task.id)) {
|
|
339
|
+
inDegree.set(task.id, 0);
|
|
340
|
+
}
|
|
341
|
+
for (const depId of task.dependencies) {
|
|
342
|
+
inDegree.set(depId, (inDegree.get(depId) ?? 0));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Set in-degree based on dependencies
|
|
346
|
+
for (const task of tasks) {
|
|
347
|
+
inDegree.set(task.id, task.dependencies.length);
|
|
348
|
+
}
|
|
349
|
+
// Build layers
|
|
350
|
+
const remaining = new Set(tasks.map((t) => t.id));
|
|
351
|
+
while (remaining.size > 0) {
|
|
352
|
+
// Find all tasks with no remaining dependencies
|
|
353
|
+
const currentLayer = [...remaining].filter((id) => inDegree.get(id) === 0);
|
|
354
|
+
if (currentLayer.length === 0) {
|
|
355
|
+
throw new Error('Cannot build execution layers: possible circular dependency');
|
|
356
|
+
}
|
|
357
|
+
layers.push(currentLayer);
|
|
358
|
+
// Remove current layer tasks and update in-degrees
|
|
359
|
+
for (const taskId of currentLayer) {
|
|
360
|
+
remaining.delete(taskId);
|
|
361
|
+
// Reduce in-degree for tasks that depend on this one
|
|
362
|
+
for (const otherId of remaining) {
|
|
363
|
+
const task = taskMap.get(otherId);
|
|
364
|
+
if (task && task.dependencies.includes(taskId)) {
|
|
365
|
+
inDegree.set(otherId, (inDegree.get(otherId) ?? 1) - 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return layers;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Detect circular dependencies using DFS
|
|
374
|
+
*/
|
|
375
|
+
detectCircularDependencies(tasks) {
|
|
376
|
+
const visited = new Set();
|
|
377
|
+
const recursionStack = new Set();
|
|
378
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
379
|
+
const dfs = (taskId) => {
|
|
380
|
+
visited.add(taskId);
|
|
381
|
+
recursionStack.add(taskId);
|
|
382
|
+
const task = taskMap.get(taskId);
|
|
383
|
+
if (!task)
|
|
384
|
+
return false;
|
|
385
|
+
for (const depId of task.dependencies) {
|
|
386
|
+
if (!visited.has(depId)) {
|
|
387
|
+
if (dfs(depId))
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
else if (recursionStack.has(depId)) {
|
|
391
|
+
throw new Error(`Circular dependency detected involving task: ${taskId} -> ${depId}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
recursionStack.delete(taskId);
|
|
395
|
+
return false;
|
|
396
|
+
};
|
|
397
|
+
for (const task of tasks) {
|
|
398
|
+
if (!visited.has(task.id)) {
|
|
399
|
+
dfs(task.id);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Extract YAML content from output (handles markdown code fences)
|
|
405
|
+
*/
|
|
406
|
+
extractYaml(output) {
|
|
407
|
+
// Try to extract from markdown code fence first
|
|
408
|
+
const yamlMatch = output.match(/```(?:yaml|yml)?\s*\n([\s\S]*?)\n```/);
|
|
409
|
+
if (yamlMatch) {
|
|
410
|
+
return yamlMatch[1].trim();
|
|
411
|
+
}
|
|
412
|
+
// Try to find yaml content without code fences
|
|
413
|
+
// Look for lines starting with "masterPrompt:" or "tasks:"
|
|
414
|
+
const lines = output.split('\n');
|
|
415
|
+
let startIndex = lines.findIndex((line) => line.trim().startsWith('masterPrompt:'));
|
|
416
|
+
if (startIndex === -1) {
|
|
417
|
+
// Try finding "tasks:" if masterPrompt not found
|
|
418
|
+
startIndex = lines.findIndex((line) => line.trim().startsWith('tasks:'));
|
|
419
|
+
}
|
|
420
|
+
if (startIndex !== -1) {
|
|
421
|
+
// Find the end of YAML content (stop at empty lines or obvious non-YAML content)
|
|
422
|
+
let endIndex = lines.length;
|
|
423
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
424
|
+
const line = lines[i].trim();
|
|
425
|
+
// Stop if we hit obvious non-YAML content
|
|
426
|
+
if (line.startsWith('#') && !line.startsWith('# ') && i > startIndex + 5) {
|
|
427
|
+
// Could be a comment, but if far from start, likely end of YAML
|
|
428
|
+
endIndex = i;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const yamlContent = lines.slice(startIndex, endIndex).join('\n');
|
|
433
|
+
return yamlContent.trim();
|
|
434
|
+
}
|
|
435
|
+
// Last resort: return everything and let yaml parser try
|
|
436
|
+
return output.trim();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Gather target files using glob pattern
|
|
440
|
+
*/
|
|
441
|
+
async gatherTargetFiles(pattern) {
|
|
442
|
+
try {
|
|
443
|
+
const files = await this.globMatcher.expandPattern(pattern);
|
|
444
|
+
return files;
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
this.logger.error({ error: error.message, pattern }, 'Failed to gather target files');
|
|
448
|
+
throw new Error(`Failed to expand file pattern: ${pattern}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Generate unique session ID
|
|
453
|
+
*/
|
|
454
|
+
generateSessionId() {
|
|
455
|
+
const now = new Date();
|
|
456
|
+
const timestamp = now.toISOString().replaceAll(/[:.]/g, '-').replace('T', '-').split('.')[0];
|
|
457
|
+
return `decompose-${timestamp}`;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Parse Claude's YAML output into a structured TaskGraph
|
|
461
|
+
*/
|
|
462
|
+
parseTaskGraph(output, options, sessionDir, targetFiles) {
|
|
463
|
+
this.logger.debug('Parsing task graph from YAML output');
|
|
464
|
+
try {
|
|
465
|
+
// Extract YAML from output (handle potential markdown code fences)
|
|
466
|
+
const yamlContent = this.extractYaml(output);
|
|
467
|
+
this.logger.debug({ yamlLength: yamlContent.length }, 'Extracted YAML content');
|
|
468
|
+
// Log first 500 chars of YAML for debugging
|
|
469
|
+
this.logger.debug({ yamlPreview: yamlContent.slice(0, 500) }, 'YAML preview (first 500 chars)');
|
|
470
|
+
// Parse YAML
|
|
471
|
+
const parsed = yaml.load(yamlContent);
|
|
472
|
+
if (!parsed.masterPrompt || !parsed.tasks || !Array.isArray(parsed.tasks)) {
|
|
473
|
+
throw new Error('Invalid task graph structure: missing masterPrompt or tasks array');
|
|
474
|
+
}
|
|
475
|
+
// Convert to TaskNode array
|
|
476
|
+
const tasks = parsed.tasks.map((t) => ({
|
|
477
|
+
agentType: t.agentType ?? 'dev',
|
|
478
|
+
dependencies: t.dependencies ?? [],
|
|
479
|
+
description: t.description,
|
|
480
|
+
estimatedMinutes: t.estimatedMinutes,
|
|
481
|
+
id: t.id,
|
|
482
|
+
outputFile: t.outputFile,
|
|
483
|
+
parallelizable: t.parallelizable ?? true,
|
|
484
|
+
prompt: t.prompt,
|
|
485
|
+
targetFiles: t.targetFiles,
|
|
486
|
+
title: t.title,
|
|
487
|
+
}));
|
|
488
|
+
// Validate task graph
|
|
489
|
+
this.validateTaskGraph(tasks);
|
|
490
|
+
// Build execution layers (topological sort)
|
|
491
|
+
const executionLayers = this.buildExecutionLayers(tasks);
|
|
492
|
+
// Calculate metadata
|
|
493
|
+
const totalMinutes = tasks.reduce((sum, t) => sum + t.estimatedMinutes, 0);
|
|
494
|
+
const maxParallel = Math.max(...executionLayers.map((layer) => layer.length));
|
|
495
|
+
// Generate session ID
|
|
496
|
+
const sessionId = this.generateSessionId();
|
|
497
|
+
const taskGraph = {
|
|
498
|
+
goal: options.goal,
|
|
499
|
+
masterPrompt: parsed.masterPrompt,
|
|
500
|
+
metadata: {
|
|
501
|
+
estimatedDuration: totalMinutes,
|
|
502
|
+
executionLayers,
|
|
503
|
+
maxParallelism: maxParallel,
|
|
504
|
+
perFileMode: options.perFile ?? false,
|
|
505
|
+
storyFormat: options.storyFormat ?? false,
|
|
506
|
+
totalFiles: targetFiles.length > 0 ? targetFiles.length : undefined,
|
|
507
|
+
totalTasks: tasks.length,
|
|
508
|
+
},
|
|
509
|
+
session: {
|
|
510
|
+
createdAt: new Date().toISOString(),
|
|
511
|
+
id: sessionId,
|
|
512
|
+
outputDirectory: sessionDir,
|
|
513
|
+
},
|
|
514
|
+
tasks,
|
|
515
|
+
};
|
|
516
|
+
return taskGraph;
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
this.logger.error({ error: error.message, outputLength: output.length }, 'Failed to parse task graph');
|
|
520
|
+
throw new Error(`Failed to parse task graph: ${error.message}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Validate YAML and ask Claude to fix it if invalid
|
|
525
|
+
*/
|
|
526
|
+
async validateAndFixYaml(output, options, sessionDir) {
|
|
527
|
+
this.logger.debug('Validating YAML output');
|
|
528
|
+
// Try to extract and parse the YAML
|
|
529
|
+
try {
|
|
530
|
+
const yamlContent = this.extractYaml(output);
|
|
531
|
+
yaml.load(yamlContent); // Just validate, don't use the result yet
|
|
532
|
+
this.logger.info('YAML validation passed');
|
|
533
|
+
return output; // YAML is valid, return as-is
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
this.logger.warn({ error: error.message }, 'YAML validation failed, asking Claude to fix it');
|
|
537
|
+
// YAML is invalid, ask Claude to fix it
|
|
538
|
+
const fixPrompt = `The following YAML has syntax errors. Please fix ALL syntax errors and return ONLY the corrected YAML (no explanations, no markdown code fences, just the raw YAML):
|
|
539
|
+
|
|
540
|
+
ERROR: ${error.message}
|
|
541
|
+
|
|
542
|
+
INVALID YAML:
|
|
543
|
+
${output}
|
|
544
|
+
|
|
545
|
+
Please output the CORRECTED YAML only. Ensure:
|
|
546
|
+
1. Proper indentation (2 spaces per level)
|
|
547
|
+
2. All strings with special characters are quoted
|
|
548
|
+
3. All lists and mappings are properly formatted
|
|
549
|
+
4. No syntax errors remain
|
|
550
|
+
|
|
551
|
+
Output ONLY the corrected YAML:
|
|
552
|
+
`;
|
|
553
|
+
this.logger.info('Asking Claude to fix YAML errors');
|
|
554
|
+
const fixResult = await this.agentRunner.runAgent(fixPrompt, {
|
|
555
|
+
agentType: 'architect',
|
|
556
|
+
timeout: 60_000, // 1 minute for fix
|
|
557
|
+
});
|
|
558
|
+
if (!fixResult.success) {
|
|
559
|
+
throw new Error(`Failed to fix YAML: ${fixResult.errors}`);
|
|
560
|
+
}
|
|
561
|
+
// Validate the fixed YAML
|
|
562
|
+
try {
|
|
563
|
+
const fixedYaml = this.extractYaml(fixResult.output);
|
|
564
|
+
yaml.load(fixedYaml); // Validate the fix
|
|
565
|
+
this.logger.info('Claude successfully fixed the YAML');
|
|
566
|
+
// Save the fixed YAML for reference
|
|
567
|
+
try {
|
|
568
|
+
const fixedYamlPath = `${sessionDir}/fixed-yaml.txt`;
|
|
569
|
+
await this.fileManager.writeFile(fixedYamlPath, fixResult.output);
|
|
570
|
+
this.logger.debug({ fixedYamlPath }, 'Saved fixed YAML');
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// Ignore save errors
|
|
574
|
+
}
|
|
575
|
+
return fixResult.output;
|
|
576
|
+
}
|
|
577
|
+
catch (fixError) {
|
|
578
|
+
this.logger.error({ error: fixError.message }, 'Claude failed to fix YAML properly');
|
|
579
|
+
throw new Error(`Claude could not fix the YAML errors.\n` +
|
|
580
|
+
`Original error: ${error.message}\n` +
|
|
581
|
+
`Fix attempt error: ${fixError.message}\n\n` +
|
|
582
|
+
`Raw output saved to: ${sessionDir}/raw-claude-output.txt`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Validate task graph for common errors
|
|
588
|
+
*/
|
|
589
|
+
validateTaskGraph(tasks) {
|
|
590
|
+
const ids = new Set();
|
|
591
|
+
for (const task of tasks) {
|
|
592
|
+
// Check unique IDs
|
|
593
|
+
if (ids.has(task.id)) {
|
|
594
|
+
throw new Error(`Duplicate task ID: ${task.id}`);
|
|
595
|
+
}
|
|
596
|
+
ids.add(task.id);
|
|
597
|
+
// Check dependencies exist
|
|
598
|
+
for (const depId of task.dependencies) {
|
|
599
|
+
if (!tasks.some((t) => t.id === depId)) {
|
|
600
|
+
throw new Error(`Task ${task.id} has invalid dependency: ${depId}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Check for circular dependencies
|
|
605
|
+
this.detectCircularDependencies(tasks);
|
|
606
|
+
}
|
|
607
|
+
}
|