Compare commits

...

4 Commits

Author SHA1 Message Date
Hayden Carson 7d3be9f6cb
Merge 7416866694 into efc69ffb2c 2026-03-02 03:55:37 +08:00
Hayden Carson 7416866694 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>
2026-03-01 14:18:16 +08:00
Hayden Carson 2a9d984a2c 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>
2026-03-01 12:53:01 +08:00
Hayden Carson 78ab55c7e3 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>
2026-02-28 16:37:43 +08:00
1 changed files with 81 additions and 78 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,9 +142,14 @@ 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) {
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
const name = artifact.name;
if (manifestEntry) {
const persona = manifestEntry.displayName || artifact.name;
const title = manifestEntry.title || this.formatTitle(artifact.name);
@ -159,13 +159,15 @@ class GitHubCopilotSetup extends BaseIdeSetup {
description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`;
}
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.
@ -197,15 +199,39 @@ 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);
}
const seenSlugs = new Set();
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);
// When multiple entries share the same command, derive a unique filename from the entry name
let promptFileName;
if (commandCounts.get(command) > 1) {
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`;
}
const promptContent = this.createWorkflowPromptContent(entry, workflowFile);
const promptPath = path.join(promptsDir, promptFileName);
await this.writeFile(promptPath, promptContent);
promptCount++;
@ -228,8 +254,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++;
@ -245,8 +270,9 @@ You must fully embody this agent's persona and follow all activation instruction
* @param {string} workflowFile - Workflow file path
* @returns {string} Prompt file content
*/
createWorkflowPromptContent(entry, workflowFile, toolsStr) {
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
// 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,13 +293,36 @@ 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 raw agent name to match agent .agent.md name field
const agentName = (entry['agent-name'] || '').trim();
let agentLine = '';
if (agentName) {
agentLine = `\nagent: '${this.escapeYamlSingleQuote(agentName)}'`;
}
// Include options (e.g., "Create Mode", "Validate Mode") when present
const options = (entry.options || '').trim();
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 `---
description: '${description}'
agent: 'agent'
tools: ${toolsStr}
name: '${promptName}'
description: '${description}'${agentLine}
---
${body}
${body}${optionsInstruction}
`;
}
@ -341,13 +390,15 @@ ${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 raw agent name to match agent .agent.md name field
const agentLine = `\nagent: '${this.escapeYamlSingleQuote(entry['agent-name'])}'`;
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,7 +415,7 @@ tools: ${toolsStr}
* @param {Object|undefined} manifestEntry - Agent manifest entry
* @returns {string} Prompt file content
*/
createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) {
createAgentActivatorPromptContent(artifact, manifestEntry) {
let description;
if (manifestEntry) {
description = manifestEntry.title || this.formatTitle(artifact.name);
@ -372,19 +423,21 @@ tools: ${toolsStr}
description = 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;
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: 'agent'
tools: ${toolsStr}
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
@ -534,56 +587,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
*/