@hyperdrive.bot/bmad-workflow 1.0.18 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config/show.js +8 -2
- package/dist/commands/decompose.js +26 -5
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/mcp/add.d.ts +16 -0
- package/dist/commands/mcp/add.js +77 -0
- package/dist/commands/mcp/credential/get.d.ts +14 -0
- package/dist/commands/mcp/credential/get.js +35 -0
- package/dist/commands/mcp/credential/list.d.ts +17 -0
- package/dist/commands/mcp/credential/list.js +67 -0
- package/dist/commands/mcp/credential/remove.d.ts +18 -0
- package/dist/commands/mcp/credential/remove.js +84 -0
- package/dist/commands/mcp/credential/set.d.ts +16 -0
- package/dist/commands/mcp/credential/set.js +41 -0
- package/dist/commands/mcp/credential/validate.d.ts +12 -0
- package/dist/commands/mcp/credential/validate.js +150 -0
- package/dist/commands/mcp/list.d.ts +17 -0
- package/dist/commands/mcp/list.js +80 -0
- package/dist/commands/mcp/logs.d.ts +15 -0
- package/dist/commands/mcp/logs.js +64 -0
- package/dist/commands/mcp/preset.d.ts +15 -0
- package/dist/commands/mcp/preset.js +84 -0
- package/dist/commands/mcp/remove.d.ts +14 -0
- package/dist/commands/mcp/remove.js +36 -0
- package/dist/commands/mcp/start.d.ts +12 -0
- package/dist/commands/mcp/start.js +80 -0
- package/dist/commands/mcp/status.d.ts +30 -0
- package/dist/commands/mcp/status.js +180 -0
- package/dist/commands/mcp/stop.d.ts +12 -0
- package/dist/commands/mcp/stop.js +47 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +1 -0
- package/dist/commands/stories/qa.js +5 -2
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +8 -0
- package/dist/commands/workflow.js +110 -2
- package/dist/mcp/types.d.ts +99 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/utils/docker-utils.d.ts +56 -0
- package/dist/mcp/utils/docker-utils.js +108 -0
- package/dist/mcp/utils/template-loader.d.ts +21 -0
- package/dist/mcp/utils/template-loader.js +60 -0
- package/dist/models/agent-options.d.ts +10 -1
- package/dist/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/agents/claude-agent-runner.js +19 -3
- package/dist/services/file-system/path-resolver.d.ts +10 -0
- package/dist/services/file-system/path-resolver.js +12 -0
- package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
- package/dist/services/mcp/mcp-config-manager.js +146 -0
- package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
- package/dist/services/mcp/mcp-context-injector.js +168 -0
- package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
- package/dist/services/mcp/mcp-credential-manager.js +124 -0
- package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
- package/dist/services/mcp/mcp-health-checker.js +162 -0
- package/dist/services/mcp/types/health-types.d.ts +31 -0
- package/dist/services/mcp/types/health-types.js +7 -0
- package/dist/services/orchestration/dependency-graph-executor.js +1 -1
- package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
- package/dist/services/orchestration/task-decomposition-service.js +90 -36
- package/dist/services/orchestration/workflow-orchestrator.d.ts +54 -2
- package/dist/services/orchestration/workflow-orchestrator.js +303 -17
- package/dist/services/review/ai-review-scanner.d.ts +66 -0
- package/dist/services/review/ai-review-scanner.js +142 -0
- package/dist/services/review/coderabbit-scanner.d.ts +25 -0
- package/dist/services/review/coderabbit-scanner.js +31 -0
- package/dist/services/review/index.d.ts +20 -0
- package/dist/services/review/index.js +15 -0
- package/dist/services/review/lint-scanner.d.ts +46 -0
- package/dist/services/review/lint-scanner.js +172 -0
- package/dist/services/review/review-config.d.ts +62 -0
- package/dist/services/review/review-config.js +91 -0
- package/dist/services/review/review-phase-executor.d.ts +69 -0
- package/dist/services/review/review-phase-executor.js +152 -0
- package/dist/services/review/review-queue.d.ts +98 -0
- package/dist/services/review/review-queue.js +174 -0
- package/dist/services/review/review-reporter.d.ts +94 -0
- package/dist/services/review/review-reporter.js +386 -0
- package/dist/services/review/scanner-factory.d.ts +42 -0
- package/dist/services/review/scanner-factory.js +60 -0
- package/dist/services/review/self-heal-loop.d.ts +58 -0
- package/dist/services/review/self-heal-loop.js +132 -0
- package/dist/services/review/severity-classifier.d.ts +17 -0
- package/dist/services/review/severity-classifier.js +314 -0
- package/dist/services/review/tech-debt-tracker.d.ts +52 -0
- package/dist/services/review/tech-debt-tracker.js +245 -0
- package/dist/services/review/types.d.ts +93 -0
- package/dist/services/review/types.js +23 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/credential-utils.d.ts +14 -0
- package/dist/utils/credential-utils.js +19 -0
- package/dist/utils/duration.d.ts +41 -0
- package/dist/utils/duration.js +89 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +4 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
|
+
import { parseDuration } from '../utils/duration.js';
|
|
3
4
|
import { createAgentRunner, isProviderSupported } from '../services/agents/agent-runner-factory.js';
|
|
5
|
+
import { getRegisteredScannerNames } from '../services/review/scanner-factory.js';
|
|
6
|
+
import { Severity } from '../services/review/types.js';
|
|
4
7
|
import { FileManager } from '../services/file-system/file-manager.js';
|
|
5
8
|
import { PathResolver } from '../services/file-system/path-resolver.js';
|
|
6
9
|
import { BatchProcessor } from '../services/orchestration/batch-processor.js';
|
|
@@ -90,6 +93,37 @@ export default class Workflow extends Command {
|
|
|
90
93
|
description: 'AI provider to use (claude, gemini, or opencode)',
|
|
91
94
|
options: ['claude', 'gemini', 'opencode'],
|
|
92
95
|
}),
|
|
96
|
+
mcp: Flags.boolean({
|
|
97
|
+
default: false,
|
|
98
|
+
description: 'Enable MCP tool injection for pipeline phases via Docker gateway',
|
|
99
|
+
helpGroup: 'MCP',
|
|
100
|
+
}),
|
|
101
|
+
'mcp-phases': Flags.string({
|
|
102
|
+
description: 'Comma-separated list of phases to inject MCP tools into (valid: epic,story,dev,review,qa)',
|
|
103
|
+
helpGroup: 'MCP',
|
|
104
|
+
}),
|
|
105
|
+
'mcp-preset': Flags.string({
|
|
106
|
+
description: 'MCP preset name to use for tool configuration (overrides config file)',
|
|
107
|
+
helpGroup: 'MCP',
|
|
108
|
+
}),
|
|
109
|
+
review: Flags.boolean({
|
|
110
|
+
default: false,
|
|
111
|
+
description: 'Enable automated code review phase between dev and QA',
|
|
112
|
+
helpGroup: 'Review Workflow',
|
|
113
|
+
}),
|
|
114
|
+
'review-block-on': Flags.string({
|
|
115
|
+
description: 'Minimum severity to block story from QA (critical|high|medium|low)',
|
|
116
|
+
helpGroup: 'Review Workflow',
|
|
117
|
+
}),
|
|
118
|
+
'review-max-fix': Flags.integer({
|
|
119
|
+
description: 'Maximum self-heal fix iterations (default: from config or 3)',
|
|
120
|
+
helpGroup: 'Review Workflow',
|
|
121
|
+
}),
|
|
122
|
+
'review-scanners': Flags.string({
|
|
123
|
+
description: 'Scanner names to run (e.g., lint, ai, coderabbit). Repeatable.',
|
|
124
|
+
helpGroup: 'Review Workflow',
|
|
125
|
+
multiple: true,
|
|
126
|
+
}),
|
|
93
127
|
qa: Flags.boolean({
|
|
94
128
|
default: false,
|
|
95
129
|
description: 'Run QA workflow after development completes',
|
|
@@ -124,9 +158,17 @@ export default class Workflow extends Command {
|
|
|
124
158
|
default: 60,
|
|
125
159
|
description: 'Seconds between story development',
|
|
126
160
|
}),
|
|
127
|
-
timeout: Flags.
|
|
161
|
+
timeout: Flags.custom({
|
|
162
|
+
parse: async (input) => parseDuration(input),
|
|
163
|
+
})({
|
|
128
164
|
default: 2_700_000,
|
|
129
|
-
description: 'Agent execution timeout
|
|
165
|
+
description: 'Agent execution timeout — accepts durations like 30s, 5m, 1h, 90m, or raw milliseconds (default: 45m)',
|
|
166
|
+
}),
|
|
167
|
+
'review-timeout': Flags.custom({
|
|
168
|
+
parse: async (input) => parseDuration(input),
|
|
169
|
+
})({
|
|
170
|
+
description: 'AI review scanner timeout — overrides --timeout for review phase only (default: 5m)',
|
|
171
|
+
helpGroup: 'Review Workflow',
|
|
130
172
|
}),
|
|
131
173
|
'max-retries': Flags.integer({
|
|
132
174
|
default: 0,
|
|
@@ -178,6 +220,35 @@ export default class Workflow extends Command {
|
|
|
178
220
|
if (flags['dry-run']) {
|
|
179
221
|
this.displayDryRunBanner();
|
|
180
222
|
}
|
|
223
|
+
// Validate review flags when --review is enabled
|
|
224
|
+
if (flags.review) {
|
|
225
|
+
// Validate --review-scanners against registered scanner names
|
|
226
|
+
if (flags['review-scanners'] && flags['review-scanners'].length > 0) {
|
|
227
|
+
const validScanners = getRegisteredScannerNames();
|
|
228
|
+
const invalidScanners = flags['review-scanners'].filter((s) => !validScanners.includes(s));
|
|
229
|
+
if (invalidScanners.length > 0) {
|
|
230
|
+
this.error(`Unknown --review-scanners value(s): ${invalidScanners.join(', ')}. Valid scanners are: ${validScanners.join(', ')}`, { exit: 1 });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Validate --review-max-fix is non-negative
|
|
234
|
+
if (flags['review-max-fix'] !== undefined && flags['review-max-fix'] < 0) {
|
|
235
|
+
this.error(`--review-max-fix must be a non-negative integer, got: ${flags['review-max-fix']}`, { exit: 1 });
|
|
236
|
+
}
|
|
237
|
+
// Validate --review-block-on against severity enum
|
|
238
|
+
if (flags['review-block-on'] !== undefined) {
|
|
239
|
+
const validSeverities = ['critical', 'high', 'medium', 'low'];
|
|
240
|
+
if (!validSeverities.includes(flags['review-block-on'])) {
|
|
241
|
+
this.error(`Invalid --review-block-on value: ${flags['review-block-on']}. Valid values are: ${validSeverities.join(', ')}`, { exit: 1 });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Map --review-block-on string to Severity enum
|
|
246
|
+
const severityMap = {
|
|
247
|
+
critical: Severity.CRITICAL,
|
|
248
|
+
high: Severity.HIGH,
|
|
249
|
+
low: Severity.LOW,
|
|
250
|
+
medium: Severity.MEDIUM,
|
|
251
|
+
};
|
|
181
252
|
// Build workflow configuration
|
|
182
253
|
const config = {
|
|
183
254
|
autoFix: flags['auto-fix'],
|
|
@@ -186,6 +257,9 @@ export default class Workflow extends Command {
|
|
|
186
257
|
epicInterval: flags['epic-interval'],
|
|
187
258
|
input: args.input,
|
|
188
259
|
maxRetries: flags['max-retries'],
|
|
260
|
+
mcp: flags.mcp || undefined,
|
|
261
|
+
mcpPhases: flags['mcp-phases'] ? flags['mcp-phases'].split(',').map((s) => s.trim()) : undefined,
|
|
262
|
+
mcpPreset: flags['mcp-preset'],
|
|
189
263
|
model: flags.model,
|
|
190
264
|
parallel: flags.parallel,
|
|
191
265
|
pipeline: flags.pipeline,
|
|
@@ -197,6 +271,11 @@ export default class Workflow extends Command {
|
|
|
197
271
|
qaRetries: flags['qa-retries'],
|
|
198
272
|
references: flags.reference || [],
|
|
199
273
|
retryBackoffMs: flags['retry-backoff'],
|
|
274
|
+
review: flags.review,
|
|
275
|
+
reviewBlockOn: flags['review-block-on'] ? severityMap[flags['review-block-on']] : undefined,
|
|
276
|
+
reviewMaxFix: flags['review-max-fix'],
|
|
277
|
+
reviewScanners: flags['review-scanners'],
|
|
278
|
+
reviewTimeout: flags['review-timeout'],
|
|
200
279
|
skipDev: flags['skip-dev'],
|
|
201
280
|
skipEpics: flags['skip-epics'],
|
|
202
281
|
skipStories: flags['skip-stories'],
|
|
@@ -204,6 +283,35 @@ export default class Workflow extends Command {
|
|
|
204
283
|
timeout: flags.timeout,
|
|
205
284
|
verbose: flags.verbose,
|
|
206
285
|
};
|
|
286
|
+
// Validate --mcp-phases when --mcp is enabled
|
|
287
|
+
if (config.mcp && config.mcpPhases) {
|
|
288
|
+
const validPhases = ['epic', 'story', 'dev', 'review', 'qa'];
|
|
289
|
+
const invalidPhases = config.mcpPhases.filter((p) => !validPhases.includes(p));
|
|
290
|
+
if (invalidPhases.length > 0) {
|
|
291
|
+
this.error(`Invalid --mcp-phases value(s): ${invalidPhases.join(', ')}. Valid phases are: ${validPhases.join(', ')}`, { exit: 1 });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// MCP gateway health pre-check (AC: #5, #6)
|
|
295
|
+
if (config.mcp) {
|
|
296
|
+
try {
|
|
297
|
+
const gatewayUrl = 'http://localhost:8080';
|
|
298
|
+
const healthUrl = `${gatewayUrl}/health`;
|
|
299
|
+
const response = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
|
|
300
|
+
if (response.ok) {
|
|
301
|
+
if (flags.verbose) {
|
|
302
|
+
this.log(colors.info('MCP gateway is healthy'));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
this.log(colors.warning(`MCP gateway returned status ${response.status} — continuing without MCP`));
|
|
307
|
+
config.mcp = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
this.log(colors.warning('MCP gateway is not reachable — continuing without MCP'));
|
|
312
|
+
config.mcp = false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
207
315
|
// Log configuration if verbose
|
|
208
316
|
if (flags.verbose) {
|
|
209
317
|
this.logger.info({ config }, 'Workflow configuration');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Gateway Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Shared interfaces for MCP gateway services (health checker, config manager, credential manager).
|
|
5
|
+
* These types define the contracts that Story 2.4 and Epic 1 services will implement.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Health status of a single component
|
|
9
|
+
*/
|
|
10
|
+
export type HealthStatus = 'disabled' | 'healthy' | 'offline' | 'unhealthy';
|
|
11
|
+
/**
|
|
12
|
+
* Health status for an individual MCP server
|
|
13
|
+
*/
|
|
14
|
+
export interface ServerHealthStatus {
|
|
15
|
+
error: null | string;
|
|
16
|
+
latency: null | number;
|
|
17
|
+
name: string;
|
|
18
|
+
status: HealthStatus;
|
|
19
|
+
useCase?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gateway-level health status
|
|
23
|
+
*/
|
|
24
|
+
export interface GatewayHealthStatus {
|
|
25
|
+
latency: null | number;
|
|
26
|
+
status: HealthStatus;
|
|
27
|
+
url: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Complete health report for the MCP gateway and all servers
|
|
31
|
+
*/
|
|
32
|
+
export interface HealthReport {
|
|
33
|
+
gateway: GatewayHealthStatus;
|
|
34
|
+
servers: ServerHealthStatus[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Credential entry with masked value
|
|
38
|
+
*/
|
|
39
|
+
export interface CredentialEntry {
|
|
40
|
+
configured: boolean;
|
|
41
|
+
key: string;
|
|
42
|
+
requiredByPreset: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* MCP server configuration entry
|
|
46
|
+
*/
|
|
47
|
+
export interface McpServerConfig {
|
|
48
|
+
apiKeyRequired?: boolean;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
name: string;
|
|
51
|
+
useCase?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Server template as defined in server-templates/*.yaml
|
|
55
|
+
*/
|
|
56
|
+
export interface ServerTemplate {
|
|
57
|
+
api_key_required: boolean;
|
|
58
|
+
args?: string[];
|
|
59
|
+
command?: string;
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
env?: Record<string, string>;
|
|
62
|
+
health_check: {
|
|
63
|
+
command: string;
|
|
64
|
+
method: 'http' | 'tool_call';
|
|
65
|
+
timeout: number;
|
|
66
|
+
};
|
|
67
|
+
layer?: 'direct' | 'gateway';
|
|
68
|
+
name: string;
|
|
69
|
+
transport: 'sse' | 'stdio';
|
|
70
|
+
url?: string;
|
|
71
|
+
use_case: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Interface contract for McpHealthChecker (Story 2.4)
|
|
75
|
+
*/
|
|
76
|
+
export interface IHealthChecker {
|
|
77
|
+
checkAll(): Promise<HealthReport>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Interface contract for McpConfigManager (Epic 1)
|
|
81
|
+
*/
|
|
82
|
+
export interface IConfigManager {
|
|
83
|
+
addServer(template: ServerTemplate): void;
|
|
84
|
+
applyPreset(presetName: string): void;
|
|
85
|
+
getEnabledServers(): McpServerConfig[];
|
|
86
|
+
getGatewayUrl(): string;
|
|
87
|
+
getPresetName(): string;
|
|
88
|
+
removeServer(name: string): boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Interface contract for McpCredentialManager (Epic 1)
|
|
92
|
+
*/
|
|
93
|
+
export interface ICredentialManager {
|
|
94
|
+
get(key: string): string | null;
|
|
95
|
+
list(): CredentialEntry[];
|
|
96
|
+
remove(key: string): boolean;
|
|
97
|
+
resolve(key: string): string | null;
|
|
98
|
+
set(key: string, value: string): void;
|
|
99
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Availability Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to check Docker availability, resolve the gateway
|
|
5
|
+
* compose file path, and format Docker errors into user-friendly messages.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Result of Docker availability check
|
|
9
|
+
*/
|
|
10
|
+
export interface DockerAvailabilityResult {
|
|
11
|
+
available: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
reason?: 'daemon-not-running' | 'not-installed';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check if Docker is available and the daemon is running
|
|
17
|
+
*
|
|
18
|
+
* Executes `docker info` to verify both installation and daemon status.
|
|
19
|
+
*
|
|
20
|
+
* @returns DockerAvailabilityResult with availability status and error details
|
|
21
|
+
*/
|
|
22
|
+
export declare function isDockerAvailable(): DockerAvailabilityResult;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the path to the Docker Compose gateway file
|
|
25
|
+
*
|
|
26
|
+
* The compose file is expected at src/mcp/infrastructure/docker-compose.gateway.yml
|
|
27
|
+
* relative to the CLI package root.
|
|
28
|
+
*
|
|
29
|
+
* @returns Absolute path to docker-compose.gateway.yml
|
|
30
|
+
*/
|
|
31
|
+
export declare function getDockerComposeFilePath(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Check if the Docker Compose gateway file exists
|
|
34
|
+
*
|
|
35
|
+
* @returns true if the compose file exists at the expected path
|
|
36
|
+
*/
|
|
37
|
+
export declare function dockerComposeFileExists(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Format a Docker error into a user-friendly message with troubleshooting hints
|
|
40
|
+
*
|
|
41
|
+
* @param result - Docker availability check result
|
|
42
|
+
* @returns Formatted error message string
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatDockerError(result: DockerAvailabilityResult): string;
|
|
45
|
+
/**
|
|
46
|
+
* Default gateway health endpoint URL
|
|
47
|
+
*/
|
|
48
|
+
export declare const DEFAULT_GATEWAY_URL = "http://localhost:8080";
|
|
49
|
+
/**
|
|
50
|
+
* Default health check poll interval in milliseconds
|
|
51
|
+
*/
|
|
52
|
+
export declare const HEALTH_CHECK_INTERVAL_MS = 2000;
|
|
53
|
+
/**
|
|
54
|
+
* Default health check timeout in milliseconds
|
|
55
|
+
*/
|
|
56
|
+
export declare const HEALTH_CHECK_TIMEOUT_MS = 30000;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Availability Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to check Docker availability, resolve the gateway
|
|
5
|
+
* compose file path, and format Docker errors into user-friendly messages.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { dirname, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { createLogger } from '../../utils/logger.js';
|
|
12
|
+
const logger = createLogger({ namespace: 'mcp:utils:docker' });
|
|
13
|
+
/**
|
|
14
|
+
* Check if Docker is available and the daemon is running
|
|
15
|
+
*
|
|
16
|
+
* Executes `docker info` to verify both installation and daemon status.
|
|
17
|
+
*
|
|
18
|
+
* @returns DockerAvailabilityResult with availability status and error details
|
|
19
|
+
*/
|
|
20
|
+
export function isDockerAvailable() {
|
|
21
|
+
try {
|
|
22
|
+
execSync('docker info', { stdio: 'pipe', timeout: 10_000 });
|
|
23
|
+
logger.debug('Docker is available');
|
|
24
|
+
return { available: true };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const err = error;
|
|
28
|
+
const stderr = err.stderr ? err.stderr.toString() : err.message;
|
|
29
|
+
if (stderr.includes('command not found') || stderr.includes('not recognized') || stderr.includes('ENOENT')) {
|
|
30
|
+
logger.warn('Docker is not installed');
|
|
31
|
+
return {
|
|
32
|
+
available: false,
|
|
33
|
+
error: stderr,
|
|
34
|
+
reason: 'not-installed',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
logger.warn('Docker daemon is not running');
|
|
38
|
+
return {
|
|
39
|
+
available: false,
|
|
40
|
+
error: stderr,
|
|
41
|
+
reason: 'daemon-not-running',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the path to the Docker Compose gateway file
|
|
47
|
+
*
|
|
48
|
+
* The compose file is expected at src/mcp/infrastructure/docker-compose.gateway.yml
|
|
49
|
+
* relative to the CLI package root.
|
|
50
|
+
*
|
|
51
|
+
* @returns Absolute path to docker-compose.gateway.yml
|
|
52
|
+
*/
|
|
53
|
+
export function getDockerComposeFilePath() {
|
|
54
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
// From src/mcp/utils/ → src/mcp/infrastructure/
|
|
56
|
+
const composePath = resolve(currentDir, '..', 'infrastructure', 'docker-compose.gateway.yml');
|
|
57
|
+
logger.debug('Resolved Docker Compose file path: %s', composePath);
|
|
58
|
+
return composePath;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if the Docker Compose gateway file exists
|
|
62
|
+
*
|
|
63
|
+
* @returns true if the compose file exists at the expected path
|
|
64
|
+
*/
|
|
65
|
+
export function dockerComposeFileExists() {
|
|
66
|
+
const composePath = getDockerComposeFilePath();
|
|
67
|
+
return existsSync(composePath);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format a Docker error into a user-friendly message with troubleshooting hints
|
|
71
|
+
*
|
|
72
|
+
* @param result - Docker availability check result
|
|
73
|
+
* @returns Formatted error message string
|
|
74
|
+
*/
|
|
75
|
+
export function formatDockerError(result) {
|
|
76
|
+
if (result.reason === 'not-installed') {
|
|
77
|
+
return [
|
|
78
|
+
'Docker is not installed on this system.',
|
|
79
|
+
'',
|
|
80
|
+
'To install Docker:',
|
|
81
|
+
' macOS: brew install --cask docker',
|
|
82
|
+
' Linux: https://docs.docker.com/engine/install/',
|
|
83
|
+
' Windows: https://docs.docker.com/desktop/install/windows-install/',
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
if (result.reason === 'daemon-not-running') {
|
|
87
|
+
return [
|
|
88
|
+
'Docker daemon is not running.',
|
|
89
|
+
'',
|
|
90
|
+
'To start Docker:',
|
|
91
|
+
' macOS/Windows: Open the Docker Desktop application',
|
|
92
|
+
' Linux: sudo systemctl start docker',
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
95
|
+
return `Docker error: ${result.error || 'Unknown error'}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Default gateway health endpoint URL
|
|
99
|
+
*/
|
|
100
|
+
export const DEFAULT_GATEWAY_URL = 'http://localhost:8080';
|
|
101
|
+
/**
|
|
102
|
+
* Default health check poll interval in milliseconds
|
|
103
|
+
*/
|
|
104
|
+
export const HEALTH_CHECK_INTERVAL_MS = 2000;
|
|
105
|
+
/**
|
|
106
|
+
* Default health check timeout in milliseconds
|
|
107
|
+
*/
|
|
108
|
+
export const HEALTH_CHECK_TIMEOUT_MS = 30_000;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Template Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads and validates MCP server template YAML files from the
|
|
5
|
+
* src/mcp/server-templates/ directory.
|
|
6
|
+
*/
|
|
7
|
+
import type { ServerTemplate } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* List all available server template names by scanning the server-templates directory
|
|
10
|
+
*
|
|
11
|
+
* @returns Array of template names (without .yaml extension)
|
|
12
|
+
*/
|
|
13
|
+
export declare function listAvailableTemplates(): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Load and parse a server template by name
|
|
16
|
+
*
|
|
17
|
+
* @param name - Server template name (e.g., 'context7', 'exa')
|
|
18
|
+
* @returns Parsed ServerTemplate object
|
|
19
|
+
* @throws Error if template not found, with available templates listed
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadServerTemplate(name: string): ServerTemplate;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Template Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads and validates MCP server template YAML files from the
|
|
5
|
+
* src/mcp/server-templates/ directory.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
import { createLogger } from '../../utils/logger.js';
|
|
12
|
+
const logger = createLogger({ namespace: 'mcp:utils:template-loader' });
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the absolute path to the server-templates directory
|
|
15
|
+
*/
|
|
16
|
+
function getTemplatesDir() {
|
|
17
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
return resolve(currentDir, '..', 'server-templates');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* List all available server template names by scanning the server-templates directory
|
|
22
|
+
*
|
|
23
|
+
* @returns Array of template names (without .yaml extension)
|
|
24
|
+
*/
|
|
25
|
+
export function listAvailableTemplates() {
|
|
26
|
+
const templatesDir = getTemplatesDir();
|
|
27
|
+
logger.debug('Scanning templates directory: %s', templatesDir);
|
|
28
|
+
if (!existsSync(templatesDir)) {
|
|
29
|
+
logger.warn('Templates directory not found: %s', templatesDir);
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const files = readdirSync(templatesDir);
|
|
33
|
+
return files
|
|
34
|
+
.filter((f) => f.endsWith('.yaml') && !f.startsWith('__'))
|
|
35
|
+
.map((f) => f.replace('.yaml', ''))
|
|
36
|
+
.sort();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load and parse a server template by name
|
|
40
|
+
*
|
|
41
|
+
* @param name - Server template name (e.g., 'context7', 'exa')
|
|
42
|
+
* @returns Parsed ServerTemplate object
|
|
43
|
+
* @throws Error if template not found, with available templates listed
|
|
44
|
+
*/
|
|
45
|
+
export function loadServerTemplate(name) {
|
|
46
|
+
const templatesDir = getTemplatesDir();
|
|
47
|
+
const templatePath = resolve(templatesDir, `${name}.yaml`);
|
|
48
|
+
logger.debug('Loading template: %s from %s', name, templatePath);
|
|
49
|
+
if (!existsSync(templatePath)) {
|
|
50
|
+
const available = listAvailableTemplates();
|
|
51
|
+
throw new Error(`Server template '${name}' not found. Available templates: ${available.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
const content = readFileSync(templatePath, 'utf8');
|
|
54
|
+
const parsed = yaml.load(content);
|
|
55
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.name) {
|
|
56
|
+
throw new Error(`Invalid server template format in '${name}.yaml': missing required 'name' field`);
|
|
57
|
+
}
|
|
58
|
+
logger.info('Loaded template: %s (transport=%s, api_key_required=%s)', parsed.name, parsed.transport, parsed.api_key_required);
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { AgentResult } from './agent-result.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Available BMAD agent types
|
|
4
4
|
*/
|
|
5
|
-
export type AgentType = 'analyst' | 'architect' | 'dev' | 'pm' | 'prd-fixer' | 'quick-flow-solo-dev' | 'sm' | 'tea' | 'tech-writer' | 'ux-designer';
|
|
5
|
+
export type AgentType = 'analyst' | 'architect' | 'dev' | 'pm' | 'prd-fixer' | 'qa' | 'quick-flow-solo-dev' | 'sm' | 'tea' | 'tech-writer' | 'ux-designer';
|
|
6
6
|
/**
|
|
7
7
|
* Agent options without prompt and callbacks (used internally)
|
|
8
8
|
*/
|
|
@@ -23,6 +23,10 @@ export interface AgentOptions {
|
|
|
23
23
|
* The type of agent to execute
|
|
24
24
|
*/
|
|
25
25
|
agentType: AgentType;
|
|
26
|
+
/**
|
|
27
|
+
* Working directory for the agent process
|
|
28
|
+
*/
|
|
29
|
+
cwd?: string;
|
|
26
30
|
/**
|
|
27
31
|
* Additional CLI flags to pass to the Claude executable
|
|
28
32
|
*/
|
|
@@ -50,6 +54,11 @@ export interface AgentOptions {
|
|
|
50
54
|
* File references to include in the agent execution
|
|
51
55
|
*/
|
|
52
56
|
references?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* System prompt to append via --append-system-prompt (Claude only).
|
|
59
|
+
* Used to enforce output format constraints (e.g., YAML-only output).
|
|
60
|
+
*/
|
|
61
|
+
systemPrompt?: string;
|
|
53
62
|
/**
|
|
54
63
|
* Timeout in milliseconds (default: 1800000ms = 30 minutes)
|
|
55
64
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Severity } from '../services/review/types.js';
|
|
1
2
|
/**
|
|
2
3
|
* Configuration for workflow execution across all phases
|
|
3
4
|
*
|
|
@@ -77,6 +78,31 @@ export interface WorkflowConfig {
|
|
|
77
78
|
* - OpenCode: anthropic/claude-3-5-sonnet, openai/gpt-4o
|
|
78
79
|
*/
|
|
79
80
|
model?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Enable MCP tool discovery in agent prompts
|
|
83
|
+
*
|
|
84
|
+
* When true, MCP context (available tools/instructions) is prepended to
|
|
85
|
+
* agent prompts before each spawn. Requires McpContextInjector to be
|
|
86
|
+
* configured in the orchestrator.
|
|
87
|
+
* @default false
|
|
88
|
+
*/
|
|
89
|
+
mcp?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Restrict MCP injection to specific workflow phases
|
|
92
|
+
*
|
|
93
|
+
* When set, only the listed phases receive MCP tool context.
|
|
94
|
+
* When omitted (and mcp=true), all phases receive MCP context.
|
|
95
|
+
* @example ['epic', 'dev'] — only epic and dev phases get MCP tools
|
|
96
|
+
*/
|
|
97
|
+
mcpPhases?: string[];
|
|
98
|
+
/**
|
|
99
|
+
* MCP preset name for tool configuration
|
|
100
|
+
*
|
|
101
|
+
* Overrides the configured preset for the pipeline run.
|
|
102
|
+
* Presets define which MCP servers and tools are available.
|
|
103
|
+
* @example 'research'
|
|
104
|
+
*/
|
|
105
|
+
mcpPreset?: string;
|
|
80
106
|
/**
|
|
81
107
|
* Filename prefix for generated files
|
|
82
108
|
*
|
|
@@ -90,6 +116,37 @@ export interface WorkflowConfig {
|
|
|
90
116
|
* @default 'claude'
|
|
91
117
|
*/
|
|
92
118
|
provider?: 'claude' | 'gemini' | 'opencode';
|
|
119
|
+
/**
|
|
120
|
+
* Run automated code review between dev and QA phases
|
|
121
|
+
*
|
|
122
|
+
* When true, completed stories are reviewed before forwarding to QA.
|
|
123
|
+
* In pipeline mode, review runs concurrently via ReviewQueue.
|
|
124
|
+
* Only PASS stories proceed to QA; FAIL stories are excluded with a warning.
|
|
125
|
+
* @default false
|
|
126
|
+
*/
|
|
127
|
+
review?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Minimum severity that blocks the pipeline during review
|
|
130
|
+
*
|
|
131
|
+
* Issues at or above this severity after self-heal will cause a FAIL verdict.
|
|
132
|
+
* @default Severity.HIGH
|
|
133
|
+
*/
|
|
134
|
+
reviewBlockOn?: Severity;
|
|
135
|
+
/**
|
|
136
|
+
* Maximum self-heal iterations during review
|
|
137
|
+
*
|
|
138
|
+
* Controls how many scan→fix cycles the review phase attempts before
|
|
139
|
+
* giving up on blocking issues.
|
|
140
|
+
* @default 3
|
|
141
|
+
*/
|
|
142
|
+
reviewMaxFix?: number;
|
|
143
|
+
/**
|
|
144
|
+
* List of scanner IDs to enable during review
|
|
145
|
+
*
|
|
146
|
+
* When specified, only these scanners are used (e.g., ['lint', 'ai']).
|
|
147
|
+
* When omitted, all configured scanners run.
|
|
148
|
+
*/
|
|
149
|
+
reviewScanners?: string[];
|
|
93
150
|
/**
|
|
94
151
|
* Run QA workflow after development completes
|
|
95
152
|
*
|
|
@@ -119,6 +176,13 @@ export interface WorkflowConfig {
|
|
|
119
176
|
* @example ['docs/architecture.md', 'docs/tech-stack.md']
|
|
120
177
|
*/
|
|
121
178
|
references: string[];
|
|
179
|
+
/**
|
|
180
|
+
* Absolute path to the workflow session directory
|
|
181
|
+
*
|
|
182
|
+
* When set, session-level reports (review summary, tech debt backlog)
|
|
183
|
+
* are written to this directory. When undefined, session reports are skipped.
|
|
184
|
+
*/
|
|
185
|
+
sessionDir?: string;
|
|
122
186
|
/**
|
|
123
187
|
* Skip development phase
|
|
124
188
|
*
|
|
@@ -147,8 +211,21 @@ export interface WorkflowConfig {
|
|
|
147
211
|
* @default 60
|
|
148
212
|
*/
|
|
149
213
|
storyInterval: number;
|
|
214
|
+
/**
|
|
215
|
+
* AI review scanner timeout in milliseconds
|
|
216
|
+
*
|
|
217
|
+
* Overrides the global timeout for the AI review phase only.
|
|
218
|
+
* When not set, falls back to the global timeout or the default (5 minutes).
|
|
219
|
+
* Accepts human-readable durations via CLI (e.g., "10m", "1h").
|
|
220
|
+
* @default 300_000 (5 minutes)
|
|
221
|
+
*/
|
|
222
|
+
reviewTimeout?: number;
|
|
150
223
|
/**
|
|
151
224
|
* Agent execution timeout in milliseconds
|
|
225
|
+
*
|
|
226
|
+
* Global timeout ceiling for all agent spawns, review scanners, and
|
|
227
|
+
* infrastructure operations. Accepts human-readable durations via CLI
|
|
228
|
+
* (e.g., "45m", "1h", "90m").
|
|
152
229
|
* @default 2_700_000 (45 minutes)
|
|
153
230
|
*/
|
|
154
231
|
timeout?: number;
|
|
@@ -141,6 +141,13 @@ export interface WorkflowResult {
|
|
|
141
141
|
* Undefined if QA phase was skipped or not executed.
|
|
142
142
|
*/
|
|
143
143
|
qaPhase?: PhaseResult;
|
|
144
|
+
/**
|
|
145
|
+
* Review phase result
|
|
146
|
+
*
|
|
147
|
+
* Undefined if review phase was skipped or not executed.
|
|
148
|
+
* Present when config.review === true.
|
|
149
|
+
*/
|
|
150
|
+
reviewPhase?: PhaseResult;
|
|
144
151
|
/**
|
|
145
152
|
* Story creation phase result
|
|
146
153
|
*
|