@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.
Files changed (98) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +5 -2
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +8 -0
  36. package/dist/commands/workflow.js +110 -2
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/workflow-config.d.ts +77 -0
  45. package/dist/models/workflow-result.d.ts +7 -0
  46. package/dist/services/agents/claude-agent-runner.js +19 -3
  47. package/dist/services/file-system/path-resolver.d.ts +10 -0
  48. package/dist/services/file-system/path-resolver.js +12 -0
  49. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  50. package/dist/services/mcp/mcp-config-manager.js +146 -0
  51. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  52. package/dist/services/mcp/mcp-context-injector.js +168 -0
  53. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  54. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  55. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  56. package/dist/services/mcp/mcp-health-checker.js +162 -0
  57. package/dist/services/mcp/types/health-types.d.ts +31 -0
  58. package/dist/services/mcp/types/health-types.js +7 -0
  59. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  60. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  61. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  62. package/dist/services/orchestration/workflow-orchestrator.d.ts +54 -2
  63. package/dist/services/orchestration/workflow-orchestrator.js +303 -17
  64. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  65. package/dist/services/review/ai-review-scanner.js +142 -0
  66. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  67. package/dist/services/review/coderabbit-scanner.js +31 -0
  68. package/dist/services/review/index.d.ts +20 -0
  69. package/dist/services/review/index.js +15 -0
  70. package/dist/services/review/lint-scanner.d.ts +46 -0
  71. package/dist/services/review/lint-scanner.js +172 -0
  72. package/dist/services/review/review-config.d.ts +62 -0
  73. package/dist/services/review/review-config.js +91 -0
  74. package/dist/services/review/review-phase-executor.d.ts +69 -0
  75. package/dist/services/review/review-phase-executor.js +152 -0
  76. package/dist/services/review/review-queue.d.ts +98 -0
  77. package/dist/services/review/review-queue.js +174 -0
  78. package/dist/services/review/review-reporter.d.ts +94 -0
  79. package/dist/services/review/review-reporter.js +386 -0
  80. package/dist/services/review/scanner-factory.d.ts +42 -0
  81. package/dist/services/review/scanner-factory.js +60 -0
  82. package/dist/services/review/self-heal-loop.d.ts +58 -0
  83. package/dist/services/review/self-heal-loop.js +132 -0
  84. package/dist/services/review/severity-classifier.d.ts +17 -0
  85. package/dist/services/review/severity-classifier.js +314 -0
  86. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  87. package/dist/services/review/tech-debt-tracker.js +245 -0
  88. package/dist/services/review/types.d.ts +93 -0
  89. package/dist/services/review/types.js +23 -0
  90. package/dist/services/validation/config-validator.d.ts +84 -0
  91. package/dist/services/validation/config-validator.js +78 -0
  92. package/dist/utils/credential-utils.d.ts +14 -0
  93. package/dist/utils/credential-utils.js +19 -0
  94. package/dist/utils/duration.d.ts +41 -0
  95. package/dist/utils/duration.js +89 -0
  96. package/dist/utils/shared-flags.d.ts +1 -0
  97. package/dist/utils/shared-flags.js +11 -2
  98. 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>;