Compare commits

...

6 Commits

Author SHA1 Message Date
Markus Ende 54e4aa477e
Merge 1ee10ddcab into 43cfc01f2c 2026-02-28 15:22:43 -06:00
Alex Verkhovsky 43cfc01f2c
feat(core): add edge case hunter review task (#1790)
Method-driven review that exhaustively walks branching paths and
boundary conditions, reporting only unhandled gaps. Orthogonal to
the attitude-driven adversarial review for complementary coverage.
2026-02-27 23:08:30 -06:00
Markus Ende 1ee10ddcab
Merge branch 'main' into fix/copilot-hardcoded-bmm-config-path 2026-02-25 13:18:14 +01:00
Brian 147144a1ec
Merge branch 'main' into fix/copilot-hardcoded-bmm-config-path 2026-02-20 20:36:33 -06:00
Markus Ende 7a016d5efa fix: address CodeRabbit review comments for github-copilot installer
- Deduplicate selectedModules to prevent duplicate paths in markdown output
- Remove unused primaryModule variable (dead code)
- Refactor loadModuleConfig to accept installedModules param instead of hardcoded 'bmm'
- Make tech-writer BMM-only check explicit (entry.module !== 'bmm' returns null)
- Add test/test-github-copilot-installer.js with comprehensive unit tests
- Add test:copilot script to package.json and include in main test command
2026-02-19 20:37:52 +01:00
Markus Ende c017a5fdba fix: use module-specific config.yaml paths in GitHub Copilot installer
Replace hardcoded bmm/config.yaml references with dynamic module-based paths
so custom modules load their own config.yaml instead of the non-existent bmm config.

- createWorkflowPromptContent(): use entry.module from bmad-help.csv
- createAgentActivatorPromptContent(): use artifact.module
- createTechWriterPromptContent(): use entry.module for config and agent paths
- generateCopilotInstructions(): dynamically list installed module paths

Fixes #1708
2026-02-19 19:58:06 +01:00
5 changed files with 365 additions and 19 deletions

View File

@ -40,7 +40,8 @@
"lint:md": "markdownlint-cli2 \"**/*.md\"", "lint:md": "markdownlint-cli2 \"**/*.md\"",
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0", "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle", "rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
"test": "npm run test:schemas && npm run test:refs && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check", "test": "npm run test:schemas && npm run test:refs && npm run test:install && npm run test:copilot && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
"test:copilot": "node test/test-github-copilot-installer.js",
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas", "test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
"test:install": "node test/test-installation-components.js", "test:install": "node test/test-installation-components.js",
"test:refs": "node test/test-file-refs-csv.js", "test:refs": "node test/test-file-refs-csv.js",

View File

@ -7,3 +7,4 @@ core,anytime,Shard Document,SD,,_bmad/core/tasks/shard-doc.xml,bmad-shard-doc,fa
core,anytime,Editorial Review - Prose,EP,,_bmad/core/tasks/editorial-review-prose.xml,bmad-editorial-review-prose,false,,,"Review prose for clarity, tone, and communication issues. Use after drafting to polish written content.",report located with target document,"three-column markdown table with suggested fixes", core,anytime,Editorial Review - Prose,EP,,_bmad/core/tasks/editorial-review-prose.xml,bmad-editorial-review-prose,false,,,"Review prose for clarity, tone, and communication issues. Use after drafting to polish written content.",report located with target document,"three-column markdown table with suggested fixes",
core,anytime,Editorial Review - Structure,ES,,_bmad/core/tasks/editorial-review-structure.xml,bmad-editorial-review-structure,false,,,"Propose cuts, reorganization, and simplification while preserving comprehension. Use when doc produced from multiple subprocesses or needs structural improvement.",report located with target document, core,anytime,Editorial Review - Structure,ES,,_bmad/core/tasks/editorial-review-structure.xml,bmad-editorial-review-structure,false,,,"Propose cuts, reorganization, and simplification while preserving comprehension. Use when doc produced from multiple subprocesses or needs structural improvement.",report located with target document,
core,anytime,Adversarial Review (General),AR,,_bmad/core/tasks/review-adversarial-general.xml,bmad-review-adversarial-general,false,,,"Review content critically to find issues and weaknesses. Use for quality assurance or before finalizing deliverables. Code Review in other modules run this automatically, but its useful also for document reviews",, core,anytime,Adversarial Review (General),AR,,_bmad/core/tasks/review-adversarial-general.xml,bmad-review-adversarial-general,false,,,"Review content critically to find issues and weaknesses. Use for quality assurance or before finalizing deliverables. Code Review in other modules run this automatically, but its useful also for document reviews",,
core,anytime,Edge Case Hunter Review,ECH,,_bmad/core/tasks/review-edge-case-hunter.xml,bmad-review-edge-case-hunter,false,,,"Walk every branching path and boundary condition in code, report only unhandled edge cases. Use alongside adversarial review for orthogonal coverage - method-driven not attitude-driven.",,

Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,64 @@
<!-- if possible, run this in a separate subagent or process with read access to the project,
but no context except the content to review -->
<task id="_bmad/core/tasks/review-edge-case-hunter.xml" name="Edge Case Hunter Review"
description="Walk every branching path and boundary condition in content, report only unhandled edge cases. Orthogonal to adversarial review - method-driven not attitude-driven.">
<objective>You are a pure path tracer. Never comment on whether code is good or bad; only list missing handling.
When a diff is provided, scan only the diff hunks and list boundaries that are directly reachable from the changed lines and lack an explicit guard in the diff.
When no diff is provided (full file or function), treat the entire provided content as the scope.
Ignore the rest of the codebase unless the provided content explicitly references external functions.</objective>
<inputs>
<input name="content" desc="Content to review - diff, full file, or function" />
<input name="also_consider" required="false"
desc="Optional areas to keep in mind during review alongside normal edge-case analysis" />
</inputs>
<output-format>Return ONLY a valid JSON array of objects. Each object must contain exactly these four fields and nothing else:
{
"location": "file:line",
"trigger_condition": "one-line description (max 15 words)",
"guard_snippet": "minimal code sketch that closes the gap",
"potential_consequence": "what could actually go wrong (max 15 words)"
}
No extra text, no explanations, no markdown wrapping.</output-format>
<llm critical="true">
<i>MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER</i>
<i>DO NOT skip steps or change the sequence</i>
<i>HALT immediately when halt-conditions are met</i>
<i>Each action xml tag within step xml tag is a REQUIRED action to complete that step</i>
<i>Your method is exhaustive path enumeration — mechanically walk every branch, not hunt by intuition</i>
<i>Trace each branching path: conditionals, switches, early returns, guard clauses, loops, error handlers</i>
<i>Trace each boundary condition: null, undefined, empty, zero, negative, overflow, max-length, type coercion, concurrency, timing</i>
<i>Report ONLY paths and conditions that lack handling — discard handled ones silently</i>
<i>Do NOT editorialize or add filler — findings only</i>
</llm>
<flow>
<step n="1" title="Receive Content">
<action>Load the content to review from provided input or context</action>
<action>If content to review is empty, ask for clarification and abort task</action>
<action>Identify content type (diff, full file, or function) to determine scope rules</action>
</step>
<step n="2" title="Exhaustive Path Analysis" critical="true">
<mandate>Walk every branching path and boundary condition within scope - report only unhandled ones</mandate>
<action>If also_consider input was provided, incorporate those areas into the analysis</action>
<action>Enumerate all branching paths and boundary conditions within scope: conditionals, switches, early returns, guard clauses, loops, error handlers, null/empty states, overflow, type edges, concurrency, timing</action>
<action>For each path: determine whether the content handles it</action>
<action>Collect only the unhandled paths as findings - discard handled ones silently</action>
</step>
<step n="3" title="Present Findings">
<action>Output findings as a JSON array following the output-format specification exactly</action>
</step>
</flow>
<halt-conditions>
<condition>HALT if zero findings - this is suspicious, re-analyze or ask for guidance</condition>
<condition>HALT if content is empty or unreadable</condition>
</halt-conditions>
</task>

View File

@ -0,0 +1,238 @@
/**
* GitHub Copilot Installer Tests
*
* Tests for the GitHubCopilotSetup class methods:
* - loadModuleConfig: module-aware config loading
* - createTechWriterPromptContent: BMM-only tech-writer handling
* - generateCopilotInstructions: selectedModules deduplication
*
* Usage: node test/test-github-copilot-installer.js
*/
const path = require('node:path');
const fs = require('fs-extra');
const { GitHubCopilotSetup } = require('../tools/cli/installers/lib/ide/github-copilot');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
yellow: '\u001B[33m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
let passed = 0;
let failed = 0;
/**
* Test helper: Assert condition
*/
function assert(condition, testName, errorMessage = '') {
if (condition) {
console.log(`${colors.green}${colors.reset} ${testName}`);
passed++;
} else {
console.log(`${colors.red}${colors.reset} ${testName}`);
if (errorMessage) {
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
}
failed++;
}
}
/**
* Test Suite
*/
async function runTests() {
console.log(`${colors.cyan}========================================`);
console.log('GitHub Copilot Installer Tests');
console.log(`========================================${colors.reset}\n`);
const tempDir = path.join(__dirname, 'temp-copilot-test');
try {
// Clean up any leftover temp directory
await fs.remove(tempDir);
await fs.ensureDir(tempDir);
const installer = new GitHubCopilotSetup();
// ============================================================
// Test Suite 1: loadModuleConfig
// ============================================================
console.log(`${colors.yellow}Test Suite 1: loadModuleConfig${colors.reset}\n`);
// Create mock bmad directory structure with multiple modules
const bmadDir = path.join(tempDir, '_bmad');
await fs.ensureDir(path.join(bmadDir, 'core'));
await fs.ensureDir(path.join(bmadDir, 'bmm'));
await fs.ensureDir(path.join(bmadDir, 'custom-module'));
// Create config files for each module
await fs.writeFile(path.join(bmadDir, 'core', 'config.yaml'), 'project_name: Core Project\nuser_name: CoreUser\n');
await fs.writeFile(path.join(bmadDir, 'bmm', 'config.yaml'), 'project_name: BMM Project\nuser_name: BmmUser\n');
await fs.writeFile(path.join(bmadDir, 'custom-module', 'config.yaml'), 'project_name: Custom Project\nuser_name: CustomUser\n');
// Test 1a: Load config with only core module (default)
const coreConfig = await installer.loadModuleConfig(bmadDir, ['core']);
assert(
coreConfig.project_name === 'Core Project',
'loadModuleConfig loads core config when only core installed',
`Got: ${coreConfig.project_name}`,
);
// Test 1b: Load config with bmm module (should prefer bmm over core)
const bmmConfig = await installer.loadModuleConfig(bmadDir, ['bmm', 'core']);
assert(bmmConfig.project_name === 'BMM Project', 'loadModuleConfig prefers bmm config over core', `Got: ${bmmConfig.project_name}`);
// Test 1c: Load config with custom module (should prefer custom over core)
const customConfig = await installer.loadModuleConfig(bmadDir, ['custom-module', 'core']);
assert(
customConfig.project_name === 'Custom Project',
'loadModuleConfig prefers custom module config over core',
`Got: ${customConfig.project_name}`,
);
// Test 1d: Load config with multiple non-core modules (first wins)
const multiConfig = await installer.loadModuleConfig(bmadDir, ['bmm', 'custom-module', 'core']);
assert(
multiConfig.project_name === 'BMM Project',
'loadModuleConfig uses first non-core module config',
`Got: ${multiConfig.project_name}`,
);
// Test 1e: Empty modules list uses default (core)
const defaultConfig = await installer.loadModuleConfig(bmadDir);
assert(
defaultConfig.project_name === 'Core Project',
'loadModuleConfig defaults to core when no modules specified',
`Got: ${defaultConfig.project_name}`,
);
// Test 1f: Non-existent module falls back to core
const fallbackConfig = await installer.loadModuleConfig(bmadDir, ['nonexistent', 'core']);
assert(
fallbackConfig.project_name === 'Core Project',
'loadModuleConfig falls back to core for non-existent modules',
`Got: ${fallbackConfig.project_name}`,
);
console.log('');
// ============================================================
// Test Suite 2: createTechWriterPromptContent (BMM-only)
// ============================================================
console.log(`${colors.yellow}Test Suite 2: createTechWriterPromptContent (BMM-only)${colors.reset}\n`);
// Test 2a: BMM tech-writer entry should generate content
const bmmTechWriterEntry = {
'agent-name': 'tech-writer',
module: 'bmm',
name: 'Write Document',
};
const bmmResult = installer.createTechWriterPromptContent(bmmTechWriterEntry);
assert(
bmmResult !== null && bmmResult.fileName === 'bmad-bmm-write-document',
'createTechWriterPromptContent generates content for BMM tech-writer',
`Got: ${bmmResult ? bmmResult.fileName : 'null'}`,
);
// Test 2b: Non-BMM tech-writer entry should return null
const customTechWriterEntry = {
'agent-name': 'tech-writer',
module: 'custom-module',
name: 'Write Document',
};
const customResult = installer.createTechWriterPromptContent(customTechWriterEntry);
assert(customResult === null, 'createTechWriterPromptContent returns null for non-BMM tech-writer', `Got: ${customResult}`);
// Test 2c: Core tech-writer entry should return null
const coreTechWriterEntry = {
'agent-name': 'tech-writer',
module: 'core',
name: 'Write Document',
};
const coreResult = installer.createTechWriterPromptContent(coreTechWriterEntry);
assert(coreResult === null, 'createTechWriterPromptContent returns null for core tech-writer', `Got: ${coreResult}`);
// Test 2d: Non-tech-writer BMM entry should return null
const nonTechWriterEntry = {
'agent-name': 'pm',
module: 'bmm',
name: 'Write Document',
};
const nonTechResult = installer.createTechWriterPromptContent(nonTechWriterEntry);
assert(nonTechResult === null, 'createTechWriterPromptContent returns null for non-tech-writer agents', `Got: ${nonTechResult}`);
// Test 2e: Unknown tech-writer command should return null
const unknownCmdEntry = {
'agent-name': 'tech-writer',
module: 'bmm',
name: 'Unknown Command',
};
const unknownResult = installer.createTechWriterPromptContent(unknownCmdEntry);
assert(unknownResult === null, 'createTechWriterPromptContent returns null for unknown commands', `Got: ${unknownResult}`);
console.log('');
// ============================================================
// Test Suite 3: selectedModules deduplication
// ============================================================
console.log(`${colors.yellow}Test Suite 3: selectedModules deduplication${colors.reset}\n`);
// We can't easily test generateCopilotInstructions directly without mocking,
// but we can verify the deduplication logic pattern
const testDedupe = (modules) => {
const installedModules = modules.length > 0 ? [...new Set(modules)] : ['core'];
return installedModules;
};
// Test 3a: Duplicate modules should be deduplicated
const dupeResult = testDedupe(['bmm', 'core', 'bmm', 'custom', 'core', 'custom']);
assert(
dupeResult.length === 3 && dupeResult.includes('bmm') && dupeResult.includes('core') && dupeResult.includes('custom'),
'Deduplication removes duplicate modules',
`Got: ${JSON.stringify(dupeResult)}`,
);
// Test 3b: Empty array defaults to core
const emptyResult = testDedupe([]);
assert(
emptyResult.length === 1 && emptyResult[0] === 'core',
'Empty modules array defaults to core',
`Got: ${JSON.stringify(emptyResult)}`,
);
// Test 3c: Order is preserved after deduplication (first occurrence wins)
const orderResult = testDedupe(['custom', 'bmm', 'custom', 'bmm']);
assert(
orderResult[0] === 'custom' && orderResult[1] === 'bmm',
'Deduplication preserves order (first occurrence)',
`Got: ${JSON.stringify(orderResult)}`,
);
} finally {
// Cleanup
await fs.remove(tempDir);
}
// Print summary
console.log(`${colors.cyan}========================================`);
console.log('Test Results:');
console.log(` Passed: ${passed}`);
console.log(` Failed: ${failed}`);
console.log(`========================================${colors.reset}\n`);
if (failed > 0) {
console.log(`${colors.red}Some tests failed!${colors.reset}`);
process.exit(1);
} else {
console.log(`${colors.green}✨ All GitHub Copilot installer tests passed!${colors.reset}`);
}
}
runTests().catch((error) => {
console.error(`${colors.red}Test runner error:${colors.reset}`, error);
process.exit(1);
});

View File

@ -247,9 +247,9 @@ You must fully embody this agent's persona and follow all activation instruction
*/ */
createWorkflowPromptContent(entry, workflowFile, toolsStr) { createWorkflowPromptContent(entry, workflowFile, toolsStr) {
const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name)); const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
// bmm/config.yaml is safe to hardcode here: these prompts are only generated when // Use the module from the bmad-help.csv entry to reference the correct config.yaml
// bmad-help.csv exists (bmm module data), so bmm is guaranteed to be installed. const configModule = entry.module || 'core';
const configLine = `1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables`; const configLine = `1. Load {project-root}/${this.bmadFolderName}/${configModule}/config.yaml and store ALL fields as session variables`;
let body; let body;
if (workflowFile.endsWith('.yaml')) { if (workflowFile.endsWith('.yaml')) {
@ -324,11 +324,13 @@ ${body}
/** /**
* Create prompt content for tech-writer agent-only commands (Pattern C) * Create prompt content for tech-writer agent-only commands (Pattern C)
* Tech-writer is BMM-specific - these commands only work with the BMM module.
* @param {Object} entry - bmad-help.csv row * @param {Object} entry - bmad-help.csv row
* @returns {Object|null} { fileName, content } or null if not a tech-writer command * @returns {Object|null} { fileName, content } or null if not a tech-writer command
*/ */
createTechWriterPromptContent(entry) { createTechWriterPromptContent(entry) {
if (entry['agent-name'] !== 'tech-writer') return null; // Tech-writer is BMM-specific - only process entries from the bmm module
if (entry['agent-name'] !== 'tech-writer' || entry.module !== 'bmm') return null;
const techWriterCommands = { const techWriterCommands = {
'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' }, 'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' },
@ -344,14 +346,16 @@ ${body}
const safeDescription = this.escapeYamlSingleQuote(cmd.description); const safeDescription = this.escapeYamlSingleQuote(cmd.description);
const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`); const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`);
// Use the module from the bmad-help.csv entry to reference the correct paths
const configModule = entry.module || 'core';
const content = `--- const content = `---
description: '${safeDescription}' description: '${safeDescription}'
agent: 'agent' agent: 'agent'
tools: ${toolsStr} tools: ${toolsStr}
--- ---
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables 1. Load {project-root}/${this.bmadFolderName}/${configModule}/config.yaml and store ALL fields as session variables
2. Load the full agent file from {project-root}/${this.bmadFolderName}/bmm/agents/tech-writer/tech-writer.md and activate the Paige (Technical Writer) persona 2. Load the full agent file from {project-root}/${this.bmadFolderName}/${configModule}/agents/tech-writer/tech-writer.md and activate the Paige (Technical Writer) persona
3. Execute the ${entry.name} menu command (${cmd.code}) 3. Execute the ${entry.name} menu command (${cmd.code})
`; `;
@ -376,15 +380,15 @@ tools: ${toolsStr}
const agentPath = artifact.agentPath || artifact.relativePath; const agentPath = artifact.agentPath || artifact.relativePath;
const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`; const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
// bmm/config.yaml is safe to hardcode: agent activators are only generated from // Use the agent's module to reference the correct config.yaml
// bmm agent artifacts, so bmm is guaranteed to be installed. const configModule = artifact.module || 'core';
return `--- return `---
description: '${safeDescription}' description: '${safeDescription}'
agent: 'agent' agent: 'agent'
tools: ${toolsStr} tools: ${toolsStr}
--- ---
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables 1. Load {project-root}/${this.bmadFolderName}/${configModule}/config.yaml and store ALL fields as session variables
2. Load the full agent file from ${agentFilePath} 2. Load the full agent file from ${agentFilePath}
3. Follow ALL activation instructions in the agent file 3. Follow ALL activation instructions in the agent file
4. Display the welcome/greeting as instructed 4. Display the welcome/greeting as instructed
@ -400,7 +404,13 @@ tools: ${toolsStr}
* @param {Map} agentManifest - Agent manifest data * @param {Map} agentManifest - Agent manifest data
*/ */
async generateCopilotInstructions(projectDir, bmadDir, agentManifest, options = {}) { async generateCopilotInstructions(projectDir, bmadDir, agentManifest, options = {}) {
const configVars = await this.loadModuleConfig(bmadDir); // Determine installed modules (excluding internal directories)
const selectedModules = options.selectedModules || [];
// Deduplicate selectedModules to prevent duplicate paths in generated markdown
const installedModules = selectedModules.length > 0 ? [...new Set(selectedModules)] : ['core'];
const configVars = await this.loadModuleConfig(bmadDir, installedModules);
// Filter to only non-core modules for display (core is always listed separately)
const nonCoreModules = installedModules.filter((m) => m !== 'core');
// Build the agents table from the manifest // Build the agents table from the manifest
let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n'; let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n';
@ -427,6 +437,36 @@ tools: ${toolsStr}
} }
const bmad = this.bmadFolderName; const bmad = this.bmadFolderName;
// Build dynamic module paths based on installed modules
const moduleAgentPaths = nonCoreModules.map((m) => `\`${bmad}/${m}/agents/\``).join(', ');
const moduleWorkflowPaths = nonCoreModules.map((m) => `\`${bmad}/${m}/workflows/\``).join(', ');
const moduleConfigPaths = nonCoreModules.map((m) => `\`${bmad}/${m}/config.yaml\``).join(', ');
// Build agent definitions line
let agentDefsLine;
if (nonCoreModules.length > 0) {
agentDefsLine = `- **Agent definitions**: ${moduleAgentPaths} and \`${bmad}/core/agents/\` (core)`;
} else {
agentDefsLine = `- **Agent definitions**: \`${bmad}/core/agents/\``;
}
// Build workflow definitions line
let workflowDefsLine;
if (nonCoreModules.length > 0) {
workflowDefsLine = `- **Workflow definitions**: ${moduleWorkflowPaths} (organized by phase)`;
} else {
workflowDefsLine = `- **Workflow definitions**: \`${bmad}/core/workflows/\``;
}
// Build module configuration line
let moduleConfigLine;
if (nonCoreModules.length > 0) {
moduleConfigLine = `- **Module configuration**: ${moduleConfigPaths}`;
} else {
moduleConfigLine = `- **Module configuration**: (no non-core modules installed)`;
}
const bmadSection = `# BMAD Method — Project Instructions const bmadSection = `# BMAD Method — Project Instructions
## Project Configuration ## Project Configuration
@ -443,12 +483,12 @@ tools: ${toolsStr}
## BMAD Runtime Structure ## BMAD Runtime Structure
- **Agent definitions**: \`${bmad}/bmm/agents/\` (BMM module) and \`${bmad}/core/agents/\` (core) ${agentDefsLine}
- **Workflow definitions**: \`${bmad}/bmm/workflows/\` (organized by phase) ${workflowDefsLine}
- **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review) - **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review)
- **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation) - **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation)
- **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows) - **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows)
- **Module configuration**: \`${bmad}/bmm/config.yaml\` ${moduleConfigLine}
- **Core configuration**: \`${bmad}/core/config.yaml\` - **Core configuration**: \`${bmad}/core/config.yaml\`
- **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\` - **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\`
- **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\` - **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\`
@ -457,7 +497,7 @@ tools: ${toolsStr}
## Key Conventions ## Key Conventions
- Always load \`${bmad}/bmm/config.yaml\` before any agent activation or workflow execution - Always load the agent/workflow's module \`config.yaml\` before activation or execution (each prompt file specifies which config to load)
- Store all config fields as session variables: \`{user_name}\`, \`{communication_language}\`, \`{output_folder}\`, \`{planning_artifacts}\`, \`{implementation_artifacts}\`, \`{project_knowledge}\` - Store all config fields as session variables: \`{user_name}\`, \`{communication_language}\`, \`{output_folder}\`, \`{planning_artifacts}\`, \`{implementation_artifacts}\`, \`{project_knowledge}\`
- MD-based workflows execute directly load and follow the \`.md\` file - MD-based workflows execute directly load and follow the \`.md\` file
- YAML-based workflows require the workflow engine load \`workflow.xml\` first, then pass the \`.yaml\` config - YAML-based workflows require the workflow engine load \`workflow.xml\` first, then pass the \`.yaml\` config
@ -504,13 +544,15 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
/** /**
* Load module config.yaml for template variables * Load module config.yaml for template variables
* @param {string} bmadDir - BMAD installation directory * @param {string} bmadDir - BMAD installation directory
* @param {string[]} installedModules - List of installed modules to check for config
* @returns {Object} Config variables * @returns {Object} Config variables
*/ */
async loadModuleConfig(bmadDir) { async loadModuleConfig(bmadDir, installedModules = ['core']) {
const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml'); // Build config paths from installed modules (non-core first, then core as fallback)
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml'); const nonCoreModules = installedModules.filter((m) => m !== 'core');
const configPaths = [...nonCoreModules.map((m) => path.join(bmadDir, m, 'config.yaml')), path.join(bmadDir, 'core', 'config.yaml')];
for (const configPath of [bmmConfigPath, coreConfigPath]) { for (const configPath of configPaths) {
if (await fs.pathExists(configPath)) { if (await fs.pathExists(configPath)) {
try { try {
const content = await fs.readFile(configPath, 'utf8'); const content = await fs.readFile(configPath, 'utf8');