@hyperdrive.bot/bmad-workflow 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config/show.js +8 -2
- package/dist/commands/decompose.js +26 -5
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/mcp/add.d.ts +16 -0
- package/dist/commands/mcp/add.js +77 -0
- package/dist/commands/mcp/credential/get.d.ts +14 -0
- package/dist/commands/mcp/credential/get.js +35 -0
- package/dist/commands/mcp/credential/list.d.ts +17 -0
- package/dist/commands/mcp/credential/list.js +67 -0
- package/dist/commands/mcp/credential/remove.d.ts +18 -0
- package/dist/commands/mcp/credential/remove.js +84 -0
- package/dist/commands/mcp/credential/set.d.ts +16 -0
- package/dist/commands/mcp/credential/set.js +41 -0
- package/dist/commands/mcp/credential/validate.d.ts +12 -0
- package/dist/commands/mcp/credential/validate.js +150 -0
- package/dist/commands/mcp/list.d.ts +17 -0
- package/dist/commands/mcp/list.js +80 -0
- package/dist/commands/mcp/logs.d.ts +15 -0
- package/dist/commands/mcp/logs.js +64 -0
- package/dist/commands/mcp/preset.d.ts +15 -0
- package/dist/commands/mcp/preset.js +84 -0
- package/dist/commands/mcp/remove.d.ts +14 -0
- package/dist/commands/mcp/remove.js +36 -0
- package/dist/commands/mcp/start.d.ts +12 -0
- package/dist/commands/mcp/start.js +80 -0
- package/dist/commands/mcp/status.d.ts +30 -0
- package/dist/commands/mcp/status.js +180 -0
- package/dist/commands/mcp/stop.d.ts +12 -0
- package/dist/commands/mcp/stop.js +47 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +1 -0
- package/dist/commands/stories/qa.js +34 -75
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +89 -0
- package/dist/commands/workflow.js +487 -14
- package/dist/mcp/types.d.ts +99 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/utils/docker-utils.d.ts +56 -0
- package/dist/mcp/utils/docker-utils.js +108 -0
- package/dist/mcp/utils/template-loader.d.ts +21 -0
- package/dist/mcp/utils/template-loader.js +60 -0
- package/dist/models/agent-options.d.ts +10 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/workflow-callbacks.d.ts +251 -0
- package/dist/models/workflow-callbacks.js +10 -0
- package/dist/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +25 -4
- package/dist/services/file-system/path-resolver.d.ts +10 -0
- package/dist/services/file-system/path-resolver.js +12 -0
- package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
- package/dist/services/mcp/mcp-config-manager.js +146 -0
- package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
- package/dist/services/mcp/mcp-context-injector.js +168 -0
- package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
- package/dist/services/mcp/mcp-credential-manager.js +124 -0
- package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
- package/dist/services/mcp/mcp-health-checker.js +162 -0
- package/dist/services/mcp/types/health-types.d.ts +31 -0
- package/dist/services/mcp/types/health-types.js +7 -0
- package/dist/services/orchestration/dependency-graph-executor.js +1 -1
- package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
- package/dist/services/orchestration/task-decomposition-service.js +90 -36
- package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
- package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
- package/dist/services/review/ai-review-scanner.d.ts +66 -0
- package/dist/services/review/ai-review-scanner.js +142 -0
- package/dist/services/review/coderabbit-scanner.d.ts +25 -0
- package/dist/services/review/coderabbit-scanner.js +31 -0
- package/dist/services/review/index.d.ts +20 -0
- package/dist/services/review/index.js +15 -0
- package/dist/services/review/lint-scanner.d.ts +46 -0
- package/dist/services/review/lint-scanner.js +172 -0
- package/dist/services/review/review-config.d.ts +62 -0
- package/dist/services/review/review-config.js +91 -0
- package/dist/services/review/review-phase-executor.d.ts +69 -0
- package/dist/services/review/review-phase-executor.js +152 -0
- package/dist/services/review/review-queue.d.ts +98 -0
- package/dist/services/review/review-queue.js +174 -0
- package/dist/services/review/review-reporter.d.ts +94 -0
- package/dist/services/review/review-reporter.js +386 -0
- package/dist/services/review/scanner-factory.d.ts +42 -0
- package/dist/services/review/scanner-factory.js +60 -0
- package/dist/services/review/self-heal-loop.d.ts +58 -0
- package/dist/services/review/self-heal-loop.js +132 -0
- package/dist/services/review/severity-classifier.d.ts +17 -0
- package/dist/services/review/severity-classifier.js +314 -0
- package/dist/services/review/tech-debt-tracker.d.ts +52 -0
- package/dist/services/review/tech-debt-tracker.js +245 -0
- package/dist/services/review/types.d.ts +93 -0
- package/dist/services/review/types.js +23 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- package/dist/utils/credential-utils.d.ts +14 -0
- package/dist/utils/credential-utils.js +19 -0
- package/dist/utils/duration.d.ts +41 -0
- package/dist/utils/duration.js +89 -0
- package/dist/utils/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +6 -3
|
@@ -0,0 +1,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.
|
|
88
|
+
timeout: Flags.custom({
|
|
89
|
+
parse: async (input) => parseDuration(input),
|
|
90
|
+
})({
|
|
89
91
|
default: 2_700_000,
|
|
90
|
-
description: 'Agent execution timeout
|
|
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.
|
|
132
|
+
this.logger.info({ pattern: args.pattern }, 'No story files matched pattern');
|
|
131
133
|
return;
|
|
132
134
|
}
|
|
133
|
-
this.
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
429
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|