From 78ab55c7e3189d9288ff302e5af89fdac555e1e6 Mon Sep 17 00:00:00 2001 From: Hayden Carson Date: Sat, 28 Feb 2026 16:37:43 +0800 Subject: [PATCH 1/3] feat: remove tools, add name and agent assignments to GitHub Copilot installer Remove tools declaration from agent and prompt frontmatter since users manage their own tooling. Add name field to agents for cleaner @mention names and to prompts for cleaner /command display. Set agent field in prompts to the actual agent displayName for context continuity instead of resetting to default. Omit agent from prompts with no assigned agent. Remove now-unused getToolsForFile() and collectExistingToolPermissions() methods and related tool-permission preservation code from setup(). Fixes #1794 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/installers/lib/ide/github-copilot.js | 111 ++++++------------ 1 file changed, 38 insertions(+), 73 deletions(-) diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js index 059127f81..d3028f5ad 100644 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ b/tools/cli/installers/lib/ide/github-copilot.js @@ -40,9 +40,6 @@ class GitHubCopilotSetup extends BaseIdeSetup { 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); @@ -58,11 +55,9 @@ class GitHubCopilotSetup extends BaseIdeSetup { 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 agentContent = this.createAgentContent(artifact, agentMeta); const targetPath = path.join(agentsDir, fileName); await this.writeFile(targetPath, agentContent); agentCount++; @@ -147,25 +142,30 @@ class GitHubCopilotSetup extends BaseIdeSetup { * @param {Object|undefined} manifestEntry - Agent manifest entry with metadata * @returns {string} Agent file content */ - createAgentContent(artifact, manifestEntry, toolsStr) { + createAgentContent(artifact, manifestEntry) { // Build enriched description from manifest metadata let description; + let name; 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}`; + name = manifestEntry.displayName || this.formatTitle(artifact.name); } else { description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`; + name = this.formatTitle(artifact.name); } + const safeName = this.escapeYamlSingleQuote(name); + // Build the agent file path for the activation block const agentPath = artifact.agentPath || artifact.relativePath; const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`; return `--- +name: '${safeName}' description: '${description.replaceAll("'", "''")}' -tools: ${toolsStr} --- You must fully embody this agent's persona and follow all activation instructions exactly as specified. @@ -204,8 +204,7 @@ You must fully embody this agent's persona and follow all activation instruction 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 promptContent = this.createWorkflowPromptContent(entry, workflowFile, agentManifest); const promptPath = path.join(promptsDir, promptFileName); await this.writeFile(promptPath, promptContent); promptCount++; @@ -228,8 +227,7 @@ You must fully embody this agent's persona and follow all activation instruction 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 promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta); const promptPath = path.join(promptsDir, fileName); await this.writeFile(promptPath, promptContent); promptCount++; @@ -243,10 +241,12 @@ You must fully embody this agent's persona and follow all activation instruction * 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 + * @param {Map} agentManifest - Agent manifest data for display name lookup * @returns {string} Prompt file content */ - createWorkflowPromptContent(entry, workflowFile, toolsStr) { + createWorkflowPromptContent(entry, workflowFile, agentManifest) { const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name)); + const promptName = this.escapeYamlSingleQuote(entry.name || description); // 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`; @@ -267,10 +267,18 @@ You must fully embody this agent's persona and follow all activation instruction 2. Load and follow the workflow at {project-root}/${workflowFile}`; } + // Build the agent line: use agent displayName from manifest if available + const agentName = (entry['agent-name'] || '').trim(); + let agentLine = ''; + if (agentName) { + const agentMeta = agentManifest.get(agentName); + const agentDisplayName = (agentMeta && agentMeta.displayName) || this.formatTitle(agentName); + agentLine = `\nagent: '${this.escapeYamlSingleQuote(agentDisplayName)}'`; + } + return `--- -description: '${description}' -agent: 'agent' -tools: ${toolsStr} +name: '${promptName}' +description: '${description}'${agentLine} --- ${body} @@ -341,13 +349,16 @@ ${body} const cmd = techWriterCommands[entry.name]; if (!cmd) return null; + const safeName = this.escapeYamlSingleQuote(entry.name); const safeDescription = this.escapeYamlSingleQuote(cmd.description); - const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`); + + // Use agent display name from merged CSV if available, otherwise format the raw name + const agentDisplayName = (entry['agent-display-name'] || '').trim() || this.formatTitle(entry['agent-name']); + const agentLine = `\nagent: '${this.escapeYamlSingleQuote(agentDisplayName)}'`; const content = `--- -description: '${safeDescription}' -agent: 'agent' -tools: ${toolsStr} +name: '${safeName}' +description: '${safeDescription}'${agentLine} --- 1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables @@ -364,14 +375,18 @@ tools: ${toolsStr} * @param {Object|undefined} manifestEntry - Agent manifest entry * @returns {string} Prompt file content */ - createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) { + createAgentActivatorPromptContent(artifact, manifestEntry) { let description; + let name; if (manifestEntry) { description = manifestEntry.title || this.formatTitle(artifact.name); + name = manifestEntry.displayName || this.formatTitle(artifact.name); } else { description = this.formatTitle(artifact.name); + name = this.formatTitle(artifact.name); } + const safeName = this.escapeYamlSingleQuote(name); const safeDescription = this.escapeYamlSingleQuote(description); const agentPath = artifact.agentPath || artifact.relativePath; const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`; @@ -379,9 +394,9 @@ tools: ${toolsStr} // bmm/config.yaml is safe to hardcode: agent activators are only generated from // bmm agent artifacts, so bmm is guaranteed to be installed. return `--- +name: '${safeName}' description: '${safeDescription}' -agent: 'agent' -tools: ${toolsStr} +agent: '${safeName}' --- 1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables @@ -534,56 +549,6 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac 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 */ From 2a9d984a2c9f63356d6fb891db3952592179398c Mon Sep 17 00:00:00 2001 From: Hayden Carson Date: Sun, 1 Mar 2026 12:53:01 +0800 Subject: [PATCH 2/3] fix: use raw agent names and resolve duplicate command filenames Use raw agent names (e.g., 'dev', 'pm') for the name field in .agent.md frontmatter and all agent references in prompt frontmatter, instead of persona display names. This provides clean @mention and /command names. Resolve Create Story / Validate Story filename collision where both entries shared command 'bmad-bmm-create-story'. When multiple entries share a command, derive unique filenames from the entry name slug. Also pass workflow options (Create Mode, Validate Mode) to the prompt body so each prompt invokes the correct workflow mode. Fixes #1794 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/installers/lib/ide/github-copilot.js | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js index d3028f5ad..b20ce2fd0 100644 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ b/tools/cli/installers/lib/ide/github-copilot.js @@ -145,16 +145,15 @@ class GitHubCopilotSetup extends BaseIdeSetup { createAgentContent(artifact, manifestEntry) { // Build enriched description from manifest metadata let description; - let name; + // Use the raw agent name (e.g., "dev", "pm") for clean @mention selection + const name = artifact.name; 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}`; - name = manifestEntry.displayName || this.formatTitle(artifact.name); } else { description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`; - name = this.formatTitle(artifact.name); } const safeName = this.escapeYamlSingleQuote(name); @@ -197,14 +196,30 @@ You must fully embody this agent's persona and follow all activation instruction const helpEntries = await this.loadBmadHelp(bmadDir); if (helpEntries) { + // Detect duplicate commands to derive unique filenames when multiple entries share one + const commandCounts = new Map(); + for (const entry of helpEntries) { + if (!entry.command || !entry['workflow-file']) continue; + commandCounts.set(entry.command, (commandCounts.get(entry.command) || 0) + 1); + } + 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 promptContent = this.createWorkflowPromptContent(entry, workflowFile, agentManifest); + + // When multiple entries share the same command, derive a unique filename from the entry name + let promptFileName; + if (commandCounts.get(command) > 1) { + const slug = entry.name.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-'); + promptFileName = `bmad-bmm-${slug}.prompt.md`; + } else { + promptFileName = `${command}.prompt.md`; + } + + const promptContent = this.createWorkflowPromptContent(entry, workflowFile); const promptPath = path.join(promptsDir, promptFileName); await this.writeFile(promptPath, promptContent); promptCount++; @@ -241,10 +256,9 @@ You must fully embody this agent's persona and follow all activation instruction * 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 - * @param {Map} agentManifest - Agent manifest data for display name lookup * @returns {string} Prompt file content */ - createWorkflowPromptContent(entry, workflowFile, agentManifest) { + createWorkflowPromptContent(entry, workflowFile) { const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name)); const promptName = this.escapeYamlSingleQuote(entry.name || description); // bmm/config.yaml is safe to hardcode here: these prompts are only generated when @@ -267,21 +281,23 @@ You must fully embody this agent's persona and follow all activation instruction 2. Load and follow the workflow at {project-root}/${workflowFile}`; } - // Build the agent line: use agent displayName from manifest if available + // Build the agent line: use raw agent name to match agent .agent.md name field const agentName = (entry['agent-name'] || '').trim(); let agentLine = ''; if (agentName) { - const agentMeta = agentManifest.get(agentName); - const agentDisplayName = (agentMeta && agentMeta.displayName) || this.formatTitle(agentName); - agentLine = `\nagent: '${this.escapeYamlSingleQuote(agentDisplayName)}'`; + agentLine = `\nagent: '${this.escapeYamlSingleQuote(agentName)}'`; } + // Include options (e.g., "Create Mode", "Validate Mode") when present + const options = (entry.options || '').trim(); + const optionsInstruction = options ? `\n4. Use option: ${options}` : ''; + return `--- name: '${promptName}' description: '${description}'${agentLine} --- -${body} +${body}${optionsInstruction} `; } @@ -352,9 +368,8 @@ ${body} const safeName = this.escapeYamlSingleQuote(entry.name); const safeDescription = this.escapeYamlSingleQuote(cmd.description); - // Use agent display name from merged CSV if available, otherwise format the raw name - const agentDisplayName = (entry['agent-display-name'] || '').trim() || this.formatTitle(entry['agent-name']); - const agentLine = `\nagent: '${this.escapeYamlSingleQuote(agentDisplayName)}'`; + // Use raw agent name to match agent .agent.md name field + const agentLine = `\nagent: '${this.escapeYamlSingleQuote(entry['agent-name'])}'`; const content = `--- name: '${safeName}' @@ -377,15 +392,14 @@ description: '${safeDescription}'${agentLine} */ createAgentActivatorPromptContent(artifact, manifestEntry) { let description; - let name; if (manifestEntry) { description = manifestEntry.title || this.formatTitle(artifact.name); - name = manifestEntry.displayName || this.formatTitle(artifact.name); } else { description = this.formatTitle(artifact.name); - name = this.formatTitle(artifact.name); } + // Use the raw agent name (e.g., "dev") to match agent .agent.md name field + const name = artifact.name; const safeName = this.escapeYamlSingleQuote(name); const safeDescription = this.escapeYamlSingleQuote(description); const agentPath = artifact.agentPath || artifact.relativePath; From 7416866694c4c8cb011d139a5b6cb74f62d28d1d Mon Sep 17 00:00:00 2001 From: Hayden Carson Date: Sun, 1 Mar 2026 14:18:16 +0800 Subject: [PATCH 3/3] fix: address PR review comments on copilot installer - Dynamically compute step number for options instruction instead of hardcoding 4 (fixes skipped step 3 for .md and .xml patterns) - Add validation guard for empty/undefined artifact.name in createAgentContent - Trim leading/trailing dashes from slugs, guard against empty and colliding slugs in duplicate command handling - Use artifact.module for config.yaml path in agent activator prompts instead of hardcoding bmm (core agents now reference core/config.yaml) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/installers/lib/ide/github-copilot.js | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js index b20ce2fd0..fb7fb4dbe 100644 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ b/tools/cli/installers/lib/ide/github-copilot.js @@ -143,6 +143,9 @@ class GitHubCopilotSetup extends BaseIdeSetup { * @returns {string} Agent file content */ createAgentContent(artifact, manifestEntry) { + if (!artifact?.name) { + throw new Error('Agent artifact must have a name'); + } // Build enriched description from manifest metadata let description; // Use the raw agent name (e.g., "dev", "pm") for clean @mention selection @@ -202,6 +205,7 @@ You must fully embody this agent's persona and follow all activation instruction if (!entry.command || !entry['workflow-file']) continue; commandCounts.set(entry.command, (commandCounts.get(entry.command) || 0) + 1); } + const seenSlugs = new Set(); for (const entry of helpEntries) { const command = entry.command; @@ -213,7 +217,15 @@ You must fully embody this agent's persona and follow all activation instruction // When multiple entries share the same command, derive a unique filename from the entry name let promptFileName; if (commandCounts.get(command) > 1) { - const slug = entry.name.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-'); + let slug = entry.name.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-').replaceAll(/^-+|-+$/g, ''); + if (!slug) { + slug = `unnamed-${promptCount}`; + } + // Guard against slug collisions + while (seenSlugs.has(slug)) { + slug = `${slug}-${promptCount}`; + } + seenSlugs.add(slug); promptFileName = `bmad-bmm-${slug}.prompt.md`; } else { promptFileName = `${command}.prompt.md`; @@ -290,7 +302,20 @@ You must fully embody this agent's persona and follow all activation instruction // Include options (e.g., "Create Mode", "Validate Mode") when present const options = (entry.options || '').trim(); - const optionsInstruction = options ? `\n4. Use option: ${options}` : ''; + let optionsInstruction = ''; + if (options) { + // Determine the next step number based on the last numbered step in the body + let nextStepNumber = 4; + const stepMatches = body.match(/(?:^|\n)(\d+)\.\s/g); + if (stepMatches && stepMatches.length > 0) { + const lastMatch = stepMatches.at(-1); + const numberMatch = lastMatch.match(/(\d+)\.\s/); + if (numberMatch) { + nextStepNumber = parseInt(numberMatch[1], 10) + 1; + } + } + optionsInstruction = `\n${nextStepNumber}. Use option: ${options}`; + } return `--- name: '${promptName}' @@ -405,15 +430,14 @@ description: '${safeDescription}'${agentLine} 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. + const moduleName = artifact.module || 'bmm'; return `--- name: '${safeName}' description: '${safeDescription}' agent: '${safeName}' --- -1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables +1. Load {project-root}/${this.bmadFolderName}/${moduleName}/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