diff --git a/test/test-installation-components.js b/test/test-installation-components.js
index 1b09178ed..34ffa8baf 100644
--- a/test/test-installation-components.js
+++ b/test/test-installation-components.js
@@ -846,6 +846,13 @@ async function runTests() {
await fs.writeFile(path.join(legacyAgentsDir17, 'bmad-legacy.agent.md'), 'legacy agent\n');
await fs.writeFile(path.join(legacyPromptsDir17, 'bmad-legacy.prompt.md'), 'legacy prompt\n');
+ // Create legacy copilot-instructions.md with BMAD markers
+ const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
+ await fs.writeFile(
+ copilotInstructionsPath17,
+ 'User content before\n\nBMAD generated content\n\nUser content after\n',
+ );
+
const ideManager17 = new IdeManager();
await ideManager17.ensureInitialized();
const result17 = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, {
@@ -867,6 +874,17 @@ async function runTests() {
assert(!(await fs.pathExists(legacyPromptsDir17)), 'GitHub Copilot setup removes legacy prompts dir');
+ // Verify copilot-instructions.md BMAD markers were stripped but user content preserved
+ const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8');
+ assert(
+ !cleanedInstructions17.includes('BMAD:START') && !cleanedInstructions17.includes('BMAD generated content'),
+ 'GitHub Copilot setup strips BMAD markers from copilot-instructions.md',
+ );
+ assert(
+ cleanedInstructions17.includes('User content before') && cleanedInstructions17.includes('User content after'),
+ 'GitHub Copilot setup preserves user content in copilot-instructions.md',
+ );
+
await fs.remove(tempProjectDir17);
await fs.remove(installedBmadDir17);
} catch (error) {
diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js
index d23d8d6d0..030b85e19 100644
--- a/tools/cli/installers/lib/ide/_config-driven.js
+++ b/tools/cli/installers/lib/ide/_config-driven.js
@@ -655,6 +655,11 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
}
}
+ // Strip BMAD markers from copilot-instructions.md if present
+ if (this.name === 'github-copilot') {
+ await this.cleanupCopilotInstructions(projectDir, options);
+ }
+
// Clean all target directories
if (this.installerConfig?.targets) {
const parentDirs = new Set();
@@ -768,6 +773,40 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
}
}
}
+ /**
+ * Strip BMAD-owned content from .github/copilot-instructions.md.
+ * The old custom installer injected content between and markers.
+ * Deletes the file if nothing remains. Restores .bak backup if one exists.
+ */
+ async cleanupCopilotInstructions(projectDir, options = {}) {
+ const filePath = path.join(projectDir, '.github', 'copilot-instructions.md');
+
+ if (!(await fs.pathExists(filePath))) return;
+
+ const content = await fs.readFile(filePath, 'utf8');
+ const startIdx = content.indexOf('');
+ const endIdx = content.indexOf('');
+
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return;
+
+ const cleaned = content.slice(0, startIdx) + content.slice(endIdx + ''.length);
+
+ if (cleaned.trim().length === 0) {
+ await fs.remove(filePath);
+ const backupPath = `${filePath}.bak`;
+ if (await fs.pathExists(backupPath)) {
+ await fs.rename(backupPath, filePath);
+ if (!options.silent) await prompts.log.message(' Restored copilot-instructions.md from backup');
+ }
+ } else {
+ await fs.writeFile(filePath, cleaned, 'utf8');
+ const backupPath = `${filePath}.bak`;
+ if (await fs.pathExists(backupPath)) await fs.remove(backupPath);
+ }
+
+ if (!options.silent) await prompts.log.message(' Cleaned BMAD markers from copilot-instructions.md');
+ }
+
/**
* Check ancestor directories for existing BMAD files in the same target_dir.
* IDEs like Claude Code inherit commands from parent directories, so an existing
diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js
deleted file mode 100644
index 059127f81..000000000
--- a/tools/cli/installers/lib/ide/github-copilot.js
+++ /dev/null
@@ -1,699 +0,0 @@
-const path = require('node:path');
-const { BaseIdeSetup } = require('./_base-ide');
-const prompts = require('../../../lib/prompts');
-const { AgentCommandGenerator } = require('./shared/agent-command-generator');
-const { BMAD_FOLDER_NAME, toDashPath } = require('./shared/path-utils');
-const fs = require('fs-extra');
-const csv = require('csv-parse/sync');
-const yaml = require('yaml');
-
-/**
- * GitHub Copilot setup handler
- * Creates agents in .github/agents/, prompts in .github/prompts/,
- * copilot-instructions.md, and configures VS Code settings
- */
-class GitHubCopilotSetup extends BaseIdeSetup {
- constructor() {
- super('github-copilot', 'GitHub Copilot', false);
- // Don't set configDir to '.github' — nearly every GitHub repo has that directory,
- // which would cause the base detect() to false-positive. Use detectionPaths instead.
- this.configDir = null;
- this.githubDir = '.github';
- this.agentsDir = 'agents';
- this.promptsDir = 'prompts';
- this.detectionPaths = ['.github/copilot-instructions.md', '.github/agents'];
- }
-
- /**
- * Setup GitHub Copilot configuration
- * @param {string} projectDir - Project directory
- * @param {string} bmadDir - BMAD installation directory
- * @param {Object} options - Setup options
- */
- async setup(projectDir, bmadDir, options = {}) {
- if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
-
- // Create .github/agents and .github/prompts directories
- const githubDir = path.join(projectDir, this.githubDir);
- const agentsDir = path.join(githubDir, this.agentsDir);
- const promptsDir = path.join(githubDir, this.promptsDir);
- await this.ensureDir(agentsDir);
- await this.ensureDir(promptsDir);
-
- // Preserve any customised tool permissions from existing files before cleanup
- this.existingToolPermissions = await this.collectExistingToolPermissions(projectDir);
-
- // Clean up any existing BMAD files before reinstalling
- await this.cleanup(projectDir);
-
- // Load agent manifest for enriched descriptions
- const agentManifest = await this.loadAgentManifest(bmadDir);
-
- // Generate agent launchers
- const agentGen = new AgentCommandGenerator(this.bmadFolderName);
- const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
-
- // Create agent .agent.md files
- let agentCount = 0;
- for (const artifact of agentArtifacts) {
- const agentMeta = agentManifest.get(artifact.name);
-
- // Compute fileName first so we can look up any existing tool permissions
- const dashName = toDashPath(artifact.relativePath);
- const fileName = dashName.replace(/\.md$/, '.agent.md');
- const toolsStr = this.getToolsForFile(fileName);
- const agentContent = this.createAgentContent(artifact, agentMeta, toolsStr);
- const targetPath = path.join(agentsDir, fileName);
- await this.writeFile(targetPath, agentContent);
- agentCount++;
- }
-
- // Generate prompt files from bmad-help.csv
- const promptCount = await this.generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest);
-
- // Generate copilot-instructions.md
- await this.generateCopilotInstructions(projectDir, bmadDir, agentManifest, options);
-
- if (!options.silent) await prompts.log.success(`${this.name} configured: ${agentCount} agents, ${promptCount} prompts → .github/`);
-
- return {
- success: true,
- results: {
- agents: agentCount,
- workflows: promptCount,
- tasks: 0,
- tools: 0,
- },
- };
- }
-
- /**
- * Load agent manifest CSV into a Map keyed by agent name
- * @param {string} bmadDir - BMAD installation directory
- * @returns {Map} Agent metadata keyed by name
- */
- async loadAgentManifest(bmadDir) {
- const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
- const agents = new Map();
-
- if (!(await fs.pathExists(manifestPath))) {
- return agents;
- }
-
- try {
- const csvContent = await fs.readFile(manifestPath, 'utf8');
- const records = csv.parse(csvContent, {
- columns: true,
- skip_empty_lines: true,
- });
-
- for (const record of records) {
- agents.set(record.name, record);
- }
- } catch {
- // Gracefully degrade if manifest is unreadable/malformed
- }
-
- return agents;
- }
-
- /**
- * Load bmad-help.csv to drive prompt generation
- * @param {string} bmadDir - BMAD installation directory
- * @returns {Array|null} Parsed CSV rows
- */
- async loadBmadHelp(bmadDir) {
- const helpPath = path.join(bmadDir, '_config', 'bmad-help.csv');
-
- if (!(await fs.pathExists(helpPath))) {
- return null;
- }
-
- try {
- const csvContent = await fs.readFile(helpPath, 'utf8');
- return csv.parse(csvContent, {
- columns: true,
- skip_empty_lines: true,
- });
- } catch {
- // Gracefully degrade if help CSV is unreadable/malformed
- return null;
- }
- }
-
- /**
- * Create agent .agent.md content with enriched description
- * @param {Object} artifact - Agent artifact from AgentCommandGenerator
- * @param {Object|undefined} manifestEntry - Agent manifest entry with metadata
- * @returns {string} Agent file content
- */
- createAgentContent(artifact, manifestEntry, toolsStr) {
- // Build enriched description from manifest metadata
- let description;
- if (manifestEntry) {
- const persona = manifestEntry.displayName || artifact.name;
- const title = manifestEntry.title || this.formatTitle(artifact.name);
- const capabilities = manifestEntry.capabilities || 'agent capabilities';
- description = `${persona} — ${title}: ${capabilities}`;
- } else {
- description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`;
- }
-
- // Build the agent file path for the activation block
- const agentPath = artifact.agentPath || artifact.relativePath;
- const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
-
- return `---
-description: '${description.replaceAll("'", "''")}'
-tools: ${toolsStr}
----
-
-You must fully embody this agent's persona and follow all activation instructions exactly as specified.
-
-
-1. LOAD the FULL agent file from ${agentFilePath}
-2. READ its entire contents - this contains the complete agent persona, menu, and instructions
-3. FOLLOW every step in the section precisely
-4. DISPLAY the welcome/greeting as instructed
-5. PRESENT the numbered menu
-6. WAIT for user input before proceeding
-
-`;
- }
-
- /**
- * Generate .prompt.md files for workflows, tasks, tech-writer commands, and agent activators
- * @param {string} projectDir - Project directory
- * @param {string} bmadDir - BMAD installation directory
- * @param {Array} agentArtifacts - Agent artifacts for activator generation
- * @param {Map} agentManifest - Agent manifest data
- * @returns {number} Count of prompts generated
- */
- async generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest) {
- const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
- let promptCount = 0;
-
- // Load bmad-help.csv to drive workflow/task prompt generation
- const helpEntries = await this.loadBmadHelp(bmadDir);
-
- if (helpEntries) {
- for (const entry of helpEntries) {
- const command = entry.command;
- if (!command) continue; // Skip entries without a command (tech-writer commands have no command column)
-
- const workflowFile = entry['workflow-file'];
- if (!workflowFile) continue; // Skip entries with no workflow file path
- const promptFileName = `${command}.prompt.md`;
- const toolsStr = this.getToolsForFile(promptFileName);
- const promptContent = this.createWorkflowPromptContent(entry, workflowFile, toolsStr);
- const promptPath = path.join(promptsDir, promptFileName);
- await this.writeFile(promptPath, promptContent);
- promptCount++;
- }
-
- // Generate tech-writer command prompts (entries with no command column)
- for (const entry of helpEntries) {
- if (entry.command) continue; // Already handled above
- const techWriterPrompt = this.createTechWriterPromptContent(entry);
- if (techWriterPrompt) {
- const promptFileName = `${techWriterPrompt.fileName}.prompt.md`;
- const promptPath = path.join(promptsDir, promptFileName);
- await this.writeFile(promptPath, techWriterPrompt.content);
- promptCount++;
- }
- }
- }
-
- // Generate agent activator prompts (Pattern D)
- for (const artifact of agentArtifacts) {
- const agentMeta = agentManifest.get(artifact.name);
- const fileName = `bmad-${artifact.name}.prompt.md`;
- const toolsStr = this.getToolsForFile(fileName);
- const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta, toolsStr);
- const promptPath = path.join(promptsDir, fileName);
- await this.writeFile(promptPath, promptContent);
- promptCount++;
- }
-
- return promptCount;
- }
-
- /**
- * Create prompt content for a workflow/task entry from bmad-help.csv
- * Determines the pattern (A, B, or A for .xml tasks) based on file extension
- * @param {Object} entry - bmad-help.csv row
- * @param {string} workflowFile - Workflow file path
- * @returns {string} Prompt file content
- */
- createWorkflowPromptContent(entry, workflowFile, toolsStr) {
- const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
- // bmm/config.yaml is safe to hardcode here: these prompts are only generated when
- // bmad-help.csv exists (bmm module data), so bmm is guaranteed to be installed.
- const configLine = `1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables`;
-
- let body;
- if (workflowFile.endsWith('.yaml')) {
- // Pattern B: YAML-based workflows — use workflow engine
- body = `${configLine}
-2. Load the workflow engine at {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml
-3. Load and execute the workflow configuration at {project-root}/${workflowFile} using the engine from step 2`;
- } else if (workflowFile.endsWith('.xml')) {
- // Pattern A variant: XML tasks — load and execute directly
- body = `${configLine}
-2. Load and execute the task at {project-root}/${workflowFile}`;
- } else {
- // Pattern A: MD workflows — load and follow directly
- body = `${configLine}
-2. Load and follow the workflow at {project-root}/${workflowFile}`;
- }
-
- return `---
-description: '${description}'
-agent: 'agent'
-tools: ${toolsStr}
----
-
-${body}
-`;
- }
-
- /**
- * Create a short 2-5 word description for a prompt from the entry name
- * @param {string} name - Entry name from bmad-help.csv
- * @returns {string} Short description
- */
- createPromptDescription(name) {
- const descriptionMap = {
- 'Brainstorm Project': 'Brainstorm ideas',
- 'Market Research': 'Market research',
- 'Domain Research': 'Domain research',
- 'Technical Research': 'Technical research',
- 'Create Brief': 'Create product brief',
- 'Create PRD': 'Create PRD',
- 'Validate PRD': 'Validate PRD',
- 'Edit PRD': 'Edit PRD',
- 'Create UX': 'Create UX design',
- 'Create Architecture': 'Create architecture',
- 'Create Epics and Stories': 'Create epics and stories',
- 'Check Implementation Readiness': 'Check implementation readiness',
- 'Sprint Planning': 'Sprint planning',
- 'Sprint Status': 'Sprint status',
- 'Create Story': 'Create story',
- 'Validate Story': 'Validate story',
- 'Dev Story': 'Dev story',
- 'QA Automation Test': 'QA automation',
- 'Code Review': 'Code review',
- Retrospective: 'Retrospective',
- 'Document Project': 'Document project',
- 'Generate Project Context': 'Generate project context',
- 'Quick Spec': 'Quick spec',
- 'Quick Dev': 'Quick dev',
- 'Correct Course': 'Correct course',
- Brainstorming: 'Brainstorm ideas',
- 'Party Mode': 'Party mode',
- 'bmad-help': 'BMAD help',
- 'Index Docs': 'Index documents',
- 'Shard Document': 'Shard document',
- 'Editorial Review - Prose': 'Editorial review prose',
- 'Editorial Review - Structure': 'Editorial review structure',
- 'Adversarial Review (General)': 'Adversarial review',
- };
-
- return descriptionMap[name] || name;
- }
-
- /**
- * Create prompt content for tech-writer agent-only commands (Pattern C)
- * @param {Object} entry - bmad-help.csv row
- * @returns {Object|null} { fileName, content } or null if not a tech-writer command
- */
- createTechWriterPromptContent(entry) {
- if (entry['agent-name'] !== 'tech-writer') return null;
-
- const techWriterCommands = {
- 'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' },
- 'Update Standards': { code: 'US', file: 'bmad-bmm-update-standards', description: 'Update standards' },
- 'Mermaid Generate': { code: 'MG', file: 'bmad-bmm-mermaid-generate', description: 'Mermaid generate' },
- 'Validate Document': { code: 'VD', file: 'bmad-bmm-validate-document', description: 'Validate document' },
- 'Explain Concept': { code: 'EC', file: 'bmad-bmm-explain-concept', description: 'Explain concept' },
- };
-
- const cmd = techWriterCommands[entry.name];
- if (!cmd) return null;
-
- const safeDescription = this.escapeYamlSingleQuote(cmd.description);
- const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`);
-
- const content = `---
-description: '${safeDescription}'
-agent: 'agent'
-tools: ${toolsStr}
----
-
-1. Load {project-root}/${this.bmadFolderName}/bmm/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
-3. Execute the ${entry.name} menu command (${cmd.code})
-`;
-
- return { fileName: cmd.file, content };
- }
-
- /**
- * Create agent activator prompt content (Pattern D)
- * @param {Object} artifact - Agent artifact
- * @param {Object|undefined} manifestEntry - Agent manifest entry
- * @returns {string} Prompt file content
- */
- createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) {
- let description;
- if (manifestEntry) {
- description = manifestEntry.title || this.formatTitle(artifact.name);
- } else {
- description = this.formatTitle(artifact.name);
- }
-
- const safeDescription = this.escapeYamlSingleQuote(description);
- const agentPath = artifact.agentPath || artifact.relativePath;
- const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
-
- // bmm/config.yaml is safe to hardcode: agent activators are only generated from
- // bmm agent artifacts, so bmm is guaranteed to be installed.
- return `---
-description: '${safeDescription}'
-agent: 'agent'
-tools: ${toolsStr}
----
-
-1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
-2. Load the full agent file from ${agentFilePath}
-3. Follow ALL activation instructions in the agent file
-4. Display the welcome/greeting as instructed
-5. Present the numbered menu
-6. Wait for user input before proceeding
-`;
- }
-
- /**
- * Generate copilot-instructions.md from module config
- * @param {string} projectDir - Project directory
- * @param {string} bmadDir - BMAD installation directory
- * @param {Map} agentManifest - Agent manifest data
- */
- async generateCopilotInstructions(projectDir, bmadDir, agentManifest, options = {}) {
- const configVars = await this.loadModuleConfig(bmadDir);
-
- // Build the agents table from the manifest
- let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n';
- const agentOrder = [
- 'bmad-master',
- 'analyst',
- 'architect',
- 'dev',
- 'pm',
- 'qa',
- 'quick-flow-solo-dev',
- 'sm',
- 'tech-writer',
- 'ux-designer',
- ];
-
- for (const agentName of agentOrder) {
- const meta = agentManifest.get(agentName);
- if (meta) {
- const capabilities = meta.capabilities || 'agent capabilities';
- const cleanTitle = (meta.title || '').replaceAll('""', '"');
- agentsTable += `| ${agentName} | ${meta.displayName} | ${cleanTitle} | ${capabilities} |\n`;
- }
- }
-
- const bmad = this.bmadFolderName;
- const bmadSection = `# BMAD Method — Project Instructions
-
-## Project Configuration
-
-- **Project**: ${configVars.project_name || '{{project_name}}'}
-- **User**: ${configVars.user_name || '{{user_name}}'}
-- **Communication Language**: ${configVars.communication_language || '{{communication_language}}'}
-- **Document Output Language**: ${configVars.document_output_language || '{{document_output_language}}'}
-- **User Skill Level**: ${configVars.user_skill_level || '{{user_skill_level}}'}
-- **Output Folder**: ${configVars.output_folder || '{{output_folder}}'}
-- **Planning Artifacts**: ${configVars.planning_artifacts || '{{planning_artifacts}}'}
-- **Implementation Artifacts**: ${configVars.implementation_artifacts || '{{implementation_artifacts}}'}
-- **Project Knowledge**: ${configVars.project_knowledge || '{{project_knowledge}}'}
-
-## BMAD Runtime Structure
-
-- **Agent definitions**: \`${bmad}/bmm/agents/\` (BMM module) and \`${bmad}/core/agents/\` (core)
-- **Workflow definitions**: \`${bmad}/bmm/workflows/\` (organized by phase)
-- **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review)
-- **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation)
-- **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows)
-- **Module configuration**: \`${bmad}/bmm/config.yaml\`
-- **Core configuration**: \`${bmad}/core/config.yaml\`
-- **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\`
-- **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\`
-- **Help manifest**: \`${bmad}/_config/bmad-help.csv\`
-- **Agent memory**: \`${bmad}/_memory/\`
-
-## Key Conventions
-
-- Always load \`${bmad}/bmm/config.yaml\` before any agent activation or workflow execution
-- 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
-- YAML-based workflows require the workflow engine — load \`workflow.xml\` first, then pass the \`.yaml\` config
-- Follow step-based workflow execution: load steps JIT, never multiple at once
-- Save outputs after EACH step when using the workflow engine
-- The \`{project-root}\` variable resolves to the workspace root at runtime
-
-## Available Agents
-
-${agentsTable}
-## Slash Commands
-
-Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent activators. Agents are also available in the agents dropdown.`;
-
- const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
- const markerStart = '';
- const markerEnd = '';
- const markedContent = `${markerStart}\n${bmadSection}\n${markerEnd}`;
-
- if (await fs.pathExists(instructionsPath)) {
- const existing = await fs.readFile(instructionsPath, 'utf8');
- const startIdx = existing.indexOf(markerStart);
- const endIdx = existing.indexOf(markerEnd);
-
- if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
- // Replace only the BMAD section between markers
- const before = existing.slice(0, startIdx);
- const after = existing.slice(endIdx + markerEnd.length);
- const merged = `${before}${markedContent}${after}`;
- await this.writeFile(instructionsPath, merged);
- } else {
- // Existing file without markers — back it up before overwriting
- const backupPath = `${instructionsPath}.bak`;
- await fs.copy(instructionsPath, backupPath);
- if (!options.silent) await prompts.log.warn(` Backed up copilot-instructions.md → .bak`);
- await this.writeFile(instructionsPath, `${markedContent}\n`);
- }
- } else {
- // No existing file — create fresh with markers
- await this.writeFile(instructionsPath, `${markedContent}\n`);
- }
- }
-
- /**
- * Load module config.yaml for template variables
- * @param {string} bmadDir - BMAD installation directory
- * @returns {Object} Config variables
- */
- async loadModuleConfig(bmadDir) {
- const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml');
- const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
-
- for (const configPath of [bmmConfigPath, coreConfigPath]) {
- if (await fs.pathExists(configPath)) {
- try {
- const content = await fs.readFile(configPath, 'utf8');
- return yaml.parse(content) || {};
- } catch {
- // Fall through to next config
- }
- }
- }
-
- return {};
- }
-
- /**
- * Escape a string for use inside YAML single-quoted values.
- * In YAML, the only escape inside single quotes is '' for a literal '.
- * @param {string} value - Raw string
- * @returns {string} Escaped string safe for YAML single-quoted context
- */
- escapeYamlSingleQuote(value) {
- return (value || '').replaceAll("'", "''");
- }
-
- /**
- * Scan existing agent and prompt files for customised tool permissions before cleanup.
- * Returns a Map so permissions can be preserved across reinstalls.
- * @param {string} projectDir - Project directory
- * @returns {Map} Existing tool permissions keyed by filename
- */
- async collectExistingToolPermissions(projectDir) {
- const permissions = new Map();
- const dirs = [
- [path.join(projectDir, this.githubDir, this.agentsDir), /^bmad.*\.agent\.md$/],
- [path.join(projectDir, this.githubDir, this.promptsDir), /^bmad-.*\.prompt\.md$/],
- ];
-
- for (const [dir, pattern] of dirs) {
- if (!(await fs.pathExists(dir))) continue;
- const files = await fs.readdir(dir);
-
- for (const file of files) {
- if (!pattern.test(file)) continue;
-
- try {
- const content = await fs.readFile(path.join(dir, file), 'utf8');
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
- if (!fmMatch) continue;
-
- const frontmatter = yaml.parse(fmMatch[1]);
- if (frontmatter && Array.isArray(frontmatter.tools)) {
- permissions.set(file, frontmatter.tools);
- }
- } catch {
- // Skip unreadable files
- }
- }
- }
-
- return permissions;
- }
-
- /**
- * Get the tools array string for a file, preserving any existing customisation.
- * Falls back to the default tools if no prior customisation exists.
- * @param {string} fileName - Target filename (e.g. 'bmad-agent-bmm-pm.agent.md')
- * @returns {string} YAML inline array string
- */
- getToolsForFile(fileName) {
- const defaultTools = ['read', 'edit', 'search', 'execute'];
- const tools = (this.existingToolPermissions && this.existingToolPermissions.get(fileName)) || defaultTools;
- return '[' + tools.map((t) => `'${t}'`).join(', ') + ']';
- }
-
- /**
- * Format name as title
- */
- formatTitle(name) {
- return name
- .split('-')
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ');
- }
-
- /**
- * Cleanup GitHub Copilot configuration - surgically remove only BMAD files
- */
- async cleanup(projectDir, options = {}) {
- // Clean up agents directory
- const agentsDir = path.join(projectDir, this.githubDir, this.agentsDir);
- if (await fs.pathExists(agentsDir)) {
- const files = await fs.readdir(agentsDir);
- let removed = 0;
-
- for (const file of files) {
- if (file.startsWith('bmad') && (file.endsWith('.agent.md') || file.endsWith('.md'))) {
- await fs.remove(path.join(agentsDir, file));
- removed++;
- }
- }
-
- if (removed > 0 && !options.silent) {
- await prompts.log.message(` Cleaned up ${removed} existing BMAD agents`);
- }
- }
-
- // Clean up prompts directory
- const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
- if (await fs.pathExists(promptsDir)) {
- const files = await fs.readdir(promptsDir);
- let removed = 0;
-
- for (const file of files) {
- if (file.startsWith('bmad-') && file.endsWith('.prompt.md')) {
- await fs.remove(path.join(promptsDir, file));
- removed++;
- }
- }
-
- if (removed > 0 && !options.silent) {
- await prompts.log.message(` Cleaned up ${removed} existing BMAD prompts`);
- }
- }
-
- // During uninstall, also strip BMAD markers from copilot-instructions.md.
- // During reinstall (default), this is skipped because generateCopilotInstructions()
- // handles marker-based replacement in a single read-modify-write pass,
- // which correctly preserves user content outside the markers.
- if (options.isUninstall) {
- await this.cleanupCopilotInstructions(projectDir, options);
- }
- }
-
- /**
- * Strip BMAD marker section from copilot-instructions.md
- * If file becomes empty after stripping, delete it.
- * If a .bak backup exists and the main file was deleted, restore the backup.
- * @param {string} projectDir - Project directory
- * @param {Object} [options] - Options (e.g. { silent: true })
- */
- async cleanupCopilotInstructions(projectDir, options = {}) {
- const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
- const backupPath = `${instructionsPath}.bak`;
-
- if (!(await fs.pathExists(instructionsPath))) {
- return;
- }
-
- const content = await fs.readFile(instructionsPath, 'utf8');
- const markerStart = '';
- const markerEnd = '';
- const startIdx = content.indexOf(markerStart);
- const endIdx = content.indexOf(markerEnd);
-
- if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
- return; // No valid markers found
- }
-
- // Strip the marker section (including markers)
- const before = content.slice(0, startIdx);
- const after = content.slice(endIdx + markerEnd.length);
- const cleaned = before + after;
-
- if (cleaned.trim().length === 0) {
- // File is empty after stripping — delete it
- await fs.remove(instructionsPath);
-
- // If backup exists, restore it
- if (await fs.pathExists(backupPath)) {
- await fs.rename(backupPath, instructionsPath);
- if (!options.silent) {
- await prompts.log.message(' Restored copilot-instructions.md from backup');
- }
- }
- } else {
- // Write cleaned content back (preserve original whitespace)
- await fs.writeFile(instructionsPath, cleaned, 'utf8');
-
- // If backup exists, it's stale now — remove it
- if (await fs.pathExists(backupPath)) {
- await fs.remove(backupPath);
- }
- }
- }
-}
-
-module.exports = { GitHubCopilotSetup };
diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js
index 654574a25..0ed8f8006 100644
--- a/tools/cli/installers/lib/ide/manager.js
+++ b/tools/cli/installers/lib/ide/manager.js
@@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers
*
* Loading strategy:
- * 1. Custom installer files (github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic
+ * 1. Custom installer files (kilo.js, rovodev.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/
class IdeManager {
@@ -44,7 +44,7 @@ class IdeManager {
/**
* Dynamically load all IDE handlers
- * 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js)
+ * 1. Load custom installer files first (kilo.js, rovodev.js)
* 2. Load config-driven handlers from platform-codes.yaml
*/
async loadHandlers() {
@@ -58,11 +58,11 @@ class IdeManager {
/**
* Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model
- * Note: codex was migrated to config-driven (platform-codes.yaml) and no longer needs a custom installer
+ * Note: codex and github-copilot were migrated to config-driven (platform-codes.yaml)
*/
async loadCustomInstallerFiles() {
const ideDir = __dirname;
- const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js'];
+ const customFiles = ['kilo.js', 'rovodev.js'];
for (const file of customFiles) {
const filePath = path.join(ideDir, file);
diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml
index a33c01a92..c0d20679d 100644
--- a/tools/cli/installers/lib/ide/platform-codes.yaml
+++ b/tools/cli/installers/lib/ide/platform-codes.yaml
@@ -119,7 +119,13 @@ platforms:
preferred: false
category: ide
description: "GitHub's AI pair programmer"
- # No installer config - uses custom github-copilot.js
+ installer:
+ legacy_targets:
+ - .github/agents
+ - .github/prompts
+ target_dir: .github/skills
+ template_type: default
+ skill_format: true
iflow:
name: "iFlow"