@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.
Files changed (110) 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 +34 -75
  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 +89 -0
  36. package/dist/commands/workflow.js +487 -14
  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/index.d.ts +1 -0
  45. package/dist/models/index.js +1 -0
  46. package/dist/models/workflow-callbacks.d.ts +251 -0
  47. package/dist/models/workflow-callbacks.js +10 -0
  48. package/dist/models/workflow-config.d.ts +77 -0
  49. package/dist/models/workflow-result.d.ts +7 -0
  50. package/dist/services/WorkflowReporter.d.ts +165 -0
  51. package/dist/services/WorkflowReporter.js +691 -0
  52. package/dist/services/agents/claude-agent-runner.js +25 -4
  53. package/dist/services/file-system/path-resolver.d.ts +10 -0
  54. package/dist/services/file-system/path-resolver.js +12 -0
  55. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  56. package/dist/services/mcp/mcp-config-manager.js +146 -0
  57. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  58. package/dist/services/mcp/mcp-context-injector.js +168 -0
  59. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  60. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  61. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  62. package/dist/services/mcp/mcp-health-checker.js +162 -0
  63. package/dist/services/mcp/types/health-types.d.ts +31 -0
  64. package/dist/services/mcp/types/health-types.js +7 -0
  65. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  66. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  67. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  68. package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
  69. package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
  70. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  71. package/dist/services/review/ai-review-scanner.js +142 -0
  72. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  73. package/dist/services/review/coderabbit-scanner.js +31 -0
  74. package/dist/services/review/index.d.ts +20 -0
  75. package/dist/services/review/index.js +15 -0
  76. package/dist/services/review/lint-scanner.d.ts +46 -0
  77. package/dist/services/review/lint-scanner.js +172 -0
  78. package/dist/services/review/review-config.d.ts +62 -0
  79. package/dist/services/review/review-config.js +91 -0
  80. package/dist/services/review/review-phase-executor.d.ts +69 -0
  81. package/dist/services/review/review-phase-executor.js +152 -0
  82. package/dist/services/review/review-queue.d.ts +98 -0
  83. package/dist/services/review/review-queue.js +174 -0
  84. package/dist/services/review/review-reporter.d.ts +94 -0
  85. package/dist/services/review/review-reporter.js +386 -0
  86. package/dist/services/review/scanner-factory.d.ts +42 -0
  87. package/dist/services/review/scanner-factory.js +60 -0
  88. package/dist/services/review/self-heal-loop.d.ts +58 -0
  89. package/dist/services/review/self-heal-loop.js +132 -0
  90. package/dist/services/review/severity-classifier.d.ts +17 -0
  91. package/dist/services/review/severity-classifier.js +314 -0
  92. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  93. package/dist/services/review/tech-debt-tracker.js +245 -0
  94. package/dist/services/review/types.d.ts +93 -0
  95. package/dist/services/review/types.js +23 -0
  96. package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
  97. package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
  98. package/dist/services/validation/config-validator.d.ts +84 -0
  99. package/dist/services/validation/config-validator.js +78 -0
  100. package/dist/utils/colors.d.ts +10 -10
  101. package/dist/utils/colors.js +15 -15
  102. package/dist/utils/credential-utils.d.ts +14 -0
  103. package/dist/utils/credential-utils.js +19 -0
  104. package/dist/utils/duration.d.ts +41 -0
  105. package/dist/utils/duration.js +89 -0
  106. package/dist/utils/listr2-helpers.d.ts +216 -0
  107. package/dist/utils/listr2-helpers.js +334 -0
  108. package/dist/utils/shared-flags.d.ts +1 -0
  109. package/dist/utils/shared-flags.js +11 -2
  110. 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
  *