@hyperdrive.bot/bmad-workflow 1.0.18 → 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 +5 -2
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +8 -0
- package/dist/commands/workflow.js +110 -2
- 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/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/agents/claude-agent-runner.js +19 -3
- 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 +54 -2
- package/dist/services/orchestration/workflow-orchestrator.js +303 -17
- 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/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- 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/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +4 -2
|
@@ -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
|
+
}
|
|
@@ -35,6 +35,13 @@ export default class Workflow extends Command {
|
|
|
35
35
|
prefix: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
36
36
|
'session-prefix': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
37
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>;
|
|
38
45
|
qa: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
39
46
|
'qa-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
40
47
|
'qa-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -44,6 +51,7 @@ export default class Workflow extends Command {
|
|
|
44
51
|
'skip-stories': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
45
52
|
'story-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
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>;
|
|
47
55
|
'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
48
56
|
'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
49
57
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|