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>
This commit is contained in:
Hayden Carson 2026-02-28 16:37:43 +08:00
parent 43cfc01f2c
commit 78ab55c7e3
1 changed files with 38 additions and 73 deletions

View File

@ -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<filename, toolsArray> 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
*/