@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,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stories Create Command
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates story markdown files from an epic using parallel Claude AI agent execution.
|
|
5
|
+
* Creates story files with configurable concurrency and batch processing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* bmad-workflow stories create docs/epics/epic-1-foundation.md
|
|
10
|
+
* bmad-workflow stories create epic.md --parallel 10
|
|
11
|
+
* bmad-workflow stories create epic.md --start 3
|
|
12
|
+
* bmad-workflow stories create epic.md --reference docs/architecture.md
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { createAgentRunner } from '../../services/agents/agent-runner-factory.js';
|
|
18
|
+
import { FileManager } from '../../services/file-system/file-manager.js';
|
|
19
|
+
import { PathResolver } from '../../services/file-system/path-resolver.js';
|
|
20
|
+
import { BatchProcessor } from '../../services/orchestration/batch-processor.js';
|
|
21
|
+
import { EpicParser } from '../../services/parsers/epic-parser.js';
|
|
22
|
+
import { FileScaffolder } from '../../services/scaffolding/file-scaffolder.js';
|
|
23
|
+
import * as colors from '../../utils/colors.js';
|
|
24
|
+
import { ValidationError } from '../../utils/errors.js';
|
|
25
|
+
import { createLogger, generateCorrelationId } from '../../utils/logger.js';
|
|
26
|
+
import { createSpinner } from '../../utils/progress.js';
|
|
27
|
+
import { agentFlags } from '../../utils/shared-flags.js';
|
|
28
|
+
/**
|
|
29
|
+
* Stories Create Command
|
|
30
|
+
*
|
|
31
|
+
* Creates story files from epic using Claude AI agents with parallel processing.
|
|
32
|
+
*/
|
|
33
|
+
export default class StoriesCreateCommand extends Command {
|
|
34
|
+
static args = {
|
|
35
|
+
'epic-path': Args.string({
|
|
36
|
+
description: 'Path to epic markdown file',
|
|
37
|
+
required: true,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
static description = 'Create story files from epic using Claude AI agents';
|
|
41
|
+
static examples = [
|
|
42
|
+
{
|
|
43
|
+
command: '<%= config.bin %> <%= command.id %> docs/epics/epic-1-foundation.md',
|
|
44
|
+
description: 'Create all stories from epic',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
command: '<%= config.bin %> <%= command.id %> epic.md --parallel 10',
|
|
48
|
+
description: 'Create stories with 10 concurrent agents',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
command: '<%= config.bin %> <%= command.id %> epic.md --start 3',
|
|
52
|
+
description: 'Create stories starting from story number 3',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
command: '<%= config.bin %> <%= command.id %> epic.md --reference docs/architecture.md --reference docs/api-spec.md',
|
|
56
|
+
description: 'Create stories with additional context files',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
command: '<%= config.bin %> <%= command.id %> epic.md --agent po --task create-next-story',
|
|
60
|
+
description: 'Use Product Owner agent with custom task for story creation',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
static flags = {
|
|
64
|
+
...agentFlags,
|
|
65
|
+
interval: Flags.integer({
|
|
66
|
+
default: 30,
|
|
67
|
+
description: 'Seconds between batches',
|
|
68
|
+
}),
|
|
69
|
+
parallel: Flags.integer({
|
|
70
|
+
default: 7,
|
|
71
|
+
description: 'Max concurrent story creations',
|
|
72
|
+
}),
|
|
73
|
+
prefix: Flags.string({
|
|
74
|
+
description: 'Filename prefix for stories',
|
|
75
|
+
}),
|
|
76
|
+
reference: Flags.string({
|
|
77
|
+
description: 'Additional context files for AI agents',
|
|
78
|
+
multiple: true,
|
|
79
|
+
}),
|
|
80
|
+
start: Flags.integer({
|
|
81
|
+
description: 'Story number to start from',
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
// Service instances
|
|
85
|
+
agentRunner;
|
|
86
|
+
batchProcessor;
|
|
87
|
+
epicParser;
|
|
88
|
+
fileManager;
|
|
89
|
+
fileScaffolder;
|
|
90
|
+
logger;
|
|
91
|
+
pathResolver;
|
|
92
|
+
/**
|
|
93
|
+
* Run the command
|
|
94
|
+
*/
|
|
95
|
+
async run() {
|
|
96
|
+
const { args, flags } = await this.parse(StoriesCreateCommand);
|
|
97
|
+
const startTime = Date.now();
|
|
98
|
+
const correlationId = generateCorrelationId();
|
|
99
|
+
// Initialize services with selected provider
|
|
100
|
+
const provider = (flags.provider || 'claude');
|
|
101
|
+
this.initializeServices(flags.parallel, flags.interval, provider);
|
|
102
|
+
this.logger.info({
|
|
103
|
+
correlationId,
|
|
104
|
+
epicPath: args['epic-path'],
|
|
105
|
+
flags,
|
|
106
|
+
}, 'Starting stories create command');
|
|
107
|
+
try {
|
|
108
|
+
// Validate parallel flag
|
|
109
|
+
this.validateParallelFlag(flags.parallel);
|
|
110
|
+
// Parse epic and extract stories
|
|
111
|
+
const epicPath = path.resolve(args['epic-path']);
|
|
112
|
+
const stories = await this.parseEpicStories(epicPath);
|
|
113
|
+
// Filter stories based on --start flag
|
|
114
|
+
const filteredStories = this.filterStoriesByStart(stories, flags.start);
|
|
115
|
+
// Check for existing story files (idempotent)
|
|
116
|
+
const { existingStories, newStories } = await this.checkExistingStories(filteredStories, flags.prefix);
|
|
117
|
+
// Display summary before creation
|
|
118
|
+
this.logger.info({
|
|
119
|
+
existing: existingStories.length,
|
|
120
|
+
new: newStories.length,
|
|
121
|
+
total: filteredStories.length,
|
|
122
|
+
}, 'Story file check complete');
|
|
123
|
+
if (newStories.length === 0) {
|
|
124
|
+
this.log(colors.info('All story files already exist, nothing to create'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Create stories with parallel processing
|
|
128
|
+
const results = await this.createStoriesInParallel(newStories, epicPath, flags);
|
|
129
|
+
// Display summary report
|
|
130
|
+
const duration = Date.now() - startTime;
|
|
131
|
+
this.displaySummaryReport(results, existingStories.length, duration);
|
|
132
|
+
this.logger.info({
|
|
133
|
+
correlationId,
|
|
134
|
+
created: results.filter((r) => r.success).length,
|
|
135
|
+
duration,
|
|
136
|
+
failed: results.filter((r) => !r.success).length,
|
|
137
|
+
}, 'Stories create command completed');
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const err = error;
|
|
141
|
+
this.logger.error({ correlationId, error: err }, 'Command failed');
|
|
142
|
+
this.error(colors.error(err.message), { exit: 1 });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build Claude CLI prompt for story creation
|
|
147
|
+
*/
|
|
148
|
+
buildClaudePrompt(options) {
|
|
149
|
+
const { agent = 'sm', epicPath, references, story, storyFilePath, task = 'draft' } = options;
|
|
150
|
+
const agentFile = agent;
|
|
151
|
+
const taskCommand = task;
|
|
152
|
+
let prompt = `@.bmad-core/agents/${agentFile}.md\n\n`;
|
|
153
|
+
prompt += 'Create story document from epic story definition:\n\n';
|
|
154
|
+
prompt += `Epic Number: ${story.epicNumber}\n`;
|
|
155
|
+
prompt += `Story Number: ${story.number}\n`;
|
|
156
|
+
prompt += `Story Title: ${story.title}\n\n`;
|
|
157
|
+
prompt += `Target file: @${storyFilePath}\n\n`;
|
|
158
|
+
prompt += 'References:\n';
|
|
159
|
+
prompt += `@${epicPath}\n`;
|
|
160
|
+
if (references && references.length > 0) {
|
|
161
|
+
for (const ref of references) {
|
|
162
|
+
prompt += `@${path.resolve(ref)}\n`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
prompt += '\nIMPORTANT: The target file has been pre-scaffolded with structure and metadata.\n';
|
|
166
|
+
prompt += '- DO NOT modify the Status section (already set to Draft)\n';
|
|
167
|
+
prompt += '- DO NOT modify the Created date in Change Log\n';
|
|
168
|
+
prompt += '- DO NOT change the document structure or section headers\n';
|
|
169
|
+
prompt += '- ONLY populate the empty content sections marked with [AI Agent will populate]\n';
|
|
170
|
+
prompt += '- Follow the template structure at @.bmad-core/templates/story-tmpl.yaml\n\n';
|
|
171
|
+
prompt += `Execute the *${taskCommand} command to populate the story document.\n`;
|
|
172
|
+
prompt += 'Update the file at the target path with the story content in the empty sections.\n';
|
|
173
|
+
return prompt;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check for existing story files across all story directories
|
|
177
|
+
*
|
|
178
|
+
* Checks docs/stories, docs/qa/stories, and docs/done/stories to prevent
|
|
179
|
+
* recreating stories that have been moved for QA or marked as done.
|
|
180
|
+
*/
|
|
181
|
+
async checkExistingStories(stories, prefix) {
|
|
182
|
+
const allStoryDirs = this.pathResolver.getAllStoryDirs();
|
|
183
|
+
const existingStories = [];
|
|
184
|
+
const newStories = [];
|
|
185
|
+
this.logger.info({ directories: allStoryDirs }, 'Checking for existing story files across all directories');
|
|
186
|
+
/* eslint-disable no-await-in-loop */
|
|
187
|
+
for (const story of stories) {
|
|
188
|
+
const filename = this.generateStoryFilename(story, prefix);
|
|
189
|
+
let foundInAnyDir = false;
|
|
190
|
+
// Check all story directories
|
|
191
|
+
for (const dir of allStoryDirs) {
|
|
192
|
+
const filePath = path.join(dir, filename);
|
|
193
|
+
const exists = await this.fileManager.fileExists(filePath);
|
|
194
|
+
if (exists) {
|
|
195
|
+
this.logger.info({ directory: dir, filePath, storyNumber: story.fullNumber }, 'Story already exists');
|
|
196
|
+
foundInAnyDir = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (foundInAnyDir) {
|
|
201
|
+
existingStories.push(story);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.logger.debug({ filename, storyNumber: story.fullNumber }, 'Story needs to be created');
|
|
205
|
+
newStories.push(story);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/* eslint-enable no-await-in-loop */
|
|
209
|
+
return { existingStories, newStories };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Create stories in parallel batches
|
|
213
|
+
*/
|
|
214
|
+
async createStoriesInParallel(stories, epicPath, flags) {
|
|
215
|
+
const spinner = createSpinner('Creating stories...');
|
|
216
|
+
spinner.start();
|
|
217
|
+
const results = [];
|
|
218
|
+
try {
|
|
219
|
+
// Define processor function for each story
|
|
220
|
+
const processor = async (story) => this.createStory({
|
|
221
|
+
agent: flags.agent,
|
|
222
|
+
epicPath,
|
|
223
|
+
prefix: flags.prefix,
|
|
224
|
+
references: flags.reference,
|
|
225
|
+
story,
|
|
226
|
+
task: flags.task,
|
|
227
|
+
});
|
|
228
|
+
// Progress callback for batch updates
|
|
229
|
+
const onProgress = (info) => {
|
|
230
|
+
spinner.text = `Processing batch ${info.currentBatch} of ${info.totalBatches} (${info.completedItems}/${info.totalItems} completed)...`;
|
|
231
|
+
this.logger.info({
|
|
232
|
+
batchNum: info.currentBatch,
|
|
233
|
+
completedItems: info.completedItems,
|
|
234
|
+
totalBatches: info.totalBatches,
|
|
235
|
+
totalItems: info.totalItems,
|
|
236
|
+
}, 'Processing batch');
|
|
237
|
+
};
|
|
238
|
+
// Process stories in batches
|
|
239
|
+
const batchResults = await this.batchProcessor.processBatch(stories, processor, onProgress);
|
|
240
|
+
// Extract results from BatchResult wrapper
|
|
241
|
+
for (const batchResult of batchResults) {
|
|
242
|
+
if (batchResult.success && batchResult.result) {
|
|
243
|
+
results.push(batchResult.result);
|
|
244
|
+
}
|
|
245
|
+
else if (!batchResult.success) {
|
|
246
|
+
// Create error result for failed items
|
|
247
|
+
results.push({
|
|
248
|
+
error: batchResult.error?.message || 'Unknown error',
|
|
249
|
+
filePath: '',
|
|
250
|
+
story: stories[results.length],
|
|
251
|
+
success: false,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const successCount = results.filter((r) => r.success).length;
|
|
256
|
+
spinner.succeed(colors.success(`Created ${successCount} story files`));
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const err = error;
|
|
260
|
+
spinner.fail(colors.error(`Failed to create stories: ${err.message}`));
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Create a single story file
|
|
267
|
+
*/
|
|
268
|
+
async createStory(options) {
|
|
269
|
+
const { agent, epicPath, prefix, references, story, task } = options;
|
|
270
|
+
const storyDir = this.pathResolver.getStoryDir();
|
|
271
|
+
const filename = this.generateStoryFilename(story, prefix);
|
|
272
|
+
const filePath = path.join(storyDir, filename);
|
|
273
|
+
const absolutePath = path.resolve(filePath);
|
|
274
|
+
this.logger.info({ filePath, storyNumber: story.fullNumber }, 'Creating story file');
|
|
275
|
+
try {
|
|
276
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata
|
|
277
|
+
const scaffoldedContent = this.fileScaffolder.scaffoldStory({
|
|
278
|
+
epicNumber: story.epicNumber,
|
|
279
|
+
storyNumber: story.number,
|
|
280
|
+
storyTitle: story.title,
|
|
281
|
+
});
|
|
282
|
+
await this.fileManager.writeFile(filePath, scaffoldedContent);
|
|
283
|
+
this.logger.debug({ filePath }, 'Scaffolded file created');
|
|
284
|
+
// Step 2: Build Claude CLI prompt
|
|
285
|
+
const prompt = this.buildClaudePrompt({
|
|
286
|
+
agent,
|
|
287
|
+
epicPath,
|
|
288
|
+
references,
|
|
289
|
+
story,
|
|
290
|
+
storyFilePath: absolutePath,
|
|
291
|
+
task,
|
|
292
|
+
});
|
|
293
|
+
// Step 3: Run Claude agent
|
|
294
|
+
const result = await this.agentRunner.runAgent(prompt, {
|
|
295
|
+
agentType: 'sm',
|
|
296
|
+
timeout: 1_800_000, // 30 minutes
|
|
297
|
+
});
|
|
298
|
+
// Step 4: Verify file was updated
|
|
299
|
+
if (result.success) {
|
|
300
|
+
const content = await this.fileManager.readFile(filePath);
|
|
301
|
+
if (content === scaffoldedContent) {
|
|
302
|
+
throw new Error('AI agent did not update story file');
|
|
303
|
+
}
|
|
304
|
+
this.logger.info({ filePath, storyNumber: story.fullNumber }, 'Story created successfully');
|
|
305
|
+
return {
|
|
306
|
+
filePath,
|
|
307
|
+
story,
|
|
308
|
+
success: true,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// Cleanup placeholder file on failure
|
|
312
|
+
// await this.fileManager.deleteFile(filePath) // Not implemented in FileManager yet
|
|
313
|
+
throw new Error(`AI agent failed: ${result.errors}`);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
const err = error;
|
|
317
|
+
this.logger.error({ error: err, filePath, storyNumber: story.fullNumber }, 'Story creation failed');
|
|
318
|
+
return {
|
|
319
|
+
error: err.message,
|
|
320
|
+
filePath,
|
|
321
|
+
story,
|
|
322
|
+
success: false,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Display summary report
|
|
328
|
+
*/
|
|
329
|
+
displaySummaryReport(results, skippedCount, duration) {
|
|
330
|
+
const successCount = results.filter((r) => r.success).length;
|
|
331
|
+
const failedCount = results.filter((r) => !r.success).length;
|
|
332
|
+
// Box drawing
|
|
333
|
+
const boxTop = '┌─────────────────────────────────────────┐';
|
|
334
|
+
const boxDivider = '├─────────────────────────────────────────┤';
|
|
335
|
+
const boxBottom = '└─────────────────────────────────────────┘';
|
|
336
|
+
this.log('');
|
|
337
|
+
this.log(boxTop);
|
|
338
|
+
this.log('│ Story Creation Summary │');
|
|
339
|
+
this.log(boxDivider);
|
|
340
|
+
this.log(`│ ${colors.success('Created:')} ${successCount.toString().padEnd(20)}│`);
|
|
341
|
+
this.log(`│ ${colors.dim('Skipped:')} ${skippedCount.toString().padEnd(20)}│`);
|
|
342
|
+
if (failedCount > 0) {
|
|
343
|
+
this.log(`│ ${colors.error('Failed:')} ${failedCount.toString().padEnd(20)}│`);
|
|
344
|
+
}
|
|
345
|
+
this.log(boxDivider);
|
|
346
|
+
this.log(`│ Duration: ${(duration / 1000).toFixed(2)}s${' '.repeat(15)}│`);
|
|
347
|
+
this.log(boxBottom);
|
|
348
|
+
// List failures if any
|
|
349
|
+
if (failedCount > 0) {
|
|
350
|
+
this.log('');
|
|
351
|
+
this.log(colors.bold('Failed Stories:'));
|
|
352
|
+
for (const result of results.filter((r) => !r.success)) {
|
|
353
|
+
this.log(colors.error(` ✗ ${result.story.fullNumber}: ${result.story.title} - ${result.error || 'Unknown error'}`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
this.log('');
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Filter stories by start number
|
|
360
|
+
*/
|
|
361
|
+
filterStoriesByStart(stories, startNumber) {
|
|
362
|
+
if (!startNumber) {
|
|
363
|
+
return stories;
|
|
364
|
+
}
|
|
365
|
+
const filtered = stories.filter((story) => story.number >= startNumber);
|
|
366
|
+
this.logger.info({
|
|
367
|
+
filtered: filtered.length,
|
|
368
|
+
startNumber,
|
|
369
|
+
total: stories.length,
|
|
370
|
+
}, 'Filtered stories by start number');
|
|
371
|
+
return filtered;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Generate story filename from story metadata
|
|
375
|
+
*/
|
|
376
|
+
generateStoryFilename(story, prefix) {
|
|
377
|
+
// Generate slug from title
|
|
378
|
+
const slug = story.title
|
|
379
|
+
.toLowerCase()
|
|
380
|
+
.replaceAll(/[^\w\s-]/g, '')
|
|
381
|
+
.replaceAll(/\s+/g, '-')
|
|
382
|
+
.slice(0, 50);
|
|
383
|
+
// Build filename: {prefix}{epicNum}.{storyNum}-{slug}.md
|
|
384
|
+
const effectivePrefix = prefix || '';
|
|
385
|
+
return `${effectivePrefix}${story.epicNumber}.${story.number}-${slug}.md`;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Initialize service dependencies
|
|
389
|
+
*/
|
|
390
|
+
initializeServices(maxConcurrency, intervalSeconds, provider = 'claude') {
|
|
391
|
+
this.logger = createLogger({ namespace: 'commands:stories:create' });
|
|
392
|
+
this.logger.info({ provider }, 'Initializing services with AI provider');
|
|
393
|
+
this.fileManager = new FileManager(this.logger);
|
|
394
|
+
this.pathResolver = new PathResolver(this.fileManager, this.logger);
|
|
395
|
+
this.epicParser = new EpicParser(this.logger);
|
|
396
|
+
this.agentRunner = createAgentRunner(provider, this.logger);
|
|
397
|
+
this.batchProcessor = new BatchProcessor(maxConcurrency, intervalSeconds * 1000, this.logger);
|
|
398
|
+
this.fileScaffolder = new FileScaffolder(this.logger);
|
|
399
|
+
this.logger.debug({ provider }, 'Services initialized successfully');
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Parse epic file and extract stories
|
|
403
|
+
*/
|
|
404
|
+
async parseEpicStories(epicPath) {
|
|
405
|
+
this.logger.info({ epicPath }, 'Reading epic file');
|
|
406
|
+
// Read epic file
|
|
407
|
+
const epicContent = await this.fileManager.readFile(epicPath);
|
|
408
|
+
this.logger.debug({ epicPath, length: epicContent.length }, 'Epic file read');
|
|
409
|
+
// Parse stories from epic
|
|
410
|
+
const stories = this.epicParser.parseStories(epicContent, epicPath);
|
|
411
|
+
if (stories.length === 0) {
|
|
412
|
+
throw new ValidationError('No stories found in epic. Expected format: ### Story 1.1: Title', {
|
|
413
|
+
epicPath,
|
|
414
|
+
suggestion: 'Ensure epic has story headers like: ### Story 1.1: Initialize Project',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
this.logger.info({ epicPath, storyCount: stories.length }, 'Stories extracted from epic successfully');
|
|
418
|
+
return stories;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Validate parallel flag value
|
|
422
|
+
*/
|
|
423
|
+
validateParallelFlag(parallel) {
|
|
424
|
+
if (parallel < 1 || parallel > 10) {
|
|
425
|
+
throw new ValidationError('Parallel flag must be between 1 and 10 to prevent overwhelming the system', {
|
|
426
|
+
actual: parallel,
|
|
427
|
+
suggestion: 'Use --parallel with a value between 1 and 10',
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stories Develop Command
|
|
3
|
+
*
|
|
4
|
+
* Executes development workflow for stories matching glob patterns sequentially.
|
|
5
|
+
* Updates story status from Draft to Ready, spawns Claude dev agents, and moves
|
|
6
|
+
* completed stories to QA folder. Optionally runs QA workflow after development.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```bash
|
|
10
|
+
* bmad-workflow stories develop "docs/stories/AUTH-*.md"
|
|
11
|
+
* bmad-workflow stories develop "stories/**-auth-*.md" --interval 60
|
|
12
|
+
* bmad-workflow stories develop "stories/*.md" --reference docs/architecture.md
|
|
13
|
+
* bmad-workflow stories develop "stories/*.md" --qa --qa-retries 2
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from '@oclif/core';
|
|
17
|
+
/**
|
|
18
|
+
* Stories Develop Command
|
|
19
|
+
*
|
|
20
|
+
* Develops stories sequentially using Claude AI dev agents.
|
|
21
|
+
* CRITICAL: No parallel execution - stories are developed one at a time to prevent conflicts.
|
|
22
|
+
*/
|
|
23
|
+
export default class StoriesDevelopCommand extends Command {
|
|
24
|
+
static args: {
|
|
25
|
+
pattern: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
26
|
+
};
|
|
27
|
+
static description: string;
|
|
28
|
+
static examples: {
|
|
29
|
+
command: string;
|
|
30
|
+
description: string;
|
|
31
|
+
}[];
|
|
32
|
+
static flags: {
|
|
33
|
+
interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
34
|
+
qa: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
35
|
+
'qa-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
36
|
+
'qa-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
37
|
+
reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
38
|
+
agent: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
39
|
+
cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
40
|
+
provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
41
|
+
task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
42
|
+
};
|
|
43
|
+
private agentRunner;
|
|
44
|
+
private fileManager;
|
|
45
|
+
private globMatcher;
|
|
46
|
+
private logger;
|
|
47
|
+
private pathResolver;
|
|
48
|
+
private storyParserFactory;
|
|
49
|
+
private storyTypeDetector;
|
|
50
|
+
/**
|
|
51
|
+
* Run the command
|
|
52
|
+
*/
|
|
53
|
+
run(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Build Claude CLI prompt for story development
|
|
56
|
+
*/
|
|
57
|
+
private buildClaudePrompt;
|
|
58
|
+
/**
|
|
59
|
+
* Develop stories sequentially (CRITICAL: No parallelism)
|
|
60
|
+
*/
|
|
61
|
+
private developStoriesSequentially;
|
|
62
|
+
/**
|
|
63
|
+
* Develop a single story
|
|
64
|
+
*/
|
|
65
|
+
private developStory;
|
|
66
|
+
/**
|
|
67
|
+
* Display countdown timer between stories
|
|
68
|
+
*/
|
|
69
|
+
private displayCountdown;
|
|
70
|
+
/**
|
|
71
|
+
* Display summary report
|
|
72
|
+
*/
|
|
73
|
+
private displaySummaryReport;
|
|
74
|
+
/**
|
|
75
|
+
* Initialize service dependencies
|
|
76
|
+
*/
|
|
77
|
+
private initializeServices;
|
|
78
|
+
/**
|
|
79
|
+
* Match story files using glob pattern
|
|
80
|
+
*/
|
|
81
|
+
private matchStoryFiles;
|
|
82
|
+
/**
|
|
83
|
+
* Run QA workflow for successfully developed stories
|
|
84
|
+
* Dynamically imports and delegates to StoriesQaCommand
|
|
85
|
+
*/
|
|
86
|
+
private runQaWorkflow;
|
|
87
|
+
/**
|
|
88
|
+
* Sleep for specified milliseconds
|
|
89
|
+
*/
|
|
90
|
+
private sleep;
|
|
91
|
+
}
|