@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,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stories QA Command
|
|
3
|
+
*
|
|
4
|
+
* Executes QA workflow for stories matching glob patterns sequentially.
|
|
5
|
+
* Runs QA agent for deep dive review, then Dev agent for fix-forward,
|
|
6
|
+
* with configurable retry loops until gate passes or max retries reached.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```bash
|
|
10
|
+
* bmad-workflow stories qa "docs/qa/stories/AUTH-*.md"
|
|
11
|
+
* bmad-workflow stories qa "docs/qa/stories/*.md" --max-retries 3
|
|
12
|
+
* bmad-workflow stories qa "stories/*.md" --qa-prompt "Focus on security"
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from '@oclif/core';
|
|
16
|
+
/**
|
|
17
|
+
* Stories QA Command
|
|
18
|
+
*
|
|
19
|
+
* Runs QA review and dev fix-forward cycles for stories sequentially.
|
|
20
|
+
* CRITICAL: No parallel execution - stories are processed one at a time.
|
|
21
|
+
*/
|
|
22
|
+
export default class StoriesQaCommand extends Command {
|
|
23
|
+
static args: {
|
|
24
|
+
pattern: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
25
|
+
};
|
|
26
|
+
static description: string;
|
|
27
|
+
static examples: {
|
|
28
|
+
command: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}[];
|
|
31
|
+
static flags: {
|
|
32
|
+
'dev-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
33
|
+
interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
34
|
+
'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
35
|
+
provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
36
|
+
'qa-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
37
|
+
reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
38
|
+
};
|
|
39
|
+
private agentRunner;
|
|
40
|
+
private fileManager;
|
|
41
|
+
private globMatcher;
|
|
42
|
+
private logger;
|
|
43
|
+
private pathResolver;
|
|
44
|
+
private storyParserFactory;
|
|
45
|
+
/**
|
|
46
|
+
* Run the command
|
|
47
|
+
*/
|
|
48
|
+
run(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Append QA workflow summary to story file
|
|
51
|
+
*/
|
|
52
|
+
private appendQaSummaryToStory;
|
|
53
|
+
/**
|
|
54
|
+
* Build Claude CLI prompt for Dev fix-forward
|
|
55
|
+
*/
|
|
56
|
+
private buildDevFixPrompt;
|
|
57
|
+
/**
|
|
58
|
+
* Build Claude CLI prompt for QA review
|
|
59
|
+
*/
|
|
60
|
+
private buildQaPrompt;
|
|
61
|
+
/**
|
|
62
|
+
* Display countdown timer between stories
|
|
63
|
+
*/
|
|
64
|
+
private displayCountdown;
|
|
65
|
+
/**
|
|
66
|
+
* Display summary report
|
|
67
|
+
*/
|
|
68
|
+
private displaySummaryReport;
|
|
69
|
+
/**
|
|
70
|
+
* Extract gate status from story content or gate file
|
|
71
|
+
* This is a simple heuristic - looks for Gate: STATUS pattern in story
|
|
72
|
+
* IMPORTANT: Returns the LAST occurrence since QA reviews are appended
|
|
73
|
+
*/
|
|
74
|
+
private extractGateStatus;
|
|
75
|
+
/**
|
|
76
|
+
* Initialize service dependencies
|
|
77
|
+
*/
|
|
78
|
+
private initializeServices;
|
|
79
|
+
/**
|
|
80
|
+
* Match story files using glob pattern
|
|
81
|
+
*/
|
|
82
|
+
private matchStoryFiles;
|
|
83
|
+
/**
|
|
84
|
+
* Move story to appropriate folder based on gate status
|
|
85
|
+
*/
|
|
86
|
+
private moveStoryBasedOnGate;
|
|
87
|
+
/**
|
|
88
|
+
* Process stories sequentially through QA workflow
|
|
89
|
+
*/
|
|
90
|
+
private qaStoriesSequentially;
|
|
91
|
+
/**
|
|
92
|
+
* Process a single story through QA workflow
|
|
93
|
+
*/
|
|
94
|
+
private qaStory;
|
|
95
|
+
/**
|
|
96
|
+
* Sleep for specified milliseconds
|
|
97
|
+
*/
|
|
98
|
+
private sleep;
|
|
99
|
+
}
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stories QA Command
|
|
3
|
+
*
|
|
4
|
+
* Executes QA workflow for stories matching glob patterns sequentially.
|
|
5
|
+
* Runs QA agent for deep dive review, then Dev agent for fix-forward,
|
|
6
|
+
* with configurable retry loops until gate passes or max retries reached.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```bash
|
|
10
|
+
* bmad-workflow stories qa "docs/qa/stories/AUTH-*.md"
|
|
11
|
+
* bmad-workflow stories qa "docs/qa/stories/*.md" --max-retries 3
|
|
12
|
+
* bmad-workflow stories qa "stories/*.md" --qa-prompt "Focus on security"
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { isEpicStory } from '../../models/story.js';
|
|
18
|
+
import { createAgentRunner } from '../../services/agents/agent-runner-factory.js';
|
|
19
|
+
import { FileManager } from '../../services/file-system/file-manager.js';
|
|
20
|
+
import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
|
|
21
|
+
import { PathResolver } from '../../services/file-system/path-resolver.js';
|
|
22
|
+
import { StoryParserFactory } from '../../services/parsers/story-parser-factory.js';
|
|
23
|
+
import * as colors from '../../utils/colors.js';
|
|
24
|
+
import { createLogger, generateCorrelationId } from '../../utils/logger.js';
|
|
25
|
+
import { createSpinner } from '../../utils/progress.js';
|
|
26
|
+
/**
|
|
27
|
+
* Agent timeout in milliseconds (30 minutes)
|
|
28
|
+
* QA and Dev agents need longer timeouts for comprehensive analysis
|
|
29
|
+
*/
|
|
30
|
+
const AGENT_TIMEOUT_MS = 1_800_000;
|
|
31
|
+
/**
|
|
32
|
+
* Stories QA Command
|
|
33
|
+
*
|
|
34
|
+
* Runs QA review and dev fix-forward cycles for stories sequentially.
|
|
35
|
+
* CRITICAL: No parallel execution - stories are processed one at a time.
|
|
36
|
+
*/
|
|
37
|
+
export default class StoriesQaCommand extends Command {
|
|
38
|
+
static args = {
|
|
39
|
+
pattern: Args.string({
|
|
40
|
+
description: 'Glob pattern to match story files',
|
|
41
|
+
required: true,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
static description = 'Execute QA workflow for stories: deep dive review, dev fix-forward, and gate validation';
|
|
45
|
+
static examples = [
|
|
46
|
+
{
|
|
47
|
+
command: '<%= config.bin %> <%= command.id %> "docs/qa/stories/AUTH-*.md"',
|
|
48
|
+
description: 'QA all AUTH stories in qa folder',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
command: '<%= config.bin %> <%= command.id %> "docs/qa/stories/*.md" --max-retries 3',
|
|
52
|
+
description: 'QA stories with up to 3 fix-forward cycles',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
command: '<%= config.bin %> <%= command.id %> "stories/*.md" --qa-prompt "Focus on security vulnerabilities"',
|
|
56
|
+
description: 'QA with custom prompt instructions',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
command: '<%= config.bin %> <%= command.id %> "stories/*.md" --interval 60',
|
|
60
|
+
description: 'QA stories with 60s countdown between each',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
static flags = {
|
|
64
|
+
'dev-prompt': Flags.string({
|
|
65
|
+
description: 'Custom prompt/instructions for dev fix-forward phase',
|
|
66
|
+
helpGroup: 'Prompt Customization',
|
|
67
|
+
}),
|
|
68
|
+
interval: Flags.integer({
|
|
69
|
+
default: 30,
|
|
70
|
+
description: 'Seconds to wait between stories',
|
|
71
|
+
}),
|
|
72
|
+
'max-retries': Flags.integer({
|
|
73
|
+
default: 2,
|
|
74
|
+
description: 'Maximum QA → Dev fix cycles before giving up (0 = QA only, no fix-forward)',
|
|
75
|
+
}),
|
|
76
|
+
provider: Flags.string({
|
|
77
|
+
default: 'claude',
|
|
78
|
+
description: 'AI provider to use (claude or gemini)',
|
|
79
|
+
options: ['claude', 'gemini'],
|
|
80
|
+
}),
|
|
81
|
+
'qa-prompt': Flags.string({
|
|
82
|
+
description: 'Custom prompt/instructions for QA review phase',
|
|
83
|
+
helpGroup: 'Prompt Customization',
|
|
84
|
+
}),
|
|
85
|
+
reference: Flags.string({
|
|
86
|
+
description: 'Additional context files for agents',
|
|
87
|
+
multiple: true,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
// Service instances
|
|
91
|
+
agentRunner;
|
|
92
|
+
fileManager;
|
|
93
|
+
globMatcher;
|
|
94
|
+
logger;
|
|
95
|
+
pathResolver;
|
|
96
|
+
storyParserFactory;
|
|
97
|
+
/**
|
|
98
|
+
* Run the command
|
|
99
|
+
*/
|
|
100
|
+
async run() {
|
|
101
|
+
const { args, flags } = await this.parse(StoriesQaCommand);
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
const correlationId = generateCorrelationId();
|
|
104
|
+
// Initialize services with selected provider
|
|
105
|
+
const provider = (flags.provider || 'claude');
|
|
106
|
+
this.initializeServices(provider);
|
|
107
|
+
this.logger.info({
|
|
108
|
+
correlationId,
|
|
109
|
+
flags,
|
|
110
|
+
pattern: args.pattern,
|
|
111
|
+
}, 'Starting stories QA command');
|
|
112
|
+
try {
|
|
113
|
+
// Match story files using glob pattern
|
|
114
|
+
const storyFiles = await this.matchStoryFiles(args.pattern);
|
|
115
|
+
if (storyFiles.length === 0) {
|
|
116
|
+
this.log(colors.warning(`No story files matched pattern: ${args.pattern}`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.log(colors.info(`Found ${storyFiles.length} story file(s) to QA`));
|
|
120
|
+
this.log('');
|
|
121
|
+
// Process stories sequentially
|
|
122
|
+
const results = await this.qaStoriesSequentially(storyFiles, flags);
|
|
123
|
+
// Display summary report
|
|
124
|
+
const duration = Date.now() - startTime;
|
|
125
|
+
this.displaySummaryReport(results, duration);
|
|
126
|
+
this.logger.info({
|
|
127
|
+
correlationId,
|
|
128
|
+
duration,
|
|
129
|
+
failed: results.filter((r) => !r.success).length,
|
|
130
|
+
passed: results.filter((r) => r.finalGate === 'PASS').length,
|
|
131
|
+
succeeded: results.filter((r) => r.success).length,
|
|
132
|
+
total: results.length,
|
|
133
|
+
}, 'Stories QA command completed');
|
|
134
|
+
// Exit with error if any stories failed
|
|
135
|
+
const failedCount = results.filter((r) => !r.success).length;
|
|
136
|
+
if (failedCount > 0) {
|
|
137
|
+
this.error(colors.error(`${failedCount} story QA(s) failed`), { exit: 1 });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const err = error;
|
|
142
|
+
this.logger.error({ correlationId, error: err }, 'Command failed');
|
|
143
|
+
this.error(colors.error(err.message), { exit: 1 });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Append QA workflow summary to story file
|
|
148
|
+
*/
|
|
149
|
+
async appendQaSummaryToStory(storyPath, result, totalRetries) {
|
|
150
|
+
const content = await this.fileManager.readFile(storyPath);
|
|
151
|
+
const timestamp = new Date().toISOString();
|
|
152
|
+
const summary = `
|
|
153
|
+
|
|
154
|
+
## QA Workflow Summary
|
|
155
|
+
|
|
156
|
+
### Workflow Date: ${timestamp.split('T')[0]}
|
|
157
|
+
|
|
158
|
+
### Final Gate Status: ${result.finalGate}
|
|
159
|
+
|
|
160
|
+
### Workflow Statistics
|
|
161
|
+
- Retries Used: ${result.retriesUsed}/${totalRetries}
|
|
162
|
+
- Final Location: ${result.movedTo === 'done' ? 'docs/done/stories' : result.movedTo === 'stories' ? 'docs/stories (returned for rework)' : 'unchanged'}
|
|
163
|
+
- Success: ${result.success ? 'Yes' : 'No'}
|
|
164
|
+
${result.error ? `- Error: ${result.error}` : ''}
|
|
165
|
+
|
|
166
|
+
### Workflow Phases Completed
|
|
167
|
+
1. [${result.retriesUsed >= 0 ? 'x' : ' '}] Initial QA Deep Dive Review
|
|
168
|
+
${result.retriesUsed > 0 ? `2. [x] Dev Fix-Forward (${result.retriesUsed} cycle${result.retriesUsed > 1 ? 's' : ''})` : ''}
|
|
169
|
+
${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.finalGate === 'WAIVED' ? '3. [x] Final QA Validation - WAIVED' : '3. [ ] Final QA Validation - Not Passed'}
|
|
170
|
+
`;
|
|
171
|
+
// Append summary to file
|
|
172
|
+
await this.fileManager.writeFile(storyPath, content + summary);
|
|
173
|
+
this.logger.info({ storyPath }, 'Appended QA workflow summary to story');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Build Claude CLI prompt for Dev fix-forward
|
|
177
|
+
*/
|
|
178
|
+
buildDevFixPrompt(storyPath, customPrompt, references) {
|
|
179
|
+
let prompt = `@.bmad-core/agents/dev.md\n\n`;
|
|
180
|
+
prompt += `*review-qa ${storyPath}\n\n`;
|
|
181
|
+
if (references && references.length > 0) {
|
|
182
|
+
prompt += 'References:\n';
|
|
183
|
+
for (const ref of references) {
|
|
184
|
+
const resolvedPath = path.resolve(ref);
|
|
185
|
+
prompt += `@${resolvedPath}\n`;
|
|
186
|
+
}
|
|
187
|
+
prompt += '\n';
|
|
188
|
+
}
|
|
189
|
+
// Default dev fix-forward instructions
|
|
190
|
+
const defaultInstructions = `Fix forward based on QA findings:
|
|
191
|
+
- Read the QA Results section and gate file for this story
|
|
192
|
+
- Address all high-severity issues first
|
|
193
|
+
- Fix medium and low severity issues
|
|
194
|
+
- Add missing tests to close coverage gaps
|
|
195
|
+
- Update the story's Dev Agent Record section
|
|
196
|
+
- Run all tests to ensure fixes don't break functionality
|
|
197
|
+
- Set status to Ready for Review when complete`;
|
|
198
|
+
prompt += customPrompt || defaultInstructions;
|
|
199
|
+
prompt += '\n\n*yolo mode* always update the story file as you go\n';
|
|
200
|
+
return prompt;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Build Claude CLI prompt for QA review
|
|
204
|
+
*/
|
|
205
|
+
buildQaPrompt(storyPath, customPrompt, references) {
|
|
206
|
+
let prompt = `@.bmad-core/agents/qa.md\n\n`;
|
|
207
|
+
prompt += `*review ${storyPath}\n\n`;
|
|
208
|
+
if (references && references.length > 0) {
|
|
209
|
+
prompt += 'References:\n';
|
|
210
|
+
for (const ref of references) {
|
|
211
|
+
const resolvedPath = path.resolve(ref);
|
|
212
|
+
prompt += `@${resolvedPath}\n`;
|
|
213
|
+
}
|
|
214
|
+
prompt += '\n';
|
|
215
|
+
}
|
|
216
|
+
// Default QA deep dive instructions
|
|
217
|
+
const defaultInstructions = `Perform a comprehensive deep dive review of this story implementation:
|
|
218
|
+
- Analyze each acceptance criterion against the actual implementation
|
|
219
|
+
- Review code quality, architecture patterns, and best practices
|
|
220
|
+
- Check test coverage and test quality
|
|
221
|
+
- Identify security vulnerabilities and performance issues
|
|
222
|
+
- Validate error handling and edge cases
|
|
223
|
+
- Assess technical debt and maintainability
|
|
224
|
+
- Update the story's QA Results section with detailed findings
|
|
225
|
+
- Create/update the gate file with your assessment (PASS/CONCERNS/FAIL/WAIVED)`;
|
|
226
|
+
prompt += customPrompt || defaultInstructions;
|
|
227
|
+
prompt += '\n\n*yolo mode* always update the story file as you go\n';
|
|
228
|
+
return prompt;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Display countdown timer between stories
|
|
232
|
+
*/
|
|
233
|
+
async displayCountdown(intervalSeconds) {
|
|
234
|
+
/* eslint-disable no-await-in-loop */
|
|
235
|
+
for (let remaining = intervalSeconds; remaining > 0; remaining--) {
|
|
236
|
+
process.stdout.write(`\r${colors.warning(`⏳ Next story in ${remaining}s...`)}`);
|
|
237
|
+
await this.sleep(1000);
|
|
238
|
+
}
|
|
239
|
+
/* eslint-enable no-await-in-loop */
|
|
240
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r'); // Clear countdown line
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Display summary report
|
|
244
|
+
*/
|
|
245
|
+
displaySummaryReport(results, duration) {
|
|
246
|
+
const passCount = results.filter((r) => r.finalGate === 'PASS').length;
|
|
247
|
+
const concernsCount = results.filter((r) => r.finalGate === 'CONCERNS').length;
|
|
248
|
+
const failCount = results.filter((r) => r.finalGate === 'FAIL').length;
|
|
249
|
+
const waivedCount = results.filter((r) => r.finalGate === 'WAIVED').length;
|
|
250
|
+
const errorCount = results.filter((r) => !r.success).length;
|
|
251
|
+
const movedToDone = results.filter((r) => r.movedTo === 'done').length;
|
|
252
|
+
const movedBack = results.filter((r) => r.movedTo === 'stories').length;
|
|
253
|
+
// Box drawing
|
|
254
|
+
const boxTop = '┌─────────────────────────────────────────┐';
|
|
255
|
+
const boxDivider = '├─────────────────────────────────────────┤';
|
|
256
|
+
const boxBottom = '└─────────────────────────────────────────┘';
|
|
257
|
+
this.log('');
|
|
258
|
+
this.log(boxTop);
|
|
259
|
+
this.log('│ Story QA Summary │');
|
|
260
|
+
this.log(boxDivider);
|
|
261
|
+
this.log(`│ ${colors.success('PASS:')} ${passCount.toString().padEnd(20)}│`);
|
|
262
|
+
if (concernsCount > 0) {
|
|
263
|
+
this.log(`│ ${colors.warning('CONCERNS:')} ${concernsCount.toString().padEnd(20)}│`);
|
|
264
|
+
}
|
|
265
|
+
if (failCount > 0) {
|
|
266
|
+
this.log(`│ ${colors.error('FAIL:')} ${failCount.toString().padEnd(20)}│`);
|
|
267
|
+
}
|
|
268
|
+
if (waivedCount > 0) {
|
|
269
|
+
this.log(`│ WAIVED: ${waivedCount.toString().padEnd(20)}│`);
|
|
270
|
+
}
|
|
271
|
+
if (errorCount > 0) {
|
|
272
|
+
this.log(`│ ${colors.error('Errors:')} ${errorCount.toString().padEnd(20)}│`);
|
|
273
|
+
}
|
|
274
|
+
this.log(boxDivider);
|
|
275
|
+
this.log(`│ Moved to Done: ${movedToDone.toString().padEnd(20)}│`);
|
|
276
|
+
this.log(`│ Returned to Dev: ${movedBack.toString().padEnd(20)}│`);
|
|
277
|
+
this.log(boxDivider);
|
|
278
|
+
this.log(`│ Duration: ${(duration / 1000).toFixed(2)}s${' '.repeat(15)}│`);
|
|
279
|
+
this.log(boxBottom);
|
|
280
|
+
// List details for non-PASS stories
|
|
281
|
+
const nonPassStories = results.filter((r) => r.finalGate !== 'PASS' || !r.success);
|
|
282
|
+
if (nonPassStories.length > 0) {
|
|
283
|
+
this.log('');
|
|
284
|
+
this.log(colors.bold('Stories Requiring Attention:'));
|
|
285
|
+
for (const result of nonPassStories) {
|
|
286
|
+
const status = result.success ? result.finalGate : 'ERROR';
|
|
287
|
+
const statusColor = status === 'CONCERNS' ? colors.warning : colors.error;
|
|
288
|
+
this.log(statusColor(` ${status}: ${result.storyNumber} - ${path.basename(result.storyPath)}${result.error ? ` (${result.error})` : ''}`));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
this.log('');
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Extract gate status from story content or gate file
|
|
295
|
+
* This is a simple heuristic - looks for Gate: STATUS pattern in story
|
|
296
|
+
* IMPORTANT: Returns the LAST occurrence since QA reviews are appended
|
|
297
|
+
*/
|
|
298
|
+
extractGateStatus(storyContent) {
|
|
299
|
+
// Look for ALL "Gate: STATUS" patterns and use the LAST one (most recent review)
|
|
300
|
+
const gateMatches = [...storyContent.matchAll(/\*?\*?Gate:\*?\*?\s*(PASS|CONCERNS|FAIL|WAIVED)/gi)];
|
|
301
|
+
if (gateMatches.length > 0) {
|
|
302
|
+
const lastMatch = gateMatches.at(-1);
|
|
303
|
+
return lastMatch[1].toUpperCase();
|
|
304
|
+
}
|
|
305
|
+
// Look for "Gate Status" section with status (also get last match)
|
|
306
|
+
const statusMatches = [...storyContent.matchAll(/###\s*Gate\s*Status[\s\S]*?(PASS|CONCERNS|FAIL|WAIVED)/gi)];
|
|
307
|
+
if (statusMatches.length > 0) {
|
|
308
|
+
const lastMatch = statusMatches.at(-1);
|
|
309
|
+
return lastMatch[1].toUpperCase();
|
|
310
|
+
}
|
|
311
|
+
return 'UNKNOWN';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Initialize service dependencies
|
|
315
|
+
*/
|
|
316
|
+
initializeServices(provider = 'claude') {
|
|
317
|
+
this.logger = createLogger({ namespace: 'commands:stories:qa' });
|
|
318
|
+
this.logger.info({ provider }, 'Initializing services with AI provider');
|
|
319
|
+
this.fileManager = new FileManager(this.logger);
|
|
320
|
+
this.pathResolver = new PathResolver(this.fileManager, this.logger);
|
|
321
|
+
this.globMatcher = new GlobMatcher(this.fileManager, this.logger);
|
|
322
|
+
this.storyParserFactory = new StoryParserFactory(this.fileManager, this.logger);
|
|
323
|
+
this.agentRunner = createAgentRunner(provider, this.logger);
|
|
324
|
+
this.logger.debug({ provider }, 'Services initialized successfully');
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Match story files using glob pattern
|
|
328
|
+
*/
|
|
329
|
+
async matchStoryFiles(pattern) {
|
|
330
|
+
this.logger.info({ pattern }, 'Matching story files with glob pattern');
|
|
331
|
+
// Expand glob pattern
|
|
332
|
+
const matches = await this.globMatcher.expandPattern(pattern);
|
|
333
|
+
this.logger.info({ matchCount: matches.length, pattern }, 'Glob pattern matching complete');
|
|
334
|
+
return matches;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Move story to appropriate folder based on gate status
|
|
338
|
+
*/
|
|
339
|
+
async moveStoryBasedOnGate(storyPath, gateStatus) {
|
|
340
|
+
try {
|
|
341
|
+
if (gateStatus === 'PASS' || gateStatus === 'WAIVED') {
|
|
342
|
+
// Move to done/stories
|
|
343
|
+
const doneStoryDir = this.pathResolver.getDoneStoryDir();
|
|
344
|
+
const destPath = path.join(doneStoryDir, path.basename(storyPath));
|
|
345
|
+
// Ensure done directory exists
|
|
346
|
+
await this.fileManager.createDirectory(doneStoryDir);
|
|
347
|
+
await this.fileManager.moveFile(storyPath, destPath);
|
|
348
|
+
this.logger.info({ destPath, gateStatus, storyPath }, 'Moved story to done folder');
|
|
349
|
+
return 'done';
|
|
350
|
+
}
|
|
351
|
+
if (gateStatus === 'FAIL' || gateStatus === 'CONCERNS') {
|
|
352
|
+
// Move back to stories for rework
|
|
353
|
+
const storyDir = this.pathResolver.getStoryDir();
|
|
354
|
+
const destPath = path.join(storyDir, path.basename(storyPath));
|
|
355
|
+
// Only move if not already in stories dir (normalize paths for comparison)
|
|
356
|
+
if (path.resolve(path.dirname(storyPath)) !== path.resolve(storyDir)) {
|
|
357
|
+
await this.fileManager.moveFile(storyPath, destPath);
|
|
358
|
+
this.logger.info({ destPath, gateStatus, storyPath }, 'Moved story back to stories folder for rework');
|
|
359
|
+
return 'stories';
|
|
360
|
+
}
|
|
361
|
+
return 'none';
|
|
362
|
+
}
|
|
363
|
+
return 'none';
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const err = error;
|
|
367
|
+
this.logger.error({ error: err, storyPath }, 'Failed to move story');
|
|
368
|
+
return 'none';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Process stories sequentially through QA workflow
|
|
373
|
+
*/
|
|
374
|
+
async qaStoriesSequentially(storyFiles, flags) {
|
|
375
|
+
const results = [];
|
|
376
|
+
const total = storyFiles.length;
|
|
377
|
+
/* eslint-disable no-await-in-loop */
|
|
378
|
+
// Sequential loop - DO NOT use Promise.all - await in loop is REQUIRED here
|
|
379
|
+
for (let index = 0; index < storyFiles.length; index++) {
|
|
380
|
+
const storyPath = storyFiles[index];
|
|
381
|
+
const storyNum = index + 1;
|
|
382
|
+
this.log(colors.bold(`\n[${storyNum}/${total}] QA: ${path.basename(storyPath)}`));
|
|
383
|
+
// Parse story metadata
|
|
384
|
+
const spinner = createSpinner('Parsing story metadata...');
|
|
385
|
+
spinner.start();
|
|
386
|
+
let storyMetadata;
|
|
387
|
+
try {
|
|
388
|
+
storyMetadata = await this.storyParserFactory.parseStory(storyPath);
|
|
389
|
+
const storyId = storyMetadata.id;
|
|
390
|
+
spinner.succeed(colors.success(`Story ${storyId}: ${storyMetadata.title}`));
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
const err = error;
|
|
394
|
+
spinner.fail(colors.error(`Failed to parse story: ${err.message}`));
|
|
395
|
+
results.push({
|
|
396
|
+
error: `Parse error: ${err.message}`,
|
|
397
|
+
finalGate: 'UNKNOWN',
|
|
398
|
+
movedTo: 'none',
|
|
399
|
+
retriesUsed: 0,
|
|
400
|
+
storyNumber: 'unknown',
|
|
401
|
+
storyPath,
|
|
402
|
+
success: false,
|
|
403
|
+
});
|
|
404
|
+
continue; // Skip to next story
|
|
405
|
+
}
|
|
406
|
+
// Run QA workflow
|
|
407
|
+
const result = await this.qaStory(storyPath, storyMetadata, {
|
|
408
|
+
'dev-prompt': flags['dev-prompt'],
|
|
409
|
+
'max-retries': flags['max-retries'],
|
|
410
|
+
'qa-prompt': flags['qa-prompt'],
|
|
411
|
+
reference: flags.reference,
|
|
412
|
+
});
|
|
413
|
+
results.push(result);
|
|
414
|
+
// Display result
|
|
415
|
+
if (result.finalGate === 'PASS') {
|
|
416
|
+
this.log(colors.success(` ✓ PASSED - Moved to done`));
|
|
417
|
+
}
|
|
418
|
+
else if (result.finalGate === 'WAIVED') {
|
|
419
|
+
this.log(colors.warning(` ⚠ WAIVED - Moved to done`));
|
|
420
|
+
}
|
|
421
|
+
else if (result.success) {
|
|
422
|
+
this.log(colors.warning(` ⚠ ${result.finalGate} - Returned for rework`));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
this.log(colors.error(` ✗ Error: ${result.error}`));
|
|
426
|
+
}
|
|
427
|
+
// Display countdown timer before next story (except for last story)
|
|
428
|
+
if (index < storyFiles.length - 1) {
|
|
429
|
+
await this.displayCountdown(flags.interval);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/* eslint-enable no-await-in-loop */
|
|
433
|
+
return results;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Process a single story through QA workflow
|
|
437
|
+
*/
|
|
438
|
+
async qaStory(storyPath, storyMetadata, flags) {
|
|
439
|
+
const storyNumber = isEpicStory(storyMetadata) ? storyMetadata.number : storyMetadata.id;
|
|
440
|
+
let retriesUsed = 0;
|
|
441
|
+
let currentGate = 'UNKNOWN';
|
|
442
|
+
this.logger.info({ storyNumber, storyPath }, 'Starting story QA workflow');
|
|
443
|
+
try {
|
|
444
|
+
// Phase 1: Initial QA Deep Dive
|
|
445
|
+
this.log(colors.info(' Phase 1: QA Deep Dive Review...'));
|
|
446
|
+
const qaPrompt = this.buildQaPrompt(storyPath, flags['qa-prompt'], flags.reference);
|
|
447
|
+
const qaResult = await this.agentRunner.runAgent(qaPrompt, {
|
|
448
|
+
agentType: 'tea',
|
|
449
|
+
timeout: AGENT_TIMEOUT_MS,
|
|
450
|
+
});
|
|
451
|
+
if (!qaResult.success) {
|
|
452
|
+
throw new Error(`QA agent failed: ${qaResult.errors}`);
|
|
453
|
+
}
|
|
454
|
+
// Read story to get gate status
|
|
455
|
+
let storyContent = await this.fileManager.readFile(storyPath);
|
|
456
|
+
currentGate = this.extractGateStatus(storyContent);
|
|
457
|
+
this.log(colors.info(` Initial Gate: ${currentGate}`));
|
|
458
|
+
// Phase 2: Dev Fix-Forward Loop (if needed and retries allowed)
|
|
459
|
+
while ((currentGate === 'CONCERNS' || currentGate === 'FAIL') && retriesUsed < flags['max-retries']) {
|
|
460
|
+
retriesUsed++;
|
|
461
|
+
this.log(colors.warning(` Phase 2: Dev Fix-Forward (Retry ${retriesUsed}/${flags['max-retries']})...`));
|
|
462
|
+
// Run Dev agent to fix issues (sequential retry loop by design)
|
|
463
|
+
const devPrompt = this.buildDevFixPrompt(storyPath, flags['dev-prompt'], flags.reference);
|
|
464
|
+
// eslint-disable-next-line no-await-in-loop
|
|
465
|
+
const devResult = await this.agentRunner.runAgent(devPrompt, {
|
|
466
|
+
agentType: 'dev',
|
|
467
|
+
timeout: AGENT_TIMEOUT_MS,
|
|
468
|
+
});
|
|
469
|
+
if (!devResult.success) {
|
|
470
|
+
this.logger.warn({ errors: devResult.errors, retriesUsed }, 'Dev fix-forward failed, continuing...');
|
|
471
|
+
// Continue anyway - QA will re-evaluate
|
|
472
|
+
}
|
|
473
|
+
// Phase 3: Re-run QA to validate fixes
|
|
474
|
+
this.log(colors.info(` Phase 3: QA Re-validation (Retry ${retriesUsed})...`));
|
|
475
|
+
// eslint-disable-next-line no-await-in-loop
|
|
476
|
+
const reQaResult = await this.agentRunner.runAgent(qaPrompt, {
|
|
477
|
+
agentType: 'tea',
|
|
478
|
+
timeout: AGENT_TIMEOUT_MS,
|
|
479
|
+
});
|
|
480
|
+
if (!reQaResult.success) {
|
|
481
|
+
this.logger.warn({ errors: reQaResult.errors, retriesUsed }, 'QA re-validation failed, continuing...');
|
|
482
|
+
}
|
|
483
|
+
// Re-read story to get updated gate status
|
|
484
|
+
// eslint-disable-next-line no-await-in-loop
|
|
485
|
+
storyContent = await this.fileManager.readFile(storyPath);
|
|
486
|
+
currentGate = this.extractGateStatus(storyContent);
|
|
487
|
+
this.log(colors.info(` Gate after retry ${retriesUsed}: ${currentGate}`));
|
|
488
|
+
}
|
|
489
|
+
// Append QA workflow summary to story
|
|
490
|
+
const result = {
|
|
491
|
+
finalGate: currentGate,
|
|
492
|
+
movedTo: 'none',
|
|
493
|
+
retriesUsed,
|
|
494
|
+
storyNumber,
|
|
495
|
+
storyPath,
|
|
496
|
+
success: true,
|
|
497
|
+
};
|
|
498
|
+
await this.appendQaSummaryToStory(storyPath, result, flags['max-retries']);
|
|
499
|
+
// Move story based on final gate status
|
|
500
|
+
result.movedTo = await this.moveStoryBasedOnGate(storyPath, currentGate);
|
|
501
|
+
this.logger.info({
|
|
502
|
+
finalGate: currentGate,
|
|
503
|
+
movedTo: result.movedTo,
|
|
504
|
+
retriesUsed,
|
|
505
|
+
storyNumber,
|
|
506
|
+
}, 'Story QA workflow completed');
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
const err = error;
|
|
511
|
+
this.logger.error({ error: err, storyNumber, storyPath }, 'Story QA workflow failed');
|
|
512
|
+
return {
|
|
513
|
+
error: err.message,
|
|
514
|
+
finalGate: currentGate,
|
|
515
|
+
movedTo: 'none',
|
|
516
|
+
retriesUsed,
|
|
517
|
+
storyNumber,
|
|
518
|
+
storyPath,
|
|
519
|
+
success: false,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Sleep for specified milliseconds
|
|
525
|
+
*/
|
|
526
|
+
async sleep(ms) {
|
|
527
|
+
// eslint-disable-next-line no-promise-executor-return -- Simple setTimeout wrapper
|
|
528
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
529
|
+
}
|
|
530
|
+
}
|