@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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Credential Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages API key credentials for MCP servers.
|
|
5
|
+
* Resolution chain: environment variable > stored credential file.
|
|
6
|
+
* Stores credentials at ~/.bmad/credentials/.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import yaml from 'js-yaml';
|
|
12
|
+
import { createLogger } from '../../utils/logger.js';
|
|
13
|
+
const logger = createLogger({ namespace: 'services:mcp:credential-manager' });
|
|
14
|
+
const CREDENTIALS_DIR = join(homedir(), '.bmad', 'credentials');
|
|
15
|
+
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'mcp-keys.yaml');
|
|
16
|
+
export class McpCredentialManager {
|
|
17
|
+
credentialsPath;
|
|
18
|
+
store;
|
|
19
|
+
constructor(credentialsPath) {
|
|
20
|
+
this.credentialsPath = credentialsPath || CREDENTIALS_FILE;
|
|
21
|
+
this.store = this.loadStore();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get a credential value from the stored file (not env vars).
|
|
25
|
+
*
|
|
26
|
+
* @returns The stored credential value, or null if not found
|
|
27
|
+
*/
|
|
28
|
+
get(key) {
|
|
29
|
+
const value = this.store.keys[key];
|
|
30
|
+
if (value) {
|
|
31
|
+
logger.debug('Credential %s found in store', key);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
logger.debug('Credential %s not found in store', key);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* List all known credential entries with their configuration status
|
|
39
|
+
*/
|
|
40
|
+
list() {
|
|
41
|
+
const allKeys = Object.keys(this.store.keys);
|
|
42
|
+
const knownKeys = new Set(['EXA_API_KEY', 'APIFY_TOKEN', ...allKeys]);
|
|
43
|
+
return [...knownKeys].map((key) => ({
|
|
44
|
+
configured: this.resolve(key) !== null,
|
|
45
|
+
key,
|
|
46
|
+
requiredByPreset: false,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a credential key value.
|
|
51
|
+
* Resolution chain: environment variable > stored credential file.
|
|
52
|
+
*
|
|
53
|
+
* @returns The credential value, or null if not found anywhere
|
|
54
|
+
*/
|
|
55
|
+
resolve(key) {
|
|
56
|
+
// 1. Check environment variable
|
|
57
|
+
const envValue = process.env[key];
|
|
58
|
+
if (envValue) {
|
|
59
|
+
logger.debug('Credential %s resolved from environment', key);
|
|
60
|
+
return envValue;
|
|
61
|
+
}
|
|
62
|
+
// 2. Check stored credentials
|
|
63
|
+
const storedValue = this.store.keys[key];
|
|
64
|
+
if (storedValue) {
|
|
65
|
+
logger.debug('Credential %s resolved from stored credentials', key);
|
|
66
|
+
return storedValue;
|
|
67
|
+
}
|
|
68
|
+
logger.debug('Credential %s not found', key);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Remove a credential from the store.
|
|
73
|
+
*
|
|
74
|
+
* @returns true if the key was found and removed, false if not found
|
|
75
|
+
*/
|
|
76
|
+
remove(key) {
|
|
77
|
+
if (this.store.keys[key] !== undefined) {
|
|
78
|
+
logger.info('Removing credential: %s', key);
|
|
79
|
+
delete this.store.keys[key];
|
|
80
|
+
this.saveStore();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
logger.debug('Credential %s not found for removal', key);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Store a credential key-value pair
|
|
88
|
+
*/
|
|
89
|
+
set(key, value) {
|
|
90
|
+
logger.info('Storing credential: %s', key);
|
|
91
|
+
this.store.keys[key] = value;
|
|
92
|
+
this.saveStore();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Load the credential store from disk
|
|
96
|
+
*/
|
|
97
|
+
loadStore() {
|
|
98
|
+
try {
|
|
99
|
+
if (existsSync(this.credentialsPath)) {
|
|
100
|
+
const content = readFileSync(this.credentialsPath, 'utf8');
|
|
101
|
+
const parsed = yaml.load(content);
|
|
102
|
+
if (parsed && typeof parsed === 'object' && parsed.keys) {
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.warn('Failed to load credentials from %s: %s', this.credentialsPath, error.message);
|
|
109
|
+
}
|
|
110
|
+
return { keys: {} };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Save the credential store to disk
|
|
114
|
+
*/
|
|
115
|
+
saveStore() {
|
|
116
|
+
const dir = dirname(this.credentialsPath);
|
|
117
|
+
if (!existsSync(dir)) {
|
|
118
|
+
mkdirSync(dir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
const content = yaml.dump(this.store, { sortKeys: true });
|
|
121
|
+
writeFileSync(this.credentialsPath, content, { encoding: 'utf8', mode: 0o600 });
|
|
122
|
+
logger.info('Credentials saved to %s', this.credentialsPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Health Checker Service
|
|
3
|
+
*
|
|
4
|
+
* Verifies gateway availability and individual MCP server health using
|
|
5
|
+
* template-defined check methods (tool_call or http) with configurable timeouts.
|
|
6
|
+
*
|
|
7
|
+
* Consumed by: mcp status command (Story 2.1), mcp credential validate (Story 2.3),
|
|
8
|
+
* and McpContextInjector (Epic 3, Story 3.1).
|
|
9
|
+
*/
|
|
10
|
+
import type { GatewayHealthStatus, HealthReport, IHealthChecker } from '../../mcp/types.js';
|
|
11
|
+
import type { McpServerWithHealthCheck, ServerHealthStatus } from './types/health-types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Interface for the config manager dependency.
|
|
14
|
+
* Uses McpServerWithHealthCheck (extended type with healthCheck field)
|
|
15
|
+
* instead of the base McpServerConfig.
|
|
16
|
+
*/
|
|
17
|
+
export interface IHealthCheckerConfigManager {
|
|
18
|
+
getEnabledServers(): McpServerWithHealthCheck[];
|
|
19
|
+
getGatewayUrl(): string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* McpHealthChecker verifies gateway and individual MCP server health.
|
|
23
|
+
*
|
|
24
|
+
* - Gateway is checked first; if offline, all servers are reported as offline
|
|
25
|
+
* - Per-server checks run in parallel via Promise.allSettled
|
|
26
|
+
* - Supports tool_call and http check strategies per server template config
|
|
27
|
+
*/
|
|
28
|
+
export declare class McpHealthChecker implements IHealthChecker {
|
|
29
|
+
private readonly configManager;
|
|
30
|
+
private readonly gatewayUrl;
|
|
31
|
+
constructor(configManager: IHealthCheckerConfigManager);
|
|
32
|
+
/**
|
|
33
|
+
* Check gateway health by hitting its /health endpoint
|
|
34
|
+
*
|
|
35
|
+
* @returns Gateway health status with latency measurement
|
|
36
|
+
*/
|
|
37
|
+
checkGateway(): Promise<GatewayHealthStatus>;
|
|
38
|
+
/**
|
|
39
|
+
* Check an individual MCP server's health using its template-configured method
|
|
40
|
+
*
|
|
41
|
+
* @param server - Server configuration with health check settings
|
|
42
|
+
* @returns Server health result with status, latency, and optional error
|
|
43
|
+
*/
|
|
44
|
+
checkServer(server: McpServerWithHealthCheck): Promise<ServerHealthStatus>;
|
|
45
|
+
/**
|
|
46
|
+
* Check health of the gateway and all configured servers.
|
|
47
|
+
*
|
|
48
|
+
* If the gateway is offline, all servers are immediately reported as offline
|
|
49
|
+
* without making individual server health checks.
|
|
50
|
+
*
|
|
51
|
+
* Enabled servers are checked in parallel via Promise.allSettled.
|
|
52
|
+
*
|
|
53
|
+
* @returns Complete health report for gateway and all servers
|
|
54
|
+
*/
|
|
55
|
+
checkAll(): Promise<HealthReport>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Health Checker Service
|
|
3
|
+
*
|
|
4
|
+
* Verifies gateway availability and individual MCP server health using
|
|
5
|
+
* template-defined check methods (tool_call or http) with configurable timeouts.
|
|
6
|
+
*
|
|
7
|
+
* Consumed by: mcp status command (Story 2.1), mcp credential validate (Story 2.3),
|
|
8
|
+
* and McpContextInjector (Epic 3, Story 3.1).
|
|
9
|
+
*/
|
|
10
|
+
import { createLogger } from '../../utils/logger.js';
|
|
11
|
+
const logger = createLogger({ namespace: 'services:mcp:health-checker' });
|
|
12
|
+
/** Gateway health check timeout in milliseconds */
|
|
13
|
+
const GATEWAY_TIMEOUT_MS = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* McpHealthChecker verifies gateway and individual MCP server health.
|
|
16
|
+
*
|
|
17
|
+
* - Gateway is checked first; if offline, all servers are reported as offline
|
|
18
|
+
* - Per-server checks run in parallel via Promise.allSettled
|
|
19
|
+
* - Supports tool_call and http check strategies per server template config
|
|
20
|
+
*/
|
|
21
|
+
export class McpHealthChecker {
|
|
22
|
+
configManager;
|
|
23
|
+
gatewayUrl;
|
|
24
|
+
constructor(configManager) {
|
|
25
|
+
this.configManager = configManager;
|
|
26
|
+
this.gatewayUrl = configManager.getGatewayUrl();
|
|
27
|
+
logger.debug('McpHealthChecker initialized with gateway: %s', this.gatewayUrl);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check gateway health by hitting its /health endpoint
|
|
31
|
+
*
|
|
32
|
+
* @returns Gateway health status with latency measurement
|
|
33
|
+
*/
|
|
34
|
+
async checkGateway() {
|
|
35
|
+
const url = `${this.gatewayUrl}/health`;
|
|
36
|
+
logger.debug('Checking gateway health at %s', url);
|
|
37
|
+
const start = Date.now();
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS) });
|
|
40
|
+
const latency = Date.now() - start;
|
|
41
|
+
if (response.ok) {
|
|
42
|
+
logger.debug('Gateway is healthy (latency: %dms)', latency);
|
|
43
|
+
return { status: 'healthy', latency, url: this.gatewayUrl };
|
|
44
|
+
}
|
|
45
|
+
logger.warn('Gateway returned non-ok status: %d', response.status);
|
|
46
|
+
return { status: 'unhealthy', latency, url: this.gatewayUrl };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger.warn('Gateway health check failed: %s', error.message);
|
|
50
|
+
return { status: 'offline', latency: null, url: this.gatewayUrl };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check an individual MCP server's health using its template-configured method
|
|
55
|
+
*
|
|
56
|
+
* @param server - Server configuration with health check settings
|
|
57
|
+
* @returns Server health result with status, latency, and optional error
|
|
58
|
+
*/
|
|
59
|
+
async checkServer(server) {
|
|
60
|
+
const { healthCheck, name } = server;
|
|
61
|
+
logger.debug('Checking server "%s" via %s method (timeout: %dms)', name, healthCheck.method, healthCheck.timeout);
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
try {
|
|
64
|
+
let response;
|
|
65
|
+
if (healthCheck.method === 'tool_call') {
|
|
66
|
+
response = await fetch(this.gatewayUrl, {
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
method: 'tools/call',
|
|
69
|
+
params: { name: healthCheck.command },
|
|
70
|
+
}),
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
method: 'POST',
|
|
73
|
+
signal: AbortSignal.timeout(healthCheck.timeout),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// http method
|
|
78
|
+
response = await fetch(healthCheck.url, {
|
|
79
|
+
signal: AbortSignal.timeout(healthCheck.timeout),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const latency = Date.now() - start;
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
logger.debug('Server "%s" is healthy (latency: %dms)', name, latency);
|
|
85
|
+
return { name, status: 'healthy', latency, error: null, useCase: server.useCase };
|
|
86
|
+
}
|
|
87
|
+
const errorMsg = `HTTP ${response.status}`;
|
|
88
|
+
logger.warn('Server "%s" returned non-ok: %s', name, errorMsg);
|
|
89
|
+
return { name, status: 'unhealthy', latency, error: errorMsg, useCase: server.useCase };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const err = error;
|
|
93
|
+
if (err.name === 'AbortError' || err.name === 'TimeoutError') {
|
|
94
|
+
const msg = `Health check timed out after ${healthCheck.timeout}ms`;
|
|
95
|
+
logger.warn('Server "%s" timed out', name);
|
|
96
|
+
return { name, status: 'offline', latency: null, error: msg, useCase: server.useCase };
|
|
97
|
+
}
|
|
98
|
+
logger.warn('Server "%s" health check failed: %s', name, err.message);
|
|
99
|
+
return { name, status: 'offline', latency: null, error: err.message, useCase: server.useCase };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check health of the gateway and all configured servers.
|
|
104
|
+
*
|
|
105
|
+
* If the gateway is offline, all servers are immediately reported as offline
|
|
106
|
+
* without making individual server health checks.
|
|
107
|
+
*
|
|
108
|
+
* Enabled servers are checked in parallel via Promise.allSettled.
|
|
109
|
+
*
|
|
110
|
+
* @returns Complete health report for gateway and all servers
|
|
111
|
+
*/
|
|
112
|
+
async checkAll() {
|
|
113
|
+
logger.info('Starting health check for gateway and all servers');
|
|
114
|
+
const gateway = await this.checkGateway();
|
|
115
|
+
// Short-circuit: if gateway is offline, skip individual server checks
|
|
116
|
+
if (gateway.status === 'offline') {
|
|
117
|
+
logger.warn('Gateway is offline — marking all servers as offline');
|
|
118
|
+
const servers = this.configManager.getEnabledServers();
|
|
119
|
+
return {
|
|
120
|
+
gateway,
|
|
121
|
+
servers: servers.map((s) => ({
|
|
122
|
+
error: 'Gateway is offline',
|
|
123
|
+
latency: null,
|
|
124
|
+
name: s.name,
|
|
125
|
+
status: 'offline',
|
|
126
|
+
useCase: s.useCase,
|
|
127
|
+
})),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const allServers = this.configManager.getEnabledServers();
|
|
131
|
+
// Separate enabled from disabled servers
|
|
132
|
+
const enabledServers = allServers.filter((s) => s.enabled);
|
|
133
|
+
const disabledServers = allServers.filter((s) => !s.enabled);
|
|
134
|
+
// Check enabled servers in parallel
|
|
135
|
+
const settledResults = await Promise.allSettled(enabledServers.map((s) => this.checkServer(s)));
|
|
136
|
+
const serverResults = settledResults.map((result, index) => {
|
|
137
|
+
if (result.status === 'fulfilled') {
|
|
138
|
+
return result.value;
|
|
139
|
+
}
|
|
140
|
+
// Rejected promise — unexpected error
|
|
141
|
+
const server = enabledServers[index];
|
|
142
|
+
return {
|
|
143
|
+
error: result.reason.message,
|
|
144
|
+
latency: null,
|
|
145
|
+
name: server.name,
|
|
146
|
+
status: 'unhealthy',
|
|
147
|
+
useCase: server.useCase,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
// Add disabled servers to the report
|
|
151
|
+
const disabledResults = disabledServers.map((s) => ({
|
|
152
|
+
error: null,
|
|
153
|
+
latency: null,
|
|
154
|
+
name: s.name,
|
|
155
|
+
status: 'disabled',
|
|
156
|
+
useCase: s.useCase,
|
|
157
|
+
}));
|
|
158
|
+
const servers = [...serverResults, ...disabledResults];
|
|
159
|
+
logger.info('Health check complete: gateway=%s, servers=%d', gateway.status, servers.length);
|
|
160
|
+
return { gateway, servers };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Types for the MCP health check system, extending the base types from src/mcp/types.ts
|
|
5
|
+
* with additional configuration types needed by McpHealthChecker.
|
|
6
|
+
*/
|
|
7
|
+
export type { GatewayHealthStatus, HealthReport, HealthStatus, ServerHealthStatus } from '../../../mcp/types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Health check configuration for an MCP server template
|
|
10
|
+
*/
|
|
11
|
+
export interface HealthCheckConfig {
|
|
12
|
+
/** Tool name to invoke (for tool_call method) */
|
|
13
|
+
command?: string;
|
|
14
|
+
/** Check strategy: invoke a tool via the gateway, or make a direct HTTP request */
|
|
15
|
+
method: 'http' | 'tool_call';
|
|
16
|
+
/** Timeout in milliseconds */
|
|
17
|
+
timeout: number;
|
|
18
|
+
/** Direct URL to check (for http method) */
|
|
19
|
+
url?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extended MCP server configuration including health check settings.
|
|
23
|
+
* This represents a server entry as loaded from preset/template YAML files,
|
|
24
|
+
* with all fields the health checker needs to perform checks.
|
|
25
|
+
*/
|
|
26
|
+
export interface McpServerWithHealthCheck {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
healthCheck: HealthCheckConfig;
|
|
29
|
+
name: string;
|
|
30
|
+
useCase?: string;
|
|
31
|
+
}
|
|
@@ -301,7 +301,7 @@ Use the file at the path above to document:
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
// Map agent type to valid values
|
|
304
|
-
const validAgentTypes = ['analyst', 'architect', 'dev', 'pm', 'prd-fixer', 'quick-flow-solo-dev', 'sm', 'tea', 'tech-writer', 'ux-designer'];
|
|
304
|
+
const validAgentTypes = ['analyst', 'architect', 'dev', 'pm', 'prd-fixer', 'qa', 'quick-flow-solo-dev', 'sm', 'tea', 'tech-writer', 'ux-designer'];
|
|
305
305
|
const agentType = validAgentTypes.includes(task.agentType)
|
|
306
306
|
? task.agentType
|
|
307
307
|
: 'dev';
|
|
@@ -57,7 +57,8 @@ export declare class TaskDecompositionService {
|
|
|
57
57
|
*/
|
|
58
58
|
private parseTaskGraph;
|
|
59
59
|
/**
|
|
60
|
-
* Validate YAML and ask Claude to fix it if invalid
|
|
60
|
+
* Validate YAML and ask Claude to fix it if invalid.
|
|
61
|
+
* Retries up to 2 times with escalating strictness.
|
|
61
62
|
*/
|
|
62
63
|
private validateAndFixYaml;
|
|
63
64
|
/**
|
|
@@ -39,12 +39,31 @@ export class TaskDecompositionService {
|
|
|
39
39
|
const prompt = this.buildDecompositionPrompt(options, targetFiles, sessionDir);
|
|
40
40
|
this.logger.debug({ promptLength: prompt.length }, 'Built decomposition prompt');
|
|
41
41
|
// Execute Claude agent to generate task graph
|
|
42
|
+
this.logger.info('Sending goal to AI agent for decomposition (this may take 2-5 minutes)...');
|
|
43
|
+
const startTime = Date.now();
|
|
42
44
|
const result = await this.agentRunner.runAgent(prompt, {
|
|
43
45
|
agentType: (options.agent ?? 'architect'),
|
|
46
|
+
cwd: options.cwd,
|
|
47
|
+
// Disable tools and session persistence for YAML-only generation.
|
|
48
|
+
// The subprocess doesn't need to read files or create sessions —
|
|
49
|
+
// it just needs to produce a structured YAML task graph.
|
|
50
|
+
flags: ['--tools', '""', '--no-session-persistence'],
|
|
44
51
|
model: options.model,
|
|
45
52
|
references: options.contextFiles,
|
|
53
|
+
systemPrompt: [
|
|
54
|
+
'You are a structured YAML generator for task decomposition.',
|
|
55
|
+
'Your ENTIRE response must be valid YAML — nothing else.',
|
|
56
|
+
'Start directly with "masterPrompt: |" on the first line.',
|
|
57
|
+
'Do NOT use markdown code fences (```yaml or ```).',
|
|
58
|
+
'Do NOT write any prose, summaries, commentary, or explanations.',
|
|
59
|
+
'The YAML must contain a "masterPrompt:" string key and a "tasks:" array.',
|
|
60
|
+
'Each task in the array must have: id, title, description, estimatedMinutes, dependencies, parallelizable, agentType, prompt, outputFile.',
|
|
61
|
+
'Failure to output pure YAML will cause a fatal parsing error.',
|
|
62
|
+
].join('\n'),
|
|
46
63
|
timeout: options.taskTimeout ?? 600_000, // 10 min default for planning
|
|
47
64
|
});
|
|
65
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
66
|
+
this.logger.info({ duration: `${elapsed}s`, outputLength: result.output.length }, 'AI agent response received');
|
|
48
67
|
if (!result.success) {
|
|
49
68
|
throw new Error(`Failed to decompose goal: ${result.errors}`);
|
|
50
69
|
}
|
|
@@ -472,28 +491,42 @@ ${options.perFile ? '5. **CRITICAL**: Create one task per file (per-file mode is
|
|
|
472
491
|
}
|
|
473
492
|
}
|
|
474
493
|
/**
|
|
475
|
-
* Validate YAML and ask Claude to fix it if invalid
|
|
494
|
+
* Validate YAML and ask Claude to fix it if invalid.
|
|
495
|
+
* Retries up to 2 times with escalating strictness.
|
|
476
496
|
*/
|
|
477
497
|
async validateAndFixYaml(output, options, sessionDir) {
|
|
478
498
|
this.logger.debug('Validating YAML output');
|
|
499
|
+
// First check: does the output even look like YAML?
|
|
500
|
+
const trimmed = output.trim();
|
|
501
|
+
const looksLikeYaml = trimmed.startsWith('masterPrompt:') || trimmed.startsWith('tasks:');
|
|
502
|
+
if (!looksLikeYaml) {
|
|
503
|
+
this.logger.warn('Output does not look like YAML (missing masterPrompt: or tasks: at start). Will attempt extraction and fix.');
|
|
504
|
+
}
|
|
479
505
|
// Try to extract and parse the YAML
|
|
480
506
|
try {
|
|
481
507
|
const yamlContent = this.extractYaml(output);
|
|
482
|
-
yaml.load(yamlContent);
|
|
483
|
-
|
|
508
|
+
const parsed = yaml.load(yamlContent);
|
|
509
|
+
// Validate structure, not just syntax
|
|
510
|
+
if (!parsed || !parsed.tasks || !Array.isArray(parsed.tasks) || parsed.tasks.length === 0) {
|
|
511
|
+
throw new Error('YAML parsed but missing required "tasks" array with at least one task');
|
|
512
|
+
}
|
|
513
|
+
this.logger.info({ taskCount: parsed.tasks.length }, 'YAML validation passed');
|
|
484
514
|
return output; // YAML is valid, return as-is
|
|
485
515
|
}
|
|
486
516
|
catch (error) {
|
|
487
|
-
this.logger.warn({ error: error.message }, 'YAML validation failed,
|
|
488
|
-
//
|
|
489
|
-
|
|
517
|
+
this.logger.warn({ error: error.message }, 'YAML validation failed, attempting fix (attempt 1/2)');
|
|
518
|
+
// Attempt up to 2 fix rounds
|
|
519
|
+
let lastError = error;
|
|
520
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
521
|
+
const fixPrompt = attempt === 1
|
|
522
|
+
? `FIX THIS YAML - OUTPUT ONLY THE FIXED YAML, NOTHING ELSE.
|
|
490
523
|
|
|
491
524
|
**CRITICAL: Your response must be PURE YAML only. Start directly with "masterPrompt: |"**
|
|
492
525
|
- Do NOT write any explanation
|
|
493
526
|
- Do NOT use markdown code fences
|
|
494
527
|
- Do NOT say "Here's the fixed YAML" or anything similar
|
|
495
528
|
|
|
496
|
-
ERROR TO FIX: ${
|
|
529
|
+
ERROR TO FIX: ${lastError.message}
|
|
497
530
|
|
|
498
531
|
YAML TO FIX:
|
|
499
532
|
${output}
|
|
@@ -502,40 +535,61 @@ RULES:
|
|
|
502
535
|
- Use 2 spaces for indentation
|
|
503
536
|
- Quote strings with special characters
|
|
504
537
|
- Ensure valid YAML syntax
|
|
538
|
+
- MUST have "masterPrompt:" key AND "tasks:" array with task objects
|
|
505
539
|
|
|
506
|
-
YOUR RESPONSE MUST START WITH "masterPrompt: |" - NO OTHER TEXT ALLOWED
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.
|
|
521
|
-
|
|
540
|
+
YOUR RESPONSE MUST START WITH "masterPrompt: |" - NO OTHER TEXT ALLOWED!`
|
|
541
|
+
: `YOU FAILED TO PRODUCE VALID YAML. THIS IS YOUR LAST CHANCE.
|
|
542
|
+
|
|
543
|
+
RULES:
|
|
544
|
+
1. Output ONLY valid YAML
|
|
545
|
+
2. First line MUST be: masterPrompt: |
|
|
546
|
+
3. MUST include tasks: array with id, title, description, estimatedMinutes, dependencies, parallelizable, agentType, prompt, outputFile
|
|
547
|
+
4. NO markdown, NO prose, NO code fences
|
|
548
|
+
5. Previous error: ${lastError.message}
|
|
549
|
+
|
|
550
|
+
ORIGINAL GOAL: ${options.goal}
|
|
551
|
+
|
|
552
|
+
Produce the task decomposition as PURE YAML. START NOW:`;
|
|
553
|
+
this.logger.info({ attempt }, `Asking Claude to fix YAML (attempt ${attempt}/2)`);
|
|
554
|
+
const fixResult = await this.agentRunner.runAgent(fixPrompt, {
|
|
555
|
+
agentType: 'architect',
|
|
556
|
+
flags: ['--tools', '""', '--no-session-persistence'],
|
|
557
|
+
model: options.model,
|
|
558
|
+
systemPrompt: 'You are a YAML generator. Output ONLY valid YAML. No prose. No markdown. Start with "masterPrompt: |".',
|
|
559
|
+
timeout: 120_000, // 2 minutes for fix
|
|
560
|
+
});
|
|
561
|
+
if (!fixResult.success) {
|
|
562
|
+
lastError = new Error(`Fix attempt ${attempt} failed: ${fixResult.errors}`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
522
565
|
try {
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
566
|
+
const fixedYaml = this.extractYaml(fixResult.output);
|
|
567
|
+
const parsed = yaml.load(fixedYaml);
|
|
568
|
+
if (!parsed || !parsed.tasks || !Array.isArray(parsed.tasks) || parsed.tasks.length === 0) {
|
|
569
|
+
throw new Error('Fixed YAML still missing required "tasks" array');
|
|
570
|
+
}
|
|
571
|
+
this.logger.info({ attempt, taskCount: parsed.tasks.length }, 'YAML fix succeeded');
|
|
572
|
+
// Save the fixed YAML for reference
|
|
573
|
+
try {
|
|
574
|
+
const fixedYamlPath = `${sessionDir}/fixed-yaml-attempt-${attempt}.txt`;
|
|
575
|
+
await this.fileManager.writeFile(fixedYamlPath, fixResult.output);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// Ignore save errors
|
|
579
|
+
}
|
|
580
|
+
return fixResult.output;
|
|
526
581
|
}
|
|
527
|
-
catch {
|
|
528
|
-
|
|
582
|
+
catch (fixError) {
|
|
583
|
+
lastError = fixError;
|
|
584
|
+
this.logger.warn({ attempt, error: lastError.message }, `YAML fix attempt ${attempt} produced invalid output`);
|
|
529
585
|
}
|
|
530
|
-
return fixResult.output;
|
|
531
|
-
}
|
|
532
|
-
catch (fixError) {
|
|
533
|
-
this.logger.error({ error: fixError.message }, 'Claude failed to fix YAML properly');
|
|
534
|
-
throw new Error(`Claude could not fix the YAML errors.\n` +
|
|
535
|
-
`Original error: ${error.message}\n` +
|
|
536
|
-
`Fix attempt error: ${fixError.message}\n\n` +
|
|
537
|
-
`Raw output saved to: ${sessionDir}/raw-claude-output.txt`);
|
|
538
586
|
}
|
|
587
|
+
// All attempts failed
|
|
588
|
+
this.logger.error({ error: lastError.message }, 'All YAML fix attempts failed');
|
|
589
|
+
throw new Error(`Could not produce valid YAML after 2 fix attempts.\n` +
|
|
590
|
+
`Last error: ${lastError.message}\n\n` +
|
|
591
|
+
`Raw output saved to: ${sessionDir}/raw-claude-output.txt\n` +
|
|
592
|
+
`TIP: Try running with --verbose to see the raw output, or check the session directory.`);
|
|
539
593
|
}
|
|
540
594
|
}
|
|
541
595
|
/**
|