@hyperdrive.bot/bmad-workflow 1.0.17 → 1.0.19
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/dist/commands/config/show.js +8 -2
- package/dist/commands/decompose.js +26 -5
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/mcp/add.d.ts +16 -0
- package/dist/commands/mcp/add.js +77 -0
- package/dist/commands/mcp/credential/get.d.ts +14 -0
- package/dist/commands/mcp/credential/get.js +35 -0
- package/dist/commands/mcp/credential/list.d.ts +17 -0
- package/dist/commands/mcp/credential/list.js +67 -0
- package/dist/commands/mcp/credential/remove.d.ts +18 -0
- package/dist/commands/mcp/credential/remove.js +84 -0
- package/dist/commands/mcp/credential/set.d.ts +16 -0
- package/dist/commands/mcp/credential/set.js +41 -0
- package/dist/commands/mcp/credential/validate.d.ts +12 -0
- package/dist/commands/mcp/credential/validate.js +150 -0
- package/dist/commands/mcp/list.d.ts +17 -0
- package/dist/commands/mcp/list.js +80 -0
- package/dist/commands/mcp/logs.d.ts +15 -0
- package/dist/commands/mcp/logs.js +64 -0
- package/dist/commands/mcp/preset.d.ts +15 -0
- package/dist/commands/mcp/preset.js +84 -0
- package/dist/commands/mcp/remove.d.ts +14 -0
- package/dist/commands/mcp/remove.js +36 -0
- package/dist/commands/mcp/start.d.ts +12 -0
- package/dist/commands/mcp/start.js +80 -0
- package/dist/commands/mcp/status.d.ts +30 -0
- package/dist/commands/mcp/status.js +180 -0
- package/dist/commands/mcp/stop.d.ts +12 -0
- package/dist/commands/mcp/stop.js +47 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +1 -0
- package/dist/commands/stories/qa.js +34 -75
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +89 -0
- package/dist/commands/workflow.js +487 -14
- package/dist/mcp/types.d.ts +99 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/utils/docker-utils.d.ts +56 -0
- package/dist/mcp/utils/docker-utils.js +108 -0
- package/dist/mcp/utils/template-loader.d.ts +21 -0
- package/dist/mcp/utils/template-loader.js +60 -0
- package/dist/models/agent-options.d.ts +10 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/workflow-callbacks.d.ts +251 -0
- package/dist/models/workflow-callbacks.js +10 -0
- package/dist/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +25 -4
- package/dist/services/file-system/path-resolver.d.ts +10 -0
- package/dist/services/file-system/path-resolver.js +12 -0
- package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
- package/dist/services/mcp/mcp-config-manager.js +146 -0
- package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
- package/dist/services/mcp/mcp-context-injector.js +168 -0
- package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
- package/dist/services/mcp/mcp-credential-manager.js +124 -0
- package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
- package/dist/services/mcp/mcp-health-checker.js +162 -0
- package/dist/services/mcp/types/health-types.d.ts +31 -0
- package/dist/services/mcp/types/health-types.js +7 -0
- package/dist/services/orchestration/dependency-graph-executor.js +1 -1
- package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
- package/dist/services/orchestration/task-decomposition-service.js +90 -36
- package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
- package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
- package/dist/services/review/ai-review-scanner.d.ts +66 -0
- package/dist/services/review/ai-review-scanner.js +142 -0
- package/dist/services/review/coderabbit-scanner.d.ts +25 -0
- package/dist/services/review/coderabbit-scanner.js +31 -0
- package/dist/services/review/index.d.ts +20 -0
- package/dist/services/review/index.js +15 -0
- package/dist/services/review/lint-scanner.d.ts +46 -0
- package/dist/services/review/lint-scanner.js +172 -0
- package/dist/services/review/review-config.d.ts +62 -0
- package/dist/services/review/review-config.js +91 -0
- package/dist/services/review/review-phase-executor.d.ts +69 -0
- package/dist/services/review/review-phase-executor.js +152 -0
- package/dist/services/review/review-queue.d.ts +98 -0
- package/dist/services/review/review-queue.js +174 -0
- package/dist/services/review/review-reporter.d.ts +94 -0
- package/dist/services/review/review-reporter.js +386 -0
- package/dist/services/review/scanner-factory.d.ts +42 -0
- package/dist/services/review/scanner-factory.js +60 -0
- package/dist/services/review/self-heal-loop.d.ts +58 -0
- package/dist/services/review/self-heal-loop.js +132 -0
- package/dist/services/review/severity-classifier.d.ts +17 -0
- package/dist/services/review/severity-classifier.js +314 -0
- package/dist/services/review/tech-debt-tracker.d.ts +52 -0
- package/dist/services/review/tech-debt-tracker.js +245 -0
- package/dist/services/review/types.d.ts +93 -0
- package/dist/services/review/types.js +23 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- package/dist/utils/credential-utils.d.ts +14 -0
- package/dist/utils/credential-utils.js +19 -0
- package/dist/utils/duration.d.ts +41 -0
- package/dist/utils/duration.js +89 -0
- package/dist/utils/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +6 -3
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stories Review Command
|
|
3
|
+
*
|
|
4
|
+
* Standalone command that discovers and reviews existing story files outside
|
|
5
|
+
* the full workflow pipeline. Runs automated code review (scanners + self-heal)
|
|
6
|
+
* on matched stories and produces PASS/FAIL verdicts.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```bash
|
|
10
|
+
* bmad-workflow stories review "docs/qa/stories/AUTH-*.md"
|
|
11
|
+
* bmad-workflow stories review "stories/*.md" --scanners ai,lint --dry-run
|
|
12
|
+
* bmad-workflow stories review "stories/*.md" --json
|
|
13
|
+
* bmad-workflow stories review "stories/*.md" --block-on CRITICAL --max-fix 5
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
17
|
+
import { createAgentRunner } from '../../services/agents/agent-runner-factory.js';
|
|
18
|
+
import { FileManager } from '../../services/file-system/file-manager.js';
|
|
19
|
+
import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
|
|
20
|
+
import { PathResolver } from '../../services/file-system/path-resolver.js';
|
|
21
|
+
import { DefaultReviewPhaseExecutor } from '../../services/review/review-phase-executor.js';
|
|
22
|
+
import { createScanners } from '../../services/review/scanner-factory.js';
|
|
23
|
+
import { SelfHealLoop } from '../../services/review/self-heal-loop.js';
|
|
24
|
+
import { classify } from '../../services/review/severity-classifier.js';
|
|
25
|
+
import { ReviewReporter } from '../../services/review/review-reporter.js';
|
|
26
|
+
import { TechDebtTracker } from '../../services/review/tech-debt-tracker.js';
|
|
27
|
+
import { Severity } from '../../services/review/types.js';
|
|
28
|
+
import { WorkflowSessionScaffolder } from '../../services/scaffolding/workflow-session-scaffolder.js';
|
|
29
|
+
import { StoryParserFactory } from '../../services/parsers/story-parser-factory.js';
|
|
30
|
+
import * as colors from '../../utils/colors.js';
|
|
31
|
+
import { createLogger, generateCorrelationId } from '../../utils/logger.js';
|
|
32
|
+
import { agentFlags } from '../../utils/shared-flags.js';
|
|
33
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
34
|
+
/** Valid scanner names */
|
|
35
|
+
const VALID_SCANNERS = new Set(['ai', 'lint', 'coderabbit']);
|
|
36
|
+
/** Valid severity levels for --block-on */
|
|
37
|
+
const VALID_SEVERITIES = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']);
|
|
38
|
+
/** Severity rank for display ordering */
|
|
39
|
+
const SEVERITY_RANK = {
|
|
40
|
+
CRITICAL: 4,
|
|
41
|
+
HIGH: 3,
|
|
42
|
+
MEDIUM: 2,
|
|
43
|
+
LOW: 1,
|
|
44
|
+
};
|
|
45
|
+
// ─── Validation helpers (exported for unit testing) ─────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Parse and validate --scanners flag value
|
|
48
|
+
*
|
|
49
|
+
* @param value - Comma-separated scanner names
|
|
50
|
+
* @returns Array of valid scanner names
|
|
51
|
+
* @throws Error if any scanner name is invalid
|
|
52
|
+
*/
|
|
53
|
+
export function parseScanners(value) {
|
|
54
|
+
const scanners = value.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
55
|
+
for (const scanner of scanners) {
|
|
56
|
+
if (!VALID_SCANNERS.has(scanner)) {
|
|
57
|
+
throw new Error(`Unknown scanner '${scanner}'. Valid: ${Array.from(VALID_SCANNERS).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (scanners.length === 0) {
|
|
61
|
+
throw new Error('At least one scanner must be specified. Valid: ai, lint, coderabbit');
|
|
62
|
+
}
|
|
63
|
+
return scanners;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse and validate --block-on flag value
|
|
67
|
+
*
|
|
68
|
+
* @param value - Comma-separated severity names
|
|
69
|
+
* @returns Array of valid severity strings
|
|
70
|
+
* @throws Error if any severity is invalid
|
|
71
|
+
*/
|
|
72
|
+
export function parseBlockOn(value) {
|
|
73
|
+
const severities = value.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
74
|
+
for (const severity of severities) {
|
|
75
|
+
if (!VALID_SEVERITIES.has(severity)) {
|
|
76
|
+
throw new Error(`Unknown severity '${severity}'. Valid: ${Array.from(VALID_SEVERITIES).join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (severities.length === 0) {
|
|
80
|
+
throw new Error('At least one severity must be specified. Valid: CRITICAL, HIGH, MEDIUM, LOW');
|
|
81
|
+
}
|
|
82
|
+
// Use the lowest specified severity as the threshold (most permissive blocker)
|
|
83
|
+
let lowestRank = Infinity;
|
|
84
|
+
let lowestSeverity = Severity.HIGH;
|
|
85
|
+
for (const sev of severities) {
|
|
86
|
+
const rank = SEVERITY_RANK[sev] ?? 0;
|
|
87
|
+
if (rank < lowestRank) {
|
|
88
|
+
lowestRank = rank;
|
|
89
|
+
lowestSeverity = sev;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return lowestSeverity;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate --max-fix is a positive integer
|
|
96
|
+
*
|
|
97
|
+
* @param value - Value to validate
|
|
98
|
+
* @returns The validated integer
|
|
99
|
+
* @throws Error if value is not a positive integer
|
|
100
|
+
*/
|
|
101
|
+
export function validateMaxFix(value) {
|
|
102
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
103
|
+
throw new Error(`--max-fix must be a non-negative integer, got '${value}'`);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
// ─── Command ────────────────────────────────────────────────────────────────
|
|
108
|
+
/**
|
|
109
|
+
* Stories Review Command
|
|
110
|
+
*
|
|
111
|
+
* Discovers story files via glob pattern and runs automated code review on each.
|
|
112
|
+
* Supports --dry-run (findings only, no fixes) and --json (structured CI output).
|
|
113
|
+
*/
|
|
114
|
+
export default class StoriesReviewCommand extends Command {
|
|
115
|
+
static args = {
|
|
116
|
+
pattern: Args.string({
|
|
117
|
+
description: 'Glob pattern to match story files',
|
|
118
|
+
required: true,
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
static description = 'Run automated code review on stories matching a glob pattern';
|
|
122
|
+
static examples = [
|
|
123
|
+
{
|
|
124
|
+
command: '<%= config.bin %> <%= command.id %> "docs/qa/stories/AUTH-*.md"',
|
|
125
|
+
description: 'Review all AUTH stories',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
command: '<%= config.bin %> <%= command.id %> "stories/*.md" --scanners ai,lint',
|
|
129
|
+
description: 'Review with specific scanners',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
command: '<%= config.bin %> <%= command.id %> "stories/*.md" --dry-run',
|
|
133
|
+
description: 'Report findings without auto-fix',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
command: '<%= config.bin %> <%= command.id %> "stories/*.md" --json',
|
|
137
|
+
description: 'Output structured JSON for CI pipelines',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
command: '<%= config.bin %> <%= command.id %> "stories/*.md" --block-on CRITICAL --max-fix 5',
|
|
141
|
+
description: 'Custom severity threshold and fix iterations',
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
static flags = {
|
|
145
|
+
...agentFlags,
|
|
146
|
+
'block-on': Flags.string({
|
|
147
|
+
default: 'CRITICAL,HIGH',
|
|
148
|
+
description: 'Comma-separated severities that cause FAIL verdict (default: CRITICAL,HIGH)',
|
|
149
|
+
}),
|
|
150
|
+
'dry-run': Flags.boolean({
|
|
151
|
+
default: false,
|
|
152
|
+
description: 'Report findings without running self-heal fix loop',
|
|
153
|
+
}),
|
|
154
|
+
json: Flags.boolean({
|
|
155
|
+
default: false,
|
|
156
|
+
description: 'Output structured JSON to stdout (for CI integration)',
|
|
157
|
+
}),
|
|
158
|
+
'max-fix': Flags.integer({
|
|
159
|
+
default: 3,
|
|
160
|
+
description: 'Maximum self-heal fix iterations per story',
|
|
161
|
+
}),
|
|
162
|
+
scanners: Flags.string({
|
|
163
|
+
default: 'ai,lint',
|
|
164
|
+
description: 'Comma-separated list of scanners to run (valid: ai, lint, coderabbit)',
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
// Service instances
|
|
168
|
+
agentRunner;
|
|
169
|
+
fileManager;
|
|
170
|
+
globMatcher;
|
|
171
|
+
logger;
|
|
172
|
+
pathResolver;
|
|
173
|
+
storyParserFactory;
|
|
174
|
+
/**
|
|
175
|
+
* Run the command
|
|
176
|
+
*/
|
|
177
|
+
async run() {
|
|
178
|
+
const { args, flags } = await this.parse(StoriesReviewCommand);
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
const correlationId = generateCorrelationId();
|
|
181
|
+
// Initialize services
|
|
182
|
+
const provider = (flags.provider || 'claude');
|
|
183
|
+
this.initializeServices(provider);
|
|
184
|
+
this.logger.info({ correlationId, flags, pattern: args.pattern }, 'Starting stories review command');
|
|
185
|
+
try {
|
|
186
|
+
// Step 1: Validate flags
|
|
187
|
+
const config = this.validateFlags(flags);
|
|
188
|
+
// Step 2: Discover story files
|
|
189
|
+
const storyFiles = await this.discoverStories(args.pattern);
|
|
190
|
+
// Step 3: Run review on each story
|
|
191
|
+
const results = await this.reviewStories(storyFiles, config, provider);
|
|
192
|
+
// Step 4: Write results to story files and session directory
|
|
193
|
+
const sessionDir = await this.writeResults(results);
|
|
194
|
+
// Step 5: Output results
|
|
195
|
+
const duration = Date.now() - startTime;
|
|
196
|
+
if (flags.json) {
|
|
197
|
+
this.outputJson(results);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.displaySummary(results, duration, sessionDir);
|
|
201
|
+
}
|
|
202
|
+
this.logger.info({
|
|
203
|
+
correlationId,
|
|
204
|
+
duration,
|
|
205
|
+
failed: results.filter((r) => r.verdict === 'FAIL').length,
|
|
206
|
+
passed: results.filter((r) => r.verdict === 'PASS').length,
|
|
207
|
+
total: results.length,
|
|
208
|
+
}, 'Stories review command completed');
|
|
209
|
+
// Set exit code based on results
|
|
210
|
+
const hasFailures = results.some((r) => r.verdict === 'FAIL');
|
|
211
|
+
if (hasFailures) {
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
const err = error;
|
|
217
|
+
this.logger.error({ correlationId, error: err }, 'Command failed');
|
|
218
|
+
if (flags.json) {
|
|
219
|
+
// In JSON mode, output error as JSON
|
|
220
|
+
this.log(JSON.stringify({ error: err.message, stories: [], summary: { failed: 0, passed: 0, total: 0 } }));
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this.error(colors.error(err.message), { exit: 1 });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ─── Step 1: Flag Validation ────────────────────────────────────────────
|
|
229
|
+
/**
|
|
230
|
+
* Validate and parse all CLI flags into a typed config object
|
|
231
|
+
*/
|
|
232
|
+
validateFlags(flags) {
|
|
233
|
+
const scanners = parseScanners(flags.scanners);
|
|
234
|
+
const blockOn = parseBlockOn(flags['block-on']);
|
|
235
|
+
const maxFix = validateMaxFix(flags['max-fix']);
|
|
236
|
+
this.logger.info({ blockOn, dryRun: flags['dry-run'], maxFix, scanners }, 'Validated review config');
|
|
237
|
+
return {
|
|
238
|
+
blockOn,
|
|
239
|
+
dryRun: flags['dry-run'],
|
|
240
|
+
maxFix,
|
|
241
|
+
reviewTimeout: flags['review-timeout'],
|
|
242
|
+
scanners,
|
|
243
|
+
timeout: flags.timeout,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// ─── Step 2: Story Discovery ────────────────────────────────────────────
|
|
247
|
+
/**
|
|
248
|
+
* Discover story files matching the glob pattern
|
|
249
|
+
*/
|
|
250
|
+
async discoverStories(pattern) {
|
|
251
|
+
this.logger.info({ pattern }, 'Discovering story files');
|
|
252
|
+
const matches = await this.globMatcher.expandPattern(pattern);
|
|
253
|
+
if (matches.length === 0) {
|
|
254
|
+
throw new Error(`No story files matched pattern: ${pattern}`);
|
|
255
|
+
}
|
|
256
|
+
this.logger.info({ count: matches.length, pattern }, 'Story files discovered');
|
|
257
|
+
return matches;
|
|
258
|
+
}
|
|
259
|
+
// ─── Step 3: Review Execution ───────────────────────────────────────────
|
|
260
|
+
/**
|
|
261
|
+
* Review all discovered stories sequentially
|
|
262
|
+
*/
|
|
263
|
+
async reviewStories(storyFiles, config, provider) {
|
|
264
|
+
const results = [];
|
|
265
|
+
// Build scanner config from flags
|
|
266
|
+
const scannerConfig = {
|
|
267
|
+
aiReview: config.scanners.includes('ai'),
|
|
268
|
+
coderabbit: config.scanners.includes('coderabbit'),
|
|
269
|
+
};
|
|
270
|
+
// Create scanners
|
|
271
|
+
const scanners = await createScanners(this.agentRunner, scannerConfig, this.logger, {
|
|
272
|
+
reviewTimeout: config.reviewTimeout,
|
|
273
|
+
timeout: config.timeout,
|
|
274
|
+
});
|
|
275
|
+
// Filter to only requested scanners
|
|
276
|
+
// LintScanner is always created by factory; remove it if not requested
|
|
277
|
+
const filteredScanners = config.scanners.includes('lint')
|
|
278
|
+
? scanners
|
|
279
|
+
: scanners.filter((s) => !(s.constructor.name === 'LintScanner'));
|
|
280
|
+
// Build workflow config for executor
|
|
281
|
+
const workflowConfig = {
|
|
282
|
+
dryRun: config.dryRun,
|
|
283
|
+
epicInterval: 0,
|
|
284
|
+
input: '',
|
|
285
|
+
parallel: 1,
|
|
286
|
+
pipeline: false,
|
|
287
|
+
prefix: '',
|
|
288
|
+
prdInterval: 0,
|
|
289
|
+
references: [],
|
|
290
|
+
review: true,
|
|
291
|
+
reviewBlockOn: config.blockOn,
|
|
292
|
+
reviewMaxFix: config.maxFix,
|
|
293
|
+
reviewScanners: config.scanners,
|
|
294
|
+
skipDev: true,
|
|
295
|
+
skipEpics: true,
|
|
296
|
+
skipStories: true,
|
|
297
|
+
storyInterval: 0,
|
|
298
|
+
verbose: false,
|
|
299
|
+
};
|
|
300
|
+
if (config.dryRun) {
|
|
301
|
+
// Dry-run mode: run scanners once, skip self-heal loop
|
|
302
|
+
this.logger.info('Dry-run mode: scanning without self-heal');
|
|
303
|
+
for (const storyFile of storyFiles) {
|
|
304
|
+
const storyId = this.extractStoryId(storyFile);
|
|
305
|
+
this.logger.info({ storyId, storyFile }, 'Scanning story (dry-run)');
|
|
306
|
+
const context = {
|
|
307
|
+
baseBranch: 'main',
|
|
308
|
+
changedFiles: [],
|
|
309
|
+
projectRoot: process.cwd(),
|
|
310
|
+
referenceFiles: [],
|
|
311
|
+
storyFile,
|
|
312
|
+
storyId,
|
|
313
|
+
};
|
|
314
|
+
// Run scanners without self-heal loop
|
|
315
|
+
const rawOutputs = [];
|
|
316
|
+
for (const scanner of filteredScanners) {
|
|
317
|
+
try {
|
|
318
|
+
const output = await scanner.scan(context);
|
|
319
|
+
rawOutputs.push(output);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
this.logger.warn({ error: error.message, storyId }, 'Scanner failed, continuing');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Classify issues
|
|
326
|
+
const issues = classify(rawOutputs);
|
|
327
|
+
// Determine verdict based on blocking threshold
|
|
328
|
+
const blockingIssues = issues.filter((issue) => (SEVERITY_RANK[issue.severity] ?? 0) >= (SEVERITY_RANK[config.blockOn] ?? 0));
|
|
329
|
+
results.push({
|
|
330
|
+
issues,
|
|
331
|
+
iterations: 0,
|
|
332
|
+
message: blockingIssues.length > 0
|
|
333
|
+
? `${blockingIssues.length} blocking issue(s) (>= ${config.blockOn})`
|
|
334
|
+
: undefined,
|
|
335
|
+
path: storyFile,
|
|
336
|
+
storyId,
|
|
337
|
+
verdict: blockingIssues.length > 0 ? 'FAIL' : 'PASS',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// Full review mode: use ReviewPhaseExecutor with SelfHealLoop
|
|
343
|
+
const techDebtTracker = new TechDebtTracker(this.logger);
|
|
344
|
+
const selfHealLoop = new SelfHealLoop(filteredScanners, classify, this.agentRunner, { fixTimeout: config.timeout, maxIterations: config.maxFix }, this.logger);
|
|
345
|
+
const executor = new DefaultReviewPhaseExecutor(selfHealLoop, techDebtTracker, this.logger);
|
|
346
|
+
const reviewResults = await executor.reviewAll(storyFiles, workflowConfig);
|
|
347
|
+
for (const [storyId, result] of Array.from(reviewResults.entries())) {
|
|
348
|
+
const storyFile = storyFiles.find((f) => this.extractStoryId(f) === storyId) ?? '';
|
|
349
|
+
results.push({
|
|
350
|
+
issues: result.issues,
|
|
351
|
+
iterations: result.iterations,
|
|
352
|
+
message: result.message,
|
|
353
|
+
path: storyFile,
|
|
354
|
+
storyId,
|
|
355
|
+
verdict: result.verdict,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
// ─── Step 4: Result Writing ─────────────────────────────────────────────
|
|
362
|
+
/**
|
|
363
|
+
* Write review results to story files and session directory
|
|
364
|
+
*/
|
|
365
|
+
async writeResults(results) {
|
|
366
|
+
// Create session directory
|
|
367
|
+
const scaffolder = new WorkflowSessionScaffolder(this.fileManager, this.logger);
|
|
368
|
+
const sessionDir = await scaffolder.createSessionStructure({
|
|
369
|
+
baseDir: 'docs/workflow-sessions',
|
|
370
|
+
prefix: 'stories-review',
|
|
371
|
+
});
|
|
372
|
+
// Use ReviewReporter for all report generation
|
|
373
|
+
const reporter = new ReviewReporter(this.fileManager, this.logger);
|
|
374
|
+
// Append per-story reports to story files
|
|
375
|
+
for (const result of results) {
|
|
376
|
+
if (result.path) {
|
|
377
|
+
const reviewResult = {
|
|
378
|
+
issues: result.issues,
|
|
379
|
+
iterations: result.iterations,
|
|
380
|
+
message: result.message,
|
|
381
|
+
verdict: result.verdict,
|
|
382
|
+
};
|
|
383
|
+
await reporter.appendStoryReport(result.path, reviewResult);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Build results map for session reports
|
|
387
|
+
const resultsMap = new Map();
|
|
388
|
+
for (const result of results) {
|
|
389
|
+
resultsMap.set(result.storyId, {
|
|
390
|
+
issues: result.issues,
|
|
391
|
+
iterations: result.iterations,
|
|
392
|
+
message: result.message,
|
|
393
|
+
verdict: result.verdict,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
// Write session-level reports (summary, per-story files, tech debt backlog)
|
|
397
|
+
await reporter.writeSessionReports(sessionDir, resultsMap);
|
|
398
|
+
this.logger.info({ sessionDir }, 'Review results written to session directory');
|
|
399
|
+
return sessionDir;
|
|
400
|
+
}
|
|
401
|
+
// ─── Step 5: Output ─────────────────────────────────────────────────────
|
|
402
|
+
/**
|
|
403
|
+
* Output structured JSON to stdout (for --json mode)
|
|
404
|
+
*/
|
|
405
|
+
outputJson(results) {
|
|
406
|
+
const output = {
|
|
407
|
+
stories: results.map((r) => ({
|
|
408
|
+
issues: r.issues,
|
|
409
|
+
iterations: r.iterations,
|
|
410
|
+
path: r.path,
|
|
411
|
+
verdict: r.verdict,
|
|
412
|
+
})),
|
|
413
|
+
summary: {
|
|
414
|
+
failed: results.filter((r) => r.verdict === 'FAIL').length,
|
|
415
|
+
passed: results.filter((r) => r.verdict === 'PASS').length,
|
|
416
|
+
total: results.length,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
this.log(JSON.stringify(output, null, 2));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Display human-readable summary table
|
|
423
|
+
*/
|
|
424
|
+
displaySummary(results, duration, sessionDir) {
|
|
425
|
+
const passCount = results.filter((r) => r.verdict === 'PASS').length;
|
|
426
|
+
const failCount = results.filter((r) => r.verdict === 'FAIL').length;
|
|
427
|
+
const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
428
|
+
// Aggregate issues by severity
|
|
429
|
+
const allIssues = results.flatMap((r) => r.issues);
|
|
430
|
+
const issuesBySeverity = this.groupIssuesBySeverity(allIssues);
|
|
431
|
+
// Box drawing
|
|
432
|
+
const boxTop = '┌──────────────────────────────────────────────┐';
|
|
433
|
+
const boxDivider = '├──────────────────────────────────────────────┤';
|
|
434
|
+
const boxBottom = '└──────────────────────────────────────────────┘';
|
|
435
|
+
this.log('');
|
|
436
|
+
this.log(boxTop);
|
|
437
|
+
this.log('│ Story Review Summary │');
|
|
438
|
+
this.log(boxDivider);
|
|
439
|
+
this.log(`│ ${colors.success('Passed:')} ${passCount.toString().padEnd(25)}│`);
|
|
440
|
+
this.log(`│ ${colors.error('Failed:')} ${failCount.toString().padEnd(25)}│`);
|
|
441
|
+
this.log(`│ Total Stories: ${results.length.toString().padEnd(25)}│`);
|
|
442
|
+
this.log(boxDivider);
|
|
443
|
+
this.log(`│ Total Issues: ${totalIssues.toString().padEnd(25)}│`);
|
|
444
|
+
for (const [severity, issues] of Object.entries(issuesBySeverity)) {
|
|
445
|
+
if (issues.length > 0) {
|
|
446
|
+
this.log(`│ ${severity}:${' '.repeat(Math.max(1, 17 - severity.length))}${issues.length.toString().padEnd(25)}│`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
this.log(boxDivider);
|
|
450
|
+
this.log(`│ Duration: ${this.formatDuration(duration).padEnd(25)}│`);
|
|
451
|
+
this.log(`│ Session: ${sessionDir.padEnd(25).slice(0, 25)}│`);
|
|
452
|
+
this.log(boxBottom);
|
|
453
|
+
// List failed stories
|
|
454
|
+
const failedStories = results.filter((r) => r.verdict === 'FAIL');
|
|
455
|
+
if (failedStories.length > 0) {
|
|
456
|
+
this.log('');
|
|
457
|
+
this.log(colors.bold('Failed Stories:'));
|
|
458
|
+
for (const story of failedStories) {
|
|
459
|
+
const blockingCount = story.issues.filter((i) => (SEVERITY_RANK[i.severity] ?? 0) >= (SEVERITY_RANK[Severity.HIGH] ?? 0)).length;
|
|
460
|
+
this.log(colors.error(` ${story.storyId}: ${blockingCount} blocking issue(s)`));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
this.log('');
|
|
464
|
+
}
|
|
465
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
466
|
+
/**
|
|
467
|
+
* Initialize service dependencies
|
|
468
|
+
*/
|
|
469
|
+
initializeServices(provider = 'claude') {
|
|
470
|
+
this.logger = createLogger({ namespace: 'commands:stories:review' });
|
|
471
|
+
this.logger.info({ provider }, 'Initializing services');
|
|
472
|
+
this.fileManager = new FileManager(this.logger);
|
|
473
|
+
this.pathResolver = new PathResolver(this.fileManager, this.logger);
|
|
474
|
+
this.globMatcher = new GlobMatcher(this.fileManager, this.logger);
|
|
475
|
+
this.storyParserFactory = new StoryParserFactory(this.fileManager, this.logger);
|
|
476
|
+
this.agentRunner = createAgentRunner(provider, this.logger);
|
|
477
|
+
this.logger.debug({ provider }, 'Services initialized');
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Extract story ID from file path
|
|
481
|
+
* e.g., "docs/stories/PROJ-story-1.001.md" → "PROJ-story-1.001"
|
|
482
|
+
*/
|
|
483
|
+
extractStoryId(filePath) {
|
|
484
|
+
const filename = filePath.split('/').pop() ?? filePath;
|
|
485
|
+
return filename.replace(/\.md$/, '');
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Group issues by severity level
|
|
489
|
+
*/
|
|
490
|
+
groupIssuesBySeverity(issues) {
|
|
491
|
+
const groups = {
|
|
492
|
+
CRITICAL: [],
|
|
493
|
+
HIGH: [],
|
|
494
|
+
MEDIUM: [],
|
|
495
|
+
LOW: [],
|
|
496
|
+
};
|
|
497
|
+
for (const issue of issues) {
|
|
498
|
+
if (groups[issue.severity]) {
|
|
499
|
+
groups[issue.severity].push(issue);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return groups;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Format duration in human-readable format
|
|
506
|
+
*/
|
|
507
|
+
formatDuration(ms) {
|
|
508
|
+
if (ms < 1000)
|
|
509
|
+
return `${ms}ms`;
|
|
510
|
+
if (ms < 60_000)
|
|
511
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
512
|
+
if (ms < 3_600_000)
|
|
513
|
+
return `${(ms / 60_000).toFixed(1)}m`;
|
|
514
|
+
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
@@ -33,7 +33,15 @@ export default class Workflow extends Command {
|
|
|
33
33
|
'prd-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
34
34
|
model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
35
35
|
prefix: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
36
|
+
'session-prefix': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
36
37
|
provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
38
|
+
mcp: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
39
|
+
'mcp-phases': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
40
|
+
'mcp-preset': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
41
|
+
review: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
42
|
+
'review-block-on': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
43
|
+
'review-max-fix': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
44
|
+
'review-scanners': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
37
45
|
qa: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
38
46
|
'qa-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
39
47
|
'qa-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -43,6 +51,7 @@ export default class Workflow extends Command {
|
|
|
43
51
|
'skip-stories': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
44
52
|
'story-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
53
|
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
54
|
+
'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
55
|
'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
47
56
|
'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
48
57
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -50,12 +59,72 @@ export default class Workflow extends Command {
|
|
|
50
59
|
private cancelled;
|
|
51
60
|
private logger;
|
|
52
61
|
private orchestrator;
|
|
62
|
+
private reporter;
|
|
63
|
+
private scaffolder;
|
|
64
|
+
private sessionDir;
|
|
53
65
|
/**
|
|
54
66
|
* Main command execution
|
|
55
67
|
*
|
|
56
68
|
* Orchestrates the complete workflow with progress tracking and summary display.
|
|
57
69
|
*/
|
|
58
70
|
run(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Collect all errors from workflow result into a flat list
|
|
73
|
+
*
|
|
74
|
+
* @param result - Workflow execution result
|
|
75
|
+
* @returns Array of error entries with phase context
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
private collectErrors;
|
|
79
|
+
/**
|
|
80
|
+
* Build dashboard summary from workflow result for final display
|
|
81
|
+
*
|
|
82
|
+
* @param result - Workflow execution result
|
|
83
|
+
* @returns DashboardSummary for the reporter's final dashboard
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
private buildDashboardSummary;
|
|
87
|
+
/**
|
|
88
|
+
* Create callbacks object wired to scaffolder for incremental artifact writing
|
|
89
|
+
*
|
|
90
|
+
* All callbacks are wrapped in try-catch to prevent scaffolder errors from
|
|
91
|
+
* interrupting workflow execution (AC: #9).
|
|
92
|
+
* @returns WorkflowCallbacks object with scaffolder integration
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
private createScaffolderCallbacks;
|
|
96
|
+
/**
|
|
97
|
+
* Merge multiple callback objects into a single callbacks object
|
|
98
|
+
*
|
|
99
|
+
* When the same callback is defined in multiple sources, all handlers are called
|
|
100
|
+
* in sequence (fire-and-forget). This enables dual-channel output where scaffolder
|
|
101
|
+
* and reporter receive events independently.
|
|
102
|
+
*
|
|
103
|
+
* @param sources - Array of WorkflowCallbacks objects to merge
|
|
104
|
+
* @returns Merged WorkflowCallbacks object
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
private mergeCallbacks;
|
|
108
|
+
/**
|
|
109
|
+
* Derive session prefix from input filename
|
|
110
|
+
*
|
|
111
|
+
* Extracts meaningful prefix from input path:
|
|
112
|
+
* - PRD-feature.md → feature
|
|
113
|
+
* - PRD-my-project.md → my-project
|
|
114
|
+
* - epic-001.md → epic-001
|
|
115
|
+
*
|
|
116
|
+
* @param inputPath - Input file path
|
|
117
|
+
* @returns Derived prefix string
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
private derivePrefixFromFilename;
|
|
121
|
+
/**
|
|
122
|
+
* Display detailed failure information for each phase
|
|
123
|
+
*
|
|
124
|
+
* @param result - Workflow result with all phase data
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
private displayFailureDetails;
|
|
59
128
|
/**
|
|
60
129
|
* Display dry-run banner
|
|
61
130
|
*
|
|
@@ -82,15 +151,35 @@ export default class Workflow extends Command {
|
|
|
82
151
|
* @private
|
|
83
152
|
*/
|
|
84
153
|
private displayPhaseHeader;
|
|
154
|
+
/**
|
|
155
|
+
* Finalize session by writing session report and workflow log
|
|
156
|
+
*
|
|
157
|
+
* Called on workflow completion (success or failure).
|
|
158
|
+
* Errors are caught and logged but don't interrupt workflow completion.
|
|
159
|
+
*
|
|
160
|
+
* @param result - Workflow execution result
|
|
161
|
+
* @param _config - Workflow configuration used (unused for now)
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
private finalizeSession;
|
|
85
165
|
/**
|
|
86
166
|
* Initialize services and dependencies
|
|
87
167
|
*
|
|
88
168
|
* Creates all service instances needed for workflow orchestration.
|
|
89
169
|
* @param maxConcurrency - Maximum number of concurrent operations
|
|
90
170
|
* @param provider - AI provider to use (claude or gemini)
|
|
171
|
+
* @param verbose - Enable verbose output mode
|
|
91
172
|
* @private
|
|
92
173
|
*/
|
|
93
174
|
private initializeServices;
|
|
175
|
+
/**
|
|
176
|
+
* Initialize session scaffolder and create session directory structure
|
|
177
|
+
*
|
|
178
|
+
* @param inputPath - Input file path for prefix derivation
|
|
179
|
+
* @param sessionPrefix - Optional explicit session prefix
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
private initializeSession;
|
|
94
183
|
/**
|
|
95
184
|
* Register SIGINT handler for graceful cancellation
|
|
96
185
|
*
|