@hyperdrive.bot/bmad-workflow 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +34 -75
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +89 -0
  36. package/dist/commands/workflow.js +487 -14
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/index.d.ts +1 -0
  45. package/dist/models/index.js +1 -0
  46. package/dist/models/workflow-callbacks.d.ts +251 -0
  47. package/dist/models/workflow-callbacks.js +10 -0
  48. package/dist/models/workflow-config.d.ts +77 -0
  49. package/dist/models/workflow-result.d.ts +7 -0
  50. package/dist/services/WorkflowReporter.d.ts +165 -0
  51. package/dist/services/WorkflowReporter.js +691 -0
  52. package/dist/services/agents/claude-agent-runner.js +25 -4
  53. package/dist/services/file-system/path-resolver.d.ts +10 -0
  54. package/dist/services/file-system/path-resolver.js +12 -0
  55. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  56. package/dist/services/mcp/mcp-config-manager.js +146 -0
  57. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  58. package/dist/services/mcp/mcp-context-injector.js +168 -0
  59. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  60. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  61. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  62. package/dist/services/mcp/mcp-health-checker.js +162 -0
  63. package/dist/services/mcp/types/health-types.d.ts +31 -0
  64. package/dist/services/mcp/types/health-types.js +7 -0
  65. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  66. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  67. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  68. package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
  69. package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
  70. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  71. package/dist/services/review/ai-review-scanner.js +142 -0
  72. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  73. package/dist/services/review/coderabbit-scanner.js +31 -0
  74. package/dist/services/review/index.d.ts +20 -0
  75. package/dist/services/review/index.js +15 -0
  76. package/dist/services/review/lint-scanner.d.ts +46 -0
  77. package/dist/services/review/lint-scanner.js +172 -0
  78. package/dist/services/review/review-config.d.ts +62 -0
  79. package/dist/services/review/review-config.js +91 -0
  80. package/dist/services/review/review-phase-executor.d.ts +69 -0
  81. package/dist/services/review/review-phase-executor.js +152 -0
  82. package/dist/services/review/review-queue.d.ts +98 -0
  83. package/dist/services/review/review-queue.js +174 -0
  84. package/dist/services/review/review-reporter.d.ts +94 -0
  85. package/dist/services/review/review-reporter.js +386 -0
  86. package/dist/services/review/scanner-factory.d.ts +42 -0
  87. package/dist/services/review/scanner-factory.js +60 -0
  88. package/dist/services/review/self-heal-loop.d.ts +58 -0
  89. package/dist/services/review/self-heal-loop.js +132 -0
  90. package/dist/services/review/severity-classifier.d.ts +17 -0
  91. package/dist/services/review/severity-classifier.js +314 -0
  92. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  93. package/dist/services/review/tech-debt-tracker.js +245 -0
  94. package/dist/services/review/types.d.ts +93 -0
  95. package/dist/services/review/types.js +23 -0
  96. package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
  97. package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
  98. package/dist/services/validation/config-validator.d.ts +84 -0
  99. package/dist/services/validation/config-validator.js +78 -0
  100. package/dist/utils/colors.d.ts +10 -10
  101. package/dist/utils/colors.js +15 -15
  102. package/dist/utils/credential-utils.d.ts +14 -0
  103. package/dist/utils/credential-utils.js +19 -0
  104. package/dist/utils/duration.d.ts +41 -0
  105. package/dist/utils/duration.js +89 -0
  106. package/dist/utils/listr2-helpers.d.ts +216 -0
  107. package/dist/utils/listr2-helpers.js +334 -0
  108. package/dist/utils/shared-flags.d.ts +1 -0
  109. package/dist/utils/shared-flags.js +11 -2
  110. package/package.json +6 -3
@@ -155,22 +155,43 @@ export class ClaudeAgentRunner {
155
155
  tempDir = await mkdtemp(join(tmpdir(), 'claude-prompt-'));
156
156
  tempFile = join(tempDir, 'prompt.txt');
157
157
  await writeFile(tempFile, prompt, 'utf8');
158
+ // Write system prompt to temp file if provided (Claude-only feature)
159
+ // Uses --system-prompt (full replacement) instead of --append-system-prompt
160
+ // to prevent the default Claude Code system prompt from competing with
161
+ // our output format instructions (e.g., YAML-only output).
162
+ let systemPromptArg = '';
163
+ if (options.systemPrompt) {
164
+ const systemPromptFile = join(tempDir, 'system-prompt.txt');
165
+ await writeFile(systemPromptFile, options.systemPrompt, 'utf8');
166
+ systemPromptArg = `--system-prompt "$(cat '${systemPromptFile}')"`;
167
+ }
158
168
  // Build command with temp file
159
169
  const flags = this.config.flags.join(' ');
160
170
  const modelArg = options.model ? `${this.config.modelFlag} ${options.model}` : '';
161
- const command = `${this.config.command} ${flags} ${modelArg} < "${tempFile}"`.replace(/\s+/g, ' ').trim();
171
+ const extraFlags = options.flags ? options.flags.join(' ') : '';
172
+ const command = `${this.config.command} ${flags} ${modelArg} ${extraFlags} ${systemPromptArg} < "${tempFile}"`.replace(/\s+/g, ' ').trim();
162
173
  // Log the command being executed
163
174
  this.logger.info({
164
175
  promptLength: prompt.length,
165
176
  tempFile,
166
177
  }, 'Executing command with temp file');
167
178
  // Use exec instead of spawn for better shell compatibility
168
- const { stderr, stdout } = await execAsync(command, {
169
- env: process.env,
179
+ // Note: setting CLAUDECODE to undefined in a spread does NOT remove it —
180
+ // Node coerces undefined to the string "undefined", which is still truthy.
181
+ // We must delete the key from the env object to fully unset it.
182
+ const env = { ...process.env };
183
+ delete env.CLAUDECODE;
184
+ const execOptions = {
185
+ env,
170
186
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer
171
187
  shell: process.env.SHELL || '/bin/bash',
172
188
  timeout,
173
- });
189
+ };
190
+ if (options.cwd) {
191
+ execOptions.cwd = options.cwd;
192
+ this.logger.info({ cwd: options.cwd }, 'Setting working directory for agent');
193
+ }
194
+ const { stderr, stdout } = await execAsync(command, execOptions);
174
195
  stdoutData = stdout;
175
196
  stderrData = stderr;
176
197
  const duration = Date.now() - startTime;
@@ -5,6 +5,7 @@
5
5
  * All commands use this service to get correct directories for epics, stories, and PRD files.
6
6
  */
7
7
  import type pino from 'pino';
8
+ import type { ReviewConfig } from '../validation/config-validator.js';
8
9
  import { FileManager } from './file-manager.js';
9
10
  /**
10
11
  * PathResolver service for resolving and validating file paths
@@ -118,6 +119,15 @@ export declare class PathResolver {
118
119
  * // Returns: '/path/to/project/docs/qa/stories'
119
120
  */
120
121
  getQaStoryDir(): string;
122
+ /**
123
+ * Get the review configuration section from core-config.yaml
124
+ *
125
+ * Returns the parsed review configuration if present, or undefined if not.
126
+ * The review section contains scanner, severity, self-heal, and path-rule settings.
127
+ *
128
+ * @returns Review configuration object or undefined
129
+ */
130
+ getReviewConfig(): ReviewConfig | undefined;
121
131
  /**
122
132
  * Get the story directory path
123
133
  *
@@ -154,6 +154,18 @@ export class PathResolver {
154
154
  this.logger.debug('Getting QA story directory: %s', paths.qaStoryDir);
155
155
  return paths.qaStoryDir;
156
156
  }
157
+ /**
158
+ * Get the review configuration section from core-config.yaml
159
+ *
160
+ * Returns the parsed review configuration if present, or undefined if not.
161
+ * The review section contains scanner, severity, self-heal, and path-rule settings.
162
+ *
163
+ * @returns Review configuration object or undefined
164
+ */
165
+ getReviewConfig() {
166
+ const config = this.loadConfig();
167
+ return config.review;
168
+ }
157
169
  /**
158
170
  * Get the story directory path
159
171
  *
@@ -0,0 +1,54 @@
1
+ /**
2
+ * MCP Configuration Manager
3
+ *
4
+ * Manages the active MCP gateway configuration at ~/.bmad/mcp/config.yaml.
5
+ * Provides methods to add/remove servers, apply presets, and query config state.
6
+ */
7
+ import type { IConfigManager, McpServerConfig, ServerTemplate } from '../../mcp/types.js';
8
+ export declare class McpConfigManager implements IConfigManager {
9
+ private config;
10
+ private readonly configPath;
11
+ constructor(configPath?: string);
12
+ /**
13
+ * Add a server template to the active configuration
14
+ */
15
+ addServer(template: ServerTemplate): void;
16
+ /**
17
+ * Apply a named preset to the active configuration
18
+ */
19
+ applyPreset(presetName: string): void;
20
+ /**
21
+ * Get all configured servers with their enabled/disabled status
22
+ */
23
+ getEnabledServers(): McpServerConfig[];
24
+ /**
25
+ * Get the configured gateway URL
26
+ */
27
+ getGatewayUrl(): string;
28
+ /**
29
+ * Get the name of the currently active preset
30
+ */
31
+ getPresetName(): string;
32
+ /**
33
+ * Remove a server from the active configuration
34
+ *
35
+ * @returns true if server was found and removed, false if not found
36
+ */
37
+ removeServer(name: string): boolean;
38
+ /**
39
+ * Load the active config from disk
40
+ */
41
+ private loadConfig;
42
+ /**
43
+ * Load a preset YAML file
44
+ */
45
+ private loadPreset;
46
+ /**
47
+ * Resolve preset inheritance chain (e.g., full extends research extends minimal)
48
+ */
49
+ private resolvePresetInheritance;
50
+ /**
51
+ * Save the active config to disk
52
+ */
53
+ private saveConfig;
54
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * MCP Configuration Manager
3
+ *
4
+ * Manages the active MCP gateway configuration at ~/.bmad/mcp/config.yaml.
5
+ * Provides methods to add/remove servers, apply presets, and query config state.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import yaml from 'js-yaml';
12
+ import { createLogger } from '../../utils/logger.js';
13
+ const logger = createLogger({ namespace: 'services:mcp:config-manager' });
14
+ const CONFIG_DIR = join(homedir(), '.bmad', 'mcp');
15
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
16
+ export class McpConfigManager {
17
+ config;
18
+ configPath;
19
+ constructor(configPath) {
20
+ this.configPath = configPath || CONFIG_FILE;
21
+ this.config = this.loadConfig();
22
+ }
23
+ /**
24
+ * Add a server template to the active configuration
25
+ */
26
+ addServer(template) {
27
+ logger.info('Adding server: %s', template.name);
28
+ this.config.servers[template.name] = template;
29
+ this.saveConfig();
30
+ }
31
+ /**
32
+ * Apply a named preset to the active configuration
33
+ */
34
+ applyPreset(presetName) {
35
+ logger.info('Applying preset: %s', presetName);
36
+ const validPresets = ['minimal', 'research', 'full'];
37
+ if (!validPresets.includes(presetName)) {
38
+ throw new Error(`Invalid preset '${presetName}'. Available presets: ${validPresets.join(', ')}`);
39
+ }
40
+ const presetConfig = this.loadPreset(presetName);
41
+ this.config = {
42
+ preset: presetName,
43
+ servers: {},
44
+ };
45
+ // Resolve preset inheritance
46
+ const resolvedServers = this.resolvePresetInheritance(presetConfig);
47
+ for (const [name, serverConfig] of Object.entries(resolvedServers)) {
48
+ this.config.servers[name] = { ...serverConfig, name };
49
+ }
50
+ this.saveConfig();
51
+ }
52
+ /**
53
+ * Get all configured servers with their enabled/disabled status
54
+ */
55
+ getEnabledServers() {
56
+ return Object.values(this.config.servers).map((server) => ({
57
+ apiKeyRequired: server.api_key_required,
58
+ enabled: server.enabled,
59
+ name: server.name,
60
+ useCase: server.use_case,
61
+ }));
62
+ }
63
+ /**
64
+ * Get the configured gateway URL
65
+ */
66
+ getGatewayUrl() {
67
+ return 'http://localhost:8080';
68
+ }
69
+ /**
70
+ * Get the name of the currently active preset
71
+ */
72
+ getPresetName() {
73
+ return this.config.preset || 'custom';
74
+ }
75
+ /**
76
+ * Remove a server from the active configuration
77
+ *
78
+ * @returns true if server was found and removed, false if not found
79
+ */
80
+ removeServer(name) {
81
+ logger.info('Removing server: %s', name);
82
+ if (this.config.servers[name]) {
83
+ delete this.config.servers[name];
84
+ this.saveConfig();
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+ /**
90
+ * Load the active config from disk
91
+ */
92
+ loadConfig() {
93
+ try {
94
+ if (existsSync(this.configPath)) {
95
+ const content = readFileSync(this.configPath, 'utf8');
96
+ const parsed = yaml.load(content);
97
+ if (parsed && typeof parsed === 'object') {
98
+ return {
99
+ preset: parsed.preset,
100
+ servers: parsed.servers || {},
101
+ };
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ logger.warn('Failed to load config from %s: %s', this.configPath, error.message);
107
+ }
108
+ return { servers: {} };
109
+ }
110
+ /**
111
+ * Load a preset YAML file
112
+ */
113
+ loadPreset(presetName) {
114
+ const currentDir = dirname(fileURLToPath(import.meta.url));
115
+ const presetPath = resolve(currentDir, '..', '..', 'mcp', 'presets', `${presetName}.yaml`);
116
+ if (!existsSync(presetPath)) {
117
+ throw new Error(`Preset file not found: ${presetPath}`);
118
+ }
119
+ const content = readFileSync(presetPath, 'utf8');
120
+ return yaml.load(content);
121
+ }
122
+ /**
123
+ * Resolve preset inheritance chain (e.g., full extends research extends minimal)
124
+ */
125
+ resolvePresetInheritance(presetConfig) {
126
+ let servers = {};
127
+ if (presetConfig.extends) {
128
+ const parentConfig = this.loadPreset(presetConfig.extends);
129
+ servers = this.resolvePresetInheritance(parentConfig);
130
+ }
131
+ // Merge current preset servers (overrides parent)
132
+ return { ...servers, ...presetConfig.servers };
133
+ }
134
+ /**
135
+ * Save the active config to disk
136
+ */
137
+ saveConfig() {
138
+ const dir = dirname(this.configPath);
139
+ if (!existsSync(dir)) {
140
+ mkdirSync(dir, { recursive: true });
141
+ }
142
+ const content = yaml.dump(this.config, { sortKeys: true });
143
+ writeFileSync(this.configPath, content, 'utf8');
144
+ logger.info('Config saved to %s', this.configPath);
145
+ }
146
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * MCP Context Injector
3
+ *
4
+ * Provides MCP tool discovery context to headless pipeline agents.
5
+ * Returns formatted instructions that are prepended to agent prompts,
6
+ * enabling agents to discover and use external MCP tools.
7
+ *
8
+ * Interface consumed by WorkflowOrchestrator (Story 3.3).
9
+ * Concrete implementation provided by Story 3.1.
10
+ */
11
+ import type { IConfigManager, IHealthChecker } from '../../mcp/types.js';
12
+ /**
13
+ * A single MCP tool entry describing a server and its available tools
14
+ */
15
+ export interface McpToolEntry {
16
+ description: string;
17
+ name: string;
18
+ tools: string[];
19
+ }
20
+ /**
21
+ * MCP context returned by the injector for a specific agent/phase combination
22
+ */
23
+ export interface McpContext {
24
+ /** Formatted tool descriptions from healthy MCP servers */
25
+ availableTools: McpToolEntry[];
26
+ /** The text to prepend to the agent prompt — empty string means no injection */
27
+ instructions: string;
28
+ }
29
+ /** Valid workflow phase identifiers for MCP context injection */
30
+ export type McpPhase = 'dev' | 'epic' | 'qa' | 'review' | 'story';
31
+ /**
32
+ * Interface for injecting MCP tool context into agent prompts
33
+ *
34
+ * Implementations handle: phase-server mapping, health checks,
35
+ * 500-token budget enforcement, and graceful degradation.
36
+ */
37
+ export interface McpContextInjector {
38
+ /**
39
+ * Get MCP context for a specific agent type and workflow phase
40
+ *
41
+ * @param agentType - The type of agent being spawned (e.g., 'architect', 'sm', 'dev', 'qa')
42
+ * @param phase - The workflow phase (epic, story, dev, review, qa)
43
+ * @returns MCP context with instructions to prepend (empty instructions = no injection)
44
+ */
45
+ getContextForAgent(agentType: string, phase: McpPhase): Promise<McpContext>;
46
+ }
47
+ /** Maximum token budget for MCP instructions (NFR3) */
48
+ export declare const MAX_TOKEN_BUDGET = 500;
49
+ /**
50
+ * McpContextInjectorImpl generates phase-aware MCP instructions for agent prompts.
51
+ *
52
+ * - Queries health status of configured servers
53
+ * - Filters to servers relevant for the workflow phase
54
+ * - Generates compact instructions under 500-token budget
55
+ * - Gracefully degrades: never throws, returns empty context on any error
56
+ */
57
+ export declare class McpContextInjectorImpl implements McpContextInjector {
58
+ private readonly configManager;
59
+ private readonly healthChecker;
60
+ constructor(configManager: IConfigManager, healthChecker: IHealthChecker);
61
+ /**
62
+ * Get MCP context for a specific agent type and workflow phase
63
+ *
64
+ * @param agentType - The type of agent being spawned
65
+ * @param phase - The workflow phase
66
+ * @returns MCP context with instructions (empty on any error)
67
+ */
68
+ getContextForAgent(agentType: string, phase: McpPhase): Promise<McpContext>;
69
+ /**
70
+ * Build compact MCP instructions for agent prompt injection
71
+ *
72
+ * @param tools - Available tool entries
73
+ * @param gatewayUrl - MCP gateway URL
74
+ * @returns Formatted instructions string (empty if no tools)
75
+ */
76
+ private buildMcpInstructions;
77
+ /**
78
+ * Filter servers to those allowed for the given workflow phase
79
+ *
80
+ * @param servers - Available healthy servers
81
+ * @param phase - Workflow phase
82
+ * @returns Servers relevant to the phase
83
+ */
84
+ private selectServersForPhase;
85
+ }
86
+ /**
87
+ * Approximate token count using chars/4 heuristic
88
+ *
89
+ * @param text - Text to count tokens for
90
+ * @returns Approximate token count
91
+ */
92
+ export declare function countTokens(text: string): number;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * MCP Context Injector
3
+ *
4
+ * Provides MCP tool discovery context to headless pipeline agents.
5
+ * Returns formatted instructions that are prepended to agent prompts,
6
+ * enabling agents to discover and use external MCP tools.
7
+ *
8
+ * Interface consumed by WorkflowOrchestrator (Story 3.3).
9
+ * Concrete implementation provided by Story 3.1.
10
+ */
11
+ import { createLogger } from '../../utils/logger.js';
12
+ const logger = createLogger({ namespace: 'services:mcp:context-injector' });
13
+ // --- Constants ---
14
+ /** Maximum token budget for MCP instructions (NFR3) */
15
+ export const MAX_TOKEN_BUDGET = 500;
16
+ /** Empty context returned on any error or when no servers are available */
17
+ const EMPTY_CONTEXT = { availableTools: [], instructions: '' };
18
+ /**
19
+ * Phase-to-server mapping. Defines which MCP servers are relevant per workflow phase.
20
+ * Not user-configurable — fixed per architecture decision.
21
+ */
22
+ const PHASE_SERVER_MAP = {
23
+ dev: ['context7', 'playwright'],
24
+ epic: ['context7', 'exa'],
25
+ qa: ['context7', 'playwright'],
26
+ review: ['context7'],
27
+ story: ['context7'],
28
+ };
29
+ /** Default servers for unrecognized phases */
30
+ const DEFAULT_SERVERS = ['context7'];
31
+ /**
32
+ * Known tool lists per server (used for instructions generation).
33
+ * Maps server name → representative tool names.
34
+ */
35
+ const SERVER_TOOLS = {
36
+ context7: ['resolve-library-id', 'get-library-docs'],
37
+ exa: ['web_search_exa', 'research_paper_search', 'company_research', 'competitor_finder'],
38
+ playwright: ['puppeteer_navigate', 'puppeteer_screenshot', 'puppeteer_click', 'puppeteer_fill'],
39
+ };
40
+ /** Use-case descriptions per server */
41
+ const SERVER_USE_CASES = {
42
+ context7: 'Library documentation lookup (current API references)',
43
+ exa: 'Web search, competitive analysis, academic research',
44
+ playwright: 'Browser automation, screenshots, E2E testing',
45
+ };
46
+ /** Tool selection priority text — must be included in all non-empty instructions */
47
+ const TOOL_PRIORITY_TEXT = `ALWAYS use native tools for local operations:
48
+ Read files → Read tool, Write files → Write/Edit, Search → Grep/Glob, Run commands → Bash
49
+ ONLY use MCP tools for external operations.`;
50
+ // --- Implementation ---
51
+ /**
52
+ * McpContextInjectorImpl generates phase-aware MCP instructions for agent prompts.
53
+ *
54
+ * - Queries health status of configured servers
55
+ * - Filters to servers relevant for the workflow phase
56
+ * - Generates compact instructions under 500-token budget
57
+ * - Gracefully degrades: never throws, returns empty context on any error
58
+ */
59
+ export class McpContextInjectorImpl {
60
+ configManager;
61
+ healthChecker;
62
+ constructor(configManager, healthChecker) {
63
+ this.configManager = configManager;
64
+ this.healthChecker = healthChecker;
65
+ logger.debug('McpContextInjectorImpl initialized');
66
+ }
67
+ /**
68
+ * Get MCP context for a specific agent type and workflow phase
69
+ *
70
+ * @param agentType - The type of agent being spawned
71
+ * @param phase - The workflow phase
72
+ * @returns MCP context with instructions (empty on any error)
73
+ */
74
+ async getContextForAgent(agentType, phase) {
75
+ try {
76
+ logger.debug('Getting context for agent=%s phase=%s', agentType, phase);
77
+ // 1. Get configured servers
78
+ const configuredServers = this.configManager.getEnabledServers();
79
+ // 2. Check health of all servers
80
+ const healthReport = await this.healthChecker.checkAll();
81
+ // 3. Filter to only healthy servers
82
+ const healthyServerNames = new Set(healthReport.servers
83
+ .filter((s) => s.status === 'healthy')
84
+ .map((s) => s.name));
85
+ const healthyServers = configuredServers.filter((s) => s.enabled && healthyServerNames.has(s.name));
86
+ // 4. Apply phase-server mapping
87
+ const phaseServers = this.selectServersForPhase(healthyServers, phase);
88
+ if (phaseServers.length === 0) {
89
+ logger.debug('No healthy servers available for phase=%s', phase);
90
+ return EMPTY_CONTEXT;
91
+ }
92
+ // 5. Build tool entries
93
+ const availableTools = phaseServers.map((server) => ({
94
+ description: SERVER_USE_CASES[server.name] || server.useCase || server.name,
95
+ name: server.name,
96
+ tools: SERVER_TOOLS[server.name] || [],
97
+ }));
98
+ // 6. Build instructions with token budget enforcement
99
+ const gatewayUrl = this.configManager.getGatewayUrl();
100
+ const instructions = this.buildMcpInstructions(availableTools, gatewayUrl);
101
+ logger.debug('Context generated: %d servers, %d tokens', availableTools.length, countTokens(instructions));
102
+ return { availableTools, instructions };
103
+ }
104
+ catch (error) {
105
+ logger.warn('Failed to get MCP context (returning empty): %s', error.message);
106
+ return EMPTY_CONTEXT;
107
+ }
108
+ }
109
+ /**
110
+ * Build compact MCP instructions for agent prompt injection
111
+ *
112
+ * @param tools - Available tool entries
113
+ * @param gatewayUrl - MCP gateway URL
114
+ * @returns Formatted instructions string (empty if no tools)
115
+ */
116
+ buildMcpInstructions(tools, gatewayUrl) {
117
+ if (tools.length === 0) {
118
+ return '';
119
+ }
120
+ let serverLines = tools.map((t) => `- **${t.name}**: ${t.description} [${t.tools.join(', ')}]`);
121
+ let instructions = assembleInstructions(serverLines, gatewayUrl);
122
+ // Enforce token budget — drop lowest-priority servers if over budget
123
+ while (countTokens(instructions) > MAX_TOKEN_BUDGET && serverLines.length > 1) {
124
+ serverLines = serverLines.slice(0, -1);
125
+ instructions = assembleInstructions(serverLines, gatewayUrl);
126
+ }
127
+ // If still over budget with one server, truncate
128
+ if (countTokens(instructions) > MAX_TOKEN_BUDGET && serverLines.length === 1) {
129
+ const maxChars = MAX_TOKEN_BUDGET * 4 - 100;
130
+ instructions = instructions.slice(0, maxChars);
131
+ }
132
+ return instructions;
133
+ }
134
+ /**
135
+ * Filter servers to those allowed for the given workflow phase
136
+ *
137
+ * @param servers - Available healthy servers
138
+ * @param phase - Workflow phase
139
+ * @returns Servers relevant to the phase
140
+ */
141
+ selectServersForPhase(servers, phase) {
142
+ const allowedNames = PHASE_SERVER_MAP[phase] || DEFAULT_SERVERS;
143
+ const allowedSet = new Set(allowedNames);
144
+ return servers.filter((s) => allowedSet.has(s.name));
145
+ }
146
+ }
147
+ // --- Utility functions (exported for testing) ---
148
+ /**
149
+ * Approximate token count using chars/4 heuristic
150
+ *
151
+ * @param text - Text to count tokens for
152
+ * @returns Approximate token count
153
+ */
154
+ export function countTokens(text) {
155
+ return Math.ceil(text.length / 4);
156
+ }
157
+ /**
158
+ * Assemble the full instructions string from server lines
159
+ */
160
+ function assembleInstructions(serverLines, gatewayUrl) {
161
+ return `## Available MCP Tools
162
+
163
+ ${serverLines.join('\n')}
164
+
165
+ Gateway: ${gatewayUrl}
166
+
167
+ ${TOOL_PRIORITY_TEXT}`;
168
+ }
@@ -0,0 +1,48 @@
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 type { CredentialEntry, ICredentialManager } from '../../mcp/types.js';
9
+ export declare class McpCredentialManager implements ICredentialManager {
10
+ private readonly credentialsPath;
11
+ private store;
12
+ constructor(credentialsPath?: string);
13
+ /**
14
+ * Get a credential value from the stored file (not env vars).
15
+ *
16
+ * @returns The stored credential value, or null if not found
17
+ */
18
+ get(key: string): string | null;
19
+ /**
20
+ * List all known credential entries with their configuration status
21
+ */
22
+ list(): CredentialEntry[];
23
+ /**
24
+ * Resolve a credential key value.
25
+ * Resolution chain: environment variable > stored credential file.
26
+ *
27
+ * @returns The credential value, or null if not found anywhere
28
+ */
29
+ resolve(key: string): string | null;
30
+ /**
31
+ * Remove a credential from the store.
32
+ *
33
+ * @returns true if the key was found and removed, false if not found
34
+ */
35
+ remove(key: string): boolean;
36
+ /**
37
+ * Store a credential key-value pair
38
+ */
39
+ set(key: string, value: string): void;
40
+ /**
41
+ * Load the credential store from disk
42
+ */
43
+ private loadStore;
44
+ /**
45
+ * Save the credential store to disk
46
+ */
47
+ private saveStore;
48
+ }