Merge 7416866694 into 259e8a11ba
This commit is contained in:
commit
0494020c6d
|
|
@ -40,9 +40,6 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
await this.ensureDir(agentsDir);
|
await this.ensureDir(agentsDir);
|
||||||
await this.ensureDir(promptsDir);
|
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
|
// Clean up any existing BMAD files before reinstalling
|
||||||
await this.cleanup(projectDir);
|
await this.cleanup(projectDir);
|
||||||
|
|
||||||
|
|
@ -58,11 +55,9 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
for (const artifact of agentArtifacts) {
|
for (const artifact of agentArtifacts) {
|
||||||
const agentMeta = agentManifest.get(artifact.name);
|
const agentMeta = agentManifest.get(artifact.name);
|
||||||
|
|
||||||
// Compute fileName first so we can look up any existing tool permissions
|
|
||||||
const dashName = toDashPath(artifact.relativePath);
|
const dashName = toDashPath(artifact.relativePath);
|
||||||
const fileName = dashName.replace(/\.md$/, '.agent.md');
|
const fileName = dashName.replace(/\.md$/, '.agent.md');
|
||||||
const toolsStr = this.getToolsForFile(fileName);
|
const agentContent = this.createAgentContent(artifact, agentMeta);
|
||||||
const agentContent = this.createAgentContent(artifact, agentMeta, toolsStr);
|
|
||||||
const targetPath = path.join(agentsDir, fileName);
|
const targetPath = path.join(agentsDir, fileName);
|
||||||
await this.writeFile(targetPath, agentContent);
|
await this.writeFile(targetPath, agentContent);
|
||||||
agentCount++;
|
agentCount++;
|
||||||
|
|
@ -147,9 +142,14 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
* @param {Object|undefined} manifestEntry - Agent manifest entry with metadata
|
* @param {Object|undefined} manifestEntry - Agent manifest entry with metadata
|
||||||
* @returns {string} Agent file content
|
* @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
|
// Build enriched description from manifest metadata
|
||||||
let description;
|
let description;
|
||||||
|
// Use the raw agent name (e.g., "dev", "pm") for clean @mention selection
|
||||||
|
const name = artifact.name;
|
||||||
if (manifestEntry) {
|
if (manifestEntry) {
|
||||||
const persona = manifestEntry.displayName || artifact.name;
|
const persona = manifestEntry.displayName || artifact.name;
|
||||||
const title = manifestEntry.title || this.formatTitle(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.`;
|
description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const safeName = this.escapeYamlSingleQuote(name);
|
||||||
|
|
||||||
// Build the agent file path for the activation block
|
// Build the agent file path for the activation block
|
||||||
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}`;
|
||||||
|
|
||||||
return `---
|
return `---
|
||||||
|
name: '${safeName}'
|
||||||
description: '${description.replaceAll("'", "''")}'
|
description: '${description.replaceAll("'", "''")}'
|
||||||
tools: ${toolsStr}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified.
|
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);
|
const helpEntries = await this.loadBmadHelp(bmadDir);
|
||||||
|
|
||||||
if (helpEntries) {
|
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) {
|
for (const entry of helpEntries) {
|
||||||
const command = entry.command;
|
const command = entry.command;
|
||||||
if (!command) continue; // Skip entries without a command (tech-writer commands have no command column)
|
if (!command) continue; // Skip entries without a command (tech-writer commands have no command column)
|
||||||
|
|
||||||
const workflowFile = entry['workflow-file'];
|
const workflowFile = entry['workflow-file'];
|
||||||
if (!workflowFile) continue; // Skip entries with no workflow file path
|
if (!workflowFile) continue; // Skip entries with no workflow file path
|
||||||
const promptFileName = `${command}.prompt.md`;
|
|
||||||
const toolsStr = this.getToolsForFile(promptFileName);
|
// When multiple entries share the same command, derive a unique filename from the entry name
|
||||||
const promptContent = this.createWorkflowPromptContent(entry, workflowFile, toolsStr);
|
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);
|
const promptPath = path.join(promptsDir, promptFileName);
|
||||||
await this.writeFile(promptPath, promptContent);
|
await this.writeFile(promptPath, promptContent);
|
||||||
promptCount++;
|
promptCount++;
|
||||||
|
|
@ -228,8 +254,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
||||||
for (const artifact of agentArtifacts) {
|
for (const artifact of agentArtifacts) {
|
||||||
const agentMeta = agentManifest.get(artifact.name);
|
const agentMeta = agentManifest.get(artifact.name);
|
||||||
const fileName = `bmad-${artifact.name}.prompt.md`;
|
const fileName = `bmad-${artifact.name}.prompt.md`;
|
||||||
const toolsStr = this.getToolsForFile(fileName);
|
const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta);
|
||||||
const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta, toolsStr);
|
|
||||||
const promptPath = path.join(promptsDir, fileName);
|
const promptPath = path.join(promptsDir, fileName);
|
||||||
await this.writeFile(promptPath, promptContent);
|
await this.writeFile(promptPath, promptContent);
|
||||||
promptCount++;
|
promptCount++;
|
||||||
|
|
@ -245,8 +270,9 @@ You must fully embody this agent's persona and follow all activation instruction
|
||||||
* @param {string} workflowFile - Workflow file path
|
* @param {string} workflowFile - Workflow file path
|
||||||
* @returns {string} Prompt file content
|
* @returns {string} Prompt file content
|
||||||
*/
|
*/
|
||||||
createWorkflowPromptContent(entry, workflowFile, toolsStr) {
|
createWorkflowPromptContent(entry, workflowFile) {
|
||||||
const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
|
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
|
// 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.
|
// 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`;
|
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}`;
|
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 `---
|
return `---
|
||||||
description: '${description}'
|
name: '${promptName}'
|
||||||
agent: 'agent'
|
description: '${description}'${agentLine}
|
||||||
tools: ${toolsStr}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
${body}
|
${body}${optionsInstruction}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,13 +390,15 @@ ${body}
|
||||||
const cmd = techWriterCommands[entry.name];
|
const cmd = techWriterCommands[entry.name];
|
||||||
if (!cmd) return null;
|
if (!cmd) return null;
|
||||||
|
|
||||||
|
const safeName = this.escapeYamlSingleQuote(entry.name);
|
||||||
const safeDescription = this.escapeYamlSingleQuote(cmd.description);
|
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 = `---
|
const content = `---
|
||||||
description: '${safeDescription}'
|
name: '${safeName}'
|
||||||
agent: 'agent'
|
description: '${safeDescription}'${agentLine}
|
||||||
tools: ${toolsStr}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
|
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
|
* @param {Object|undefined} manifestEntry - Agent manifest entry
|
||||||
* @returns {string} Prompt file content
|
* @returns {string} Prompt file content
|
||||||
*/
|
*/
|
||||||
createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) {
|
createAgentActivatorPromptContent(artifact, manifestEntry) {
|
||||||
let description;
|
let description;
|
||||||
if (manifestEntry) {
|
if (manifestEntry) {
|
||||||
description = manifestEntry.title || this.formatTitle(artifact.name);
|
description = manifestEntry.title || this.formatTitle(artifact.name);
|
||||||
|
|
@ -372,19 +423,21 @@ tools: ${toolsStr}
|
||||||
description = this.formatTitle(artifact.name);
|
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 safeDescription = this.escapeYamlSingleQuote(description);
|
||||||
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
|
const moduleName = artifact.module || 'bmm';
|
||||||
// bmm agent artifacts, so bmm is guaranteed to be installed.
|
|
||||||
return `---
|
return `---
|
||||||
|
name: '${safeName}'
|
||||||
description: '${safeDescription}'
|
description: '${safeDescription}'
|
||||||
agent: 'agent'
|
agent: '${safeName}'
|
||||||
tools: ${toolsStr}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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}
|
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
|
||||||
|
|
@ -534,56 +587,6 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
|
||||||
return (value || '').replaceAll("'", "''");
|
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
|
* Format name as title
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue