@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,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
+ }