@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,180 @@
1
+ /**
2
+ * MCP Status Command
3
+ *
4
+ * Displays a structured dashboard showing gateway health, per-server health
5
+ * with latency, credential status, and current active preset name.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import { DEFAULT_GATEWAY_URL, formatDockerError, isDockerAvailable } from '../../mcp/utils/docker-utils.js';
10
+ import { createLogger } from '../../utils/logger.js';
11
+ const logger = createLogger({ namespace: 'commands:mcp:status' });
12
+ /**
13
+ * Format latency for display
14
+ */
15
+ function formatLatency(ms) {
16
+ if (ms === null)
17
+ return '—';
18
+ if (ms >= 1000)
19
+ return `${(ms / 1000).toFixed(1)}s`;
20
+ return `${ms}ms`;
21
+ }
22
+ /**
23
+ * Format health status with icon
24
+ */
25
+ function formatStatus(status) {
26
+ switch (status) {
27
+ case 'disabled': {
28
+ return chalk.gray('✗ disabled');
29
+ }
30
+ case 'healthy': {
31
+ return chalk.green('✓ healthy');
32
+ }
33
+ case 'offline': {
34
+ return chalk.red('✗ offline');
35
+ }
36
+ case 'unhealthy': {
37
+ return chalk.yellow('! unhealthy');
38
+ }
39
+ default: {
40
+ return chalk.dim(status);
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Render the status dashboard to the terminal
46
+ */
47
+ function renderDashboard(healthReport, presetName, credentials) {
48
+ const lines = [];
49
+ // Header
50
+ lines.push(chalk.bold('MCP Gateway Status'));
51
+ lines.push(chalk.dim('=================='));
52
+ // Gateway line
53
+ const gwStatus = formatStatus(healthReport.gateway.status);
54
+ const gwLatency = formatLatency(healthReport.gateway.latency);
55
+ const gwLine = healthReport.gateway.latency !== null
56
+ ? `${gwStatus} (${gwLatency})`
57
+ : gwStatus;
58
+ lines.push(`Gateway: ${healthReport.gateway.url} ${gwLine}`);
59
+ lines.push(`Preset: ${presetName}`);
60
+ // Servers section
61
+ lines.push('');
62
+ lines.push(chalk.bold('Servers:'));
63
+ if (healthReport.servers.length === 0) {
64
+ lines.push(chalk.dim(' No servers configured'));
65
+ }
66
+ else {
67
+ // Calculate column widths for alignment
68
+ const maxNameLen = Math.max(...healthReport.servers.map((s) => s.name.length));
69
+ for (const server of healthReport.servers) {
70
+ const name = server.name.padEnd(maxNameLen);
71
+ const status = formatStatus(server.status);
72
+ const latency = server.status === 'disabled' ? '—' : formatLatency(server.latency);
73
+ const padLatency = `(${latency})`.padEnd(10);
74
+ const useCase = server.useCase || '';
75
+ lines.push(` ${name} ${status} ${padLatency} ${useCase}`);
76
+ }
77
+ }
78
+ // Credentials section
79
+ if (credentials.length > 0) {
80
+ lines.push('');
81
+ lines.push(chalk.bold('Credentials:'));
82
+ const maxKeyLen = Math.max(...credentials.map((c) => c.key.length));
83
+ for (const cred of credentials) {
84
+ const key = cred.key.padEnd(maxKeyLen);
85
+ if (cred.configured) {
86
+ lines.push(` ${key} ${chalk.green('✓ configured')}`);
87
+ }
88
+ else if (!cred.requiredByPreset) {
89
+ lines.push(` ${key} ${chalk.dim('— not set (not needed for current preset)')}`);
90
+ }
91
+ else {
92
+ lines.push(` ${key} ${chalk.yellow('✗ not set')}`);
93
+ }
94
+ }
95
+ }
96
+ return lines.join('\n');
97
+ }
98
+ export default class McpStatus extends Command {
99
+ static description = 'Display MCP gateway status dashboard with server health and credentials';
100
+ static examples = [
101
+ '<%= config.bin %> mcp status',
102
+ ];
103
+ async run() {
104
+ logger.info('Checking MCP gateway status');
105
+ // 1. Check Docker availability
106
+ const dockerCheck = isDockerAvailable();
107
+ if (!dockerCheck.available) {
108
+ this.error(formatDockerError(dockerCheck), { exit: 1 });
109
+ return;
110
+ }
111
+ // 2. Gather status data
112
+ // NOTE: McpHealthChecker, McpConfigManager, and McpCredentialManager
113
+ // are provided by Epic 1 / Story 2.4. Until those are implemented,
114
+ // we fall back to a direct health check and sensible defaults.
115
+ const healthReport = await this.getHealthReport();
116
+ const presetName = this.getPresetName();
117
+ const credentials = this.getCredentials();
118
+ // 3. Render dashboard
119
+ const dashboard = renderDashboard(healthReport, presetName, credentials);
120
+ this.log(dashboard);
121
+ }
122
+ /**
123
+ * Get credentials status — delegates to McpCredentialManager when available
124
+ */
125
+ getCredentials() {
126
+ try {
127
+ // TODO: Replace with McpCredentialManager.list() when Epic 1 is complete
128
+ return [];
129
+ }
130
+ catch {
131
+ return [];
132
+ }
133
+ }
134
+ /**
135
+ * Gather health report — delegates to McpHealthChecker when available
136
+ */
137
+ async getHealthReport() {
138
+ // TODO: Replace with McpHealthChecker.checkAll() when Story 2.4 is complete
139
+ // For now, do a simple gateway health check
140
+ const gatewayUrl = DEFAULT_GATEWAY_URL;
141
+ const healthUrl = `${gatewayUrl}/health`;
142
+ try {
143
+ const start = Date.now();
144
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
145
+ const latency = Date.now() - start;
146
+ return {
147
+ gateway: {
148
+ latency,
149
+ status: response.ok ? 'healthy' : 'unhealthy',
150
+ url: gatewayUrl,
151
+ },
152
+ servers: [],
153
+ };
154
+ }
155
+ catch {
156
+ return {
157
+ gateway: {
158
+ latency: null,
159
+ status: 'offline',
160
+ url: gatewayUrl,
161
+ },
162
+ servers: [],
163
+ };
164
+ }
165
+ }
166
+ /**
167
+ * Get current preset name — delegates to McpConfigManager when available
168
+ */
169
+ getPresetName() {
170
+ try {
171
+ // TODO: Replace with McpConfigManager.getPresetName() when Epic 1 is complete
172
+ return 'unknown';
173
+ }
174
+ catch {
175
+ return 'unknown';
176
+ }
177
+ }
178
+ }
179
+ // Export the render function for testing
180
+ export { renderDashboard };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * MCP Stop Command
3
+ *
4
+ * Shuts down the Docker MCP gateway via docker compose down
5
+ * and confirms shutdown to the user.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class McpStop extends Command {
9
+ static description: string;
10
+ static examples: string[];
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * MCP Stop Command
3
+ *
4
+ * Shuts down the Docker MCP gateway via docker compose down
5
+ * and confirms shutdown to the user.
6
+ */
7
+ import { execSync } from 'node:child_process';
8
+ import { Command } from '@oclif/core';
9
+ import chalk from 'chalk';
10
+ import { dockerComposeFileExists, formatDockerError, getDockerComposeFilePath, isDockerAvailable, } from '../../mcp/utils/docker-utils.js';
11
+ import { createLogger } from '../../utils/logger.js';
12
+ const logger = createLogger({ namespace: 'commands:mcp:stop' });
13
+ export default class McpStop extends Command {
14
+ static description = 'Stop the Docker MCP gateway';
15
+ static examples = [
16
+ '<%= config.bin %> mcp stop',
17
+ ];
18
+ async run() {
19
+ logger.info('Stopping MCP gateway');
20
+ // 1. Check Docker availability
21
+ const dockerCheck = isDockerAvailable();
22
+ if (!dockerCheck.available) {
23
+ this.error(formatDockerError(dockerCheck), { exit: 1 });
24
+ return;
25
+ }
26
+ // 2. Verify compose file exists
27
+ const composePath = getDockerComposeFilePath();
28
+ if (!dockerComposeFileExists()) {
29
+ this.error(`Docker Compose file not found at: ${composePath}\nNothing to stop — the gateway has not been initialized.`, { exit: 1 });
30
+ return;
31
+ }
32
+ // 3. Shut down gateway
33
+ this.log(chalk.dim('Stopping MCP gateway containers...'));
34
+ try {
35
+ execSync(`docker compose -f "${composePath}" down`, { stdio: 'pipe', timeout: 60_000 });
36
+ this.log('');
37
+ this.log(chalk.green.bold('✓ MCP Gateway stopped successfully'));
38
+ logger.info('Gateway stopped');
39
+ }
40
+ catch (error) {
41
+ const err = error;
42
+ const stderr = err.stderr ? err.stderr.toString() : err.message;
43
+ logger.error('Failed to stop gateway: %s', stderr);
44
+ this.error(`Failed to stop MCP gateway:\n${stderr}`, { exit: 1 });
45
+ }
46
+ }
47
+ }
@@ -39,6 +39,7 @@ export default class StoriesCreateCommand extends Command {
39
39
  provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
40
40
  task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
41
41
  timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
42
+ 'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
42
43
  'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
43
44
  'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
44
45
  };
@@ -41,6 +41,7 @@ export default class StoriesDevelopCommand extends Command {
41
41
  provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
42
42
  task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
43
  timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
44
+ 'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
45
  'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
45
46
  'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
46
47
  };
@@ -21,8 +21,8 @@ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
21
21
  import { PathResolver } from '../../services/file-system/path-resolver.js';
22
22
  import { StoryParserFactory } from '../../services/parsers/story-parser-factory.js';
23
23
  import * as colors from '../../utils/colors.js';
24
+ import { parseDuration } from '../../utils/duration.js';
24
25
  import { createLogger, generateCorrelationId } from '../../utils/logger.js';
25
- import { createSpinner } from '../../utils/progress.js';
26
26
  import { runAgentWithRetry } from '../../utils/retry.js';
27
27
  /**
28
28
  * Stories QA Command
@@ -85,9 +85,11 @@ export default class StoriesQaCommand extends Command {
85
85
  description: 'Additional context files for agents',
86
86
  multiple: true,
87
87
  }),
88
- timeout: Flags.integer({
88
+ timeout: Flags.custom({
89
+ parse: async (input) => parseDuration(input),
90
+ })({
89
91
  default: 2_700_000,
90
- description: 'Agent execution timeout in milliseconds (default: 2700000 = 45 minutes)',
92
+ description: 'Agent execution timeout accepts durations like 30s, 5m, 1h, 90m, or raw milliseconds (default: 45m)',
91
93
  helpGroup: 'Resilience',
92
94
  }),
93
95
  'agent-retries': Flags.integer({
@@ -127,11 +129,10 @@ export default class StoriesQaCommand extends Command {
127
129
  // Match story files using glob pattern
128
130
  const storyFiles = await this.matchStoryFiles(args.pattern);
129
131
  if (storyFiles.length === 0) {
130
- this.log(colors.warning(`No story files matched pattern: ${args.pattern}`));
132
+ this.logger.info({ pattern: args.pattern }, 'No story files matched pattern');
131
133
  return;
132
134
  }
133
- this.log(colors.info(`Found ${storyFiles.length} story file(s) to QA`));
134
- this.log('');
135
+ this.logger.info({ count: storyFiles.length }, 'Found story file(s) to QA');
135
136
  // Process stories sequentially
136
137
  const results = await this.qaStoriesSequentially(storyFiles, flags);
137
138
  // Display summary report
@@ -245,13 +246,8 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
245
246
  * Display countdown timer between stories
246
247
  */
247
248
  async displayCountdown(intervalSeconds) {
248
- /* eslint-disable no-await-in-loop */
249
- for (let remaining = intervalSeconds; remaining > 0; remaining--) {
250
- process.stdout.write(`\r${colors.warning(`⏳ Next story in ${remaining}s...`)}`);
251
- await this.sleep(1000);
252
- }
253
- /* eslint-enable no-await-in-loop */
254
- process.stdout.write('\r' + ' '.repeat(40) + '\r'); // Clear countdown line
249
+ this.logger.info({ seconds: intervalSeconds }, 'Waiting before next story');
250
+ await this.sleep(intervalSeconds * 1000);
255
251
  }
256
252
  /**
257
253
  * Display summary report
@@ -264,45 +260,22 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
264
260
  const errorCount = results.filter((r) => !r.success).length;
265
261
  const movedToDone = results.filter((r) => r.movedTo === 'done').length;
266
262
  const movedBack = results.filter((r) => r.movedTo === 'stories').length;
267
- // Box drawing
268
- const boxTop = '┌─────────────────────────────────────────┐';
269
- const boxDivider = '├─────────────────────────────────────────┤';
270
- const boxBottom = '└─────────────────────────────────────────┘';
271
- this.log('');
272
- this.log(boxTop);
273
- this.log('│ Story QA Summary │');
274
- this.log(boxDivider);
275
- this.log(`│ ${colors.success('PASS:')} ${passCount.toString().padEnd(20)}│`);
276
- if (concernsCount > 0) {
277
- this.log(`│ ${colors.warning('CONCERNS:')} ${concernsCount.toString().padEnd(20)}│`);
278
- }
279
- if (failCount > 0) {
280
- this.log(`│ ${colors.error('FAIL:')} ${failCount.toString().padEnd(20)}│`);
281
- }
282
- if (waivedCount > 0) {
283
- this.log(`│ WAIVED: ${waivedCount.toString().padEnd(20)}│`);
284
- }
285
- if (errorCount > 0) {
286
- this.log(`│ ${colors.error('Errors:')} ${errorCount.toString().padEnd(20)}│`);
287
- }
288
- this.log(boxDivider);
289
- this.log(`│ Moved to Done: ${movedToDone.toString().padEnd(20)}│`);
290
- this.log(`│ Returned to Dev: ${movedBack.toString().padEnd(20)}│`);
291
- this.log(boxDivider);
292
- this.log(`│ Duration: ${(duration / 1000).toFixed(2)}s${' '.repeat(15)}│`);
293
- this.log(boxBottom);
294
- // List details for non-PASS stories
295
263
  const nonPassStories = results.filter((r) => r.finalGate !== 'PASS' || !r.success);
296
- if (nonPassStories.length > 0) {
297
- this.log('');
298
- this.log(colors.bold('Stories Requiring Attention:'));
299
- for (const result of nonPassStories) {
300
- const status = result.success ? result.finalGate : 'ERROR';
301
- const statusColor = status === 'CONCERNS' ? colors.warning : colors.error;
302
- this.log(statusColor(` ${status}: ${result.storyNumber} - ${path.basename(result.storyPath)}${result.error ? ` (${result.error})` : ''}`));
303
- }
304
- }
305
- this.log('');
264
+ this.logger.info({
265
+ concerns: concernsCount,
266
+ durationSec: (duration / 1000).toFixed(2),
267
+ errors: errorCount,
268
+ fail: failCount,
269
+ movedBack,
270
+ movedToDone,
271
+ nonPass: nonPassStories.map((r) => ({
272
+ error: r.error,
273
+ gate: r.success ? r.finalGate : 'ERROR',
274
+ story: r.storyNumber,
275
+ })),
276
+ pass: passCount,
277
+ waived: waivedCount,
278
+ }, 'Story QA Summary');
306
279
  }
307
280
  /**
308
281
  * Extract gate status from story content or gate file
@@ -393,19 +366,16 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
393
366
  for (let index = 0; index < storyFiles.length; index++) {
394
367
  const storyPath = storyFiles[index];
395
368
  const storyNum = index + 1;
396
- this.log(colors.bold(`\n[${storyNum}/${total}] QA: ${path.basename(storyPath)}`));
369
+ this.logger.info({ storyNum, total, story: path.basename(storyPath) }, 'QA starting');
397
370
  // Parse story metadata
398
- const spinner = createSpinner('Parsing story metadata...');
399
- spinner.start();
400
371
  let storyMetadata;
401
372
  try {
402
373
  storyMetadata = await this.storyParserFactory.parseStory(storyPath);
403
- const storyId = storyMetadata.id;
404
- spinner.succeed(colors.success(`Story ${storyId}: ${storyMetadata.title}`));
374
+ this.logger.info({ storyId: storyMetadata.id, title: storyMetadata.title }, 'Parsed story');
405
375
  }
406
376
  catch (error) {
407
377
  const err = error;
408
- spinner.fail(colors.error(`Failed to parse story: ${err.message}`));
378
+ this.logger.error({ error: err.message, storyPath }, 'Failed to parse story');
409
379
  results.push({
410
380
  error: `Parse error: ${err.message}`,
411
381
  finalGate: 'UNKNOWN',
@@ -425,19 +395,8 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
425
395
  reference: flags.reference,
426
396
  });
427
397
  results.push(result);
428
- // Display result
429
- if (result.finalGate === 'PASS') {
430
- this.log(colors.success(` ✓ PASSED - Moved to done`));
431
- }
432
- else if (result.finalGate === 'WAIVED') {
433
- this.log(colors.warning(` ⚠ WAIVED - Moved to done`));
434
- }
435
- else if (result.success) {
436
- this.log(colors.warning(` ⚠ ${result.finalGate} - Returned for rework`));
437
- }
438
- else {
439
- this.log(colors.error(` ✗ Error: ${result.error}`));
440
- }
398
+ // Log result
399
+ this.logger.info({ gate: result.finalGate, movedTo: result.movedTo, story: path.basename(storyPath), success: result.success }, result.success ? `QA ${result.finalGate}` : `QA error: ${result.error}`);
441
400
  // Display countdown timer before next story (except for last story)
442
401
  if (index < storyFiles.length - 1) {
443
402
  await this.displayCountdown(flags.interval);
@@ -456,7 +415,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
456
415
  this.logger.info({ storyNumber, storyPath }, 'Starting story QA workflow');
457
416
  try {
458
417
  // Phase 1: Initial QA Deep Dive
459
- this.log(colors.info(' Phase 1: QA Deep Dive Review...'));
418
+ this.logger.info('Phase 1: QA Deep Dive Review');
460
419
  const agentTimeout = flags.timeout ?? 2_700_000;
461
420
  const agentRetries = flags['agent-retries'];
462
421
  const retryBackoff = flags['retry-backoff'];
@@ -475,11 +434,11 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
475
434
  // Read story to get gate status
476
435
  let storyContent = await this.fileManager.readFile(storyPath);
477
436
  currentGate = this.extractGateStatus(storyContent);
478
- this.log(colors.info(` Initial Gate: ${currentGate}`));
437
+ this.logger.info({ gate: currentGate }, 'Initial gate result');
479
438
  // Phase 2: Dev Fix-Forward Loop (if needed and retries allowed)
480
439
  while ((currentGate === 'CONCERNS' || currentGate === 'FAIL') && retriesUsed < flags['max-retries']) {
481
440
  retriesUsed++;
482
- this.log(colors.warning(` Phase 2: Dev Fix-Forward (Retry ${retriesUsed}/${flags['max-retries']})...`));
441
+ this.logger.info({ retry: retriesUsed, maxRetries: flags['max-retries'] }, 'Phase 2: Dev Fix-Forward');
483
442
  // Run Dev agent to fix issues (sequential retry loop by design)
484
443
  const devPrompt = this.buildDevFixPrompt(storyPath, flags['dev-prompt'], flags.reference);
485
444
  // eslint-disable-next-line no-await-in-loop
@@ -496,7 +455,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
496
455
  // Continue anyway - QA will re-evaluate
497
456
  }
498
457
  // Phase 3: Re-run QA to validate fixes
499
- this.log(colors.info(` Phase 3: QA Re-validation (Retry ${retriesUsed})...`));
458
+ this.logger.info({ retry: retriesUsed }, 'Phase 3: QA Re-validation');
500
459
  // eslint-disable-next-line no-await-in-loop
501
460
  const reQaResult = await runAgentWithRetry(this.agentRunner, qaPrompt, {
502
461
  agentType: 'tea',
@@ -513,7 +472,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
513
472
  // eslint-disable-next-line no-await-in-loop
514
473
  storyContent = await this.fileManager.readFile(storyPath);
515
474
  currentGate = this.extractGateStatus(storyContent);
516
- this.log(colors.info(` Gate after retry ${retriesUsed}: ${currentGate}`));
475
+ this.logger.info({ gate: currentGate, retry: retriesUsed }, 'Gate after retry');
517
476
  }
518
477
  // Append QA workflow summary to story
519
478
  const result = {
@@ -0,0 +1,124 @@
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 { Command } from '@oclif/core';
17
+ import { Severity } from '../../services/review/types.js';
18
+ /**
19
+ * Parse and validate --scanners flag value
20
+ *
21
+ * @param value - Comma-separated scanner names
22
+ * @returns Array of valid scanner names
23
+ * @throws Error if any scanner name is invalid
24
+ */
25
+ export declare function parseScanners(value: string): string[];
26
+ /**
27
+ * Parse and validate --block-on flag value
28
+ *
29
+ * @param value - Comma-separated severity names
30
+ * @returns Array of valid severity strings
31
+ * @throws Error if any severity is invalid
32
+ */
33
+ export declare function parseBlockOn(value: string): Severity;
34
+ /**
35
+ * Validate --max-fix is a positive integer
36
+ *
37
+ * @param value - Value to validate
38
+ * @returns The validated integer
39
+ * @throws Error if value is not a positive integer
40
+ */
41
+ export declare function validateMaxFix(value: number): number;
42
+ /**
43
+ * Stories Review Command
44
+ *
45
+ * Discovers story files via glob pattern and runs automated code review on each.
46
+ * Supports --dry-run (findings only, no fixes) and --json (structured CI output).
47
+ */
48
+ export default class StoriesReviewCommand extends Command {
49
+ static args: {
50
+ pattern: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
51
+ };
52
+ static description: string;
53
+ static examples: {
54
+ command: string;
55
+ description: string;
56
+ }[];
57
+ static flags: {
58
+ 'block-on': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
59
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
60
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
61
+ 'max-fix': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
62
+ scanners: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
63
+ agent: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
64
+ cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
65
+ model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
66
+ provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
67
+ task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
68
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
69
+ 'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
70
+ 'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
71
+ 'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
72
+ };
73
+ private agentRunner;
74
+ private fileManager;
75
+ private globMatcher;
76
+ private logger;
77
+ private pathResolver;
78
+ private storyParserFactory;
79
+ /**
80
+ * Run the command
81
+ */
82
+ run(): Promise<void>;
83
+ /**
84
+ * Validate and parse all CLI flags into a typed config object
85
+ */
86
+ private validateFlags;
87
+ /**
88
+ * Discover story files matching the glob pattern
89
+ */
90
+ private discoverStories;
91
+ /**
92
+ * Review all discovered stories sequentially
93
+ */
94
+ private reviewStories;
95
+ /**
96
+ * Write review results to story files and session directory
97
+ */
98
+ private writeResults;
99
+ /**
100
+ * Output structured JSON to stdout (for --json mode)
101
+ */
102
+ private outputJson;
103
+ /**
104
+ * Display human-readable summary table
105
+ */
106
+ private displaySummary;
107
+ /**
108
+ * Initialize service dependencies
109
+ */
110
+ private initializeServices;
111
+ /**
112
+ * Extract story ID from file path
113
+ * e.g., "docs/stories/PROJ-story-1.001.md" → "PROJ-story-1.001"
114
+ */
115
+ private extractStoryId;
116
+ /**
117
+ * Group issues by severity level
118
+ */
119
+ private groupIssuesBySeverity;
120
+ /**
121
+ * Format duration in human-readable format
122
+ */
123
+ private formatDuration;
124
+ }