fix: escape YAML descriptions and preserve user copilot-instructions

- Escape single quotes in YAML frontmatter descriptions across all prompt
  generators (createWorkflowPromptContent, createTechWriterPromptContent,
  createAgentActivatorPromptContent) to match createAgentContent behavior
- Make copilot-instructions.md non-destructive using BMAD markers
  (<!-- BMAD:START --> / <!-- BMAD:END -->) to preserve user content
- On cleanup, only remove content between markers; skip files without markers
- Back up existing unmarked files before overwriting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jheyworth 2026-02-09 00:31:46 +00:00
parent 7d0de22ddc
commit 3ba4bbcb73
1 changed files with 72 additions and 11 deletions

View File

@ -255,7 +255,7 @@ You must fully embody this agent's persona and follow all activation instruction
* @returns {string} Prompt file content * @returns {string} Prompt file content
*/ */
createWorkflowPromptContent(entry, workflowFile) { createWorkflowPromptContent(entry, workflowFile) {
const description = this.createPromptDescription(entry.name); const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
const configLine = '1. Load {project-root}/_bmad/bmm/config.yaml and store ALL fields as session variables'; const configLine = '1. Load {project-root}/_bmad/bmm/config.yaml and store ALL fields as session variables';
let body; let body;
@ -348,8 +348,10 @@ ${body}
const cmd = techWriterCommands[entry.name]; const cmd = techWriterCommands[entry.name];
if (!cmd) return null; if (!cmd) return null;
const safeDescription = this.escapeYamlSingleQuote(cmd.description);
const content = `--- const content = `---
description: '${cmd.description}' description: '${safeDescription}'
agent: 'agent' agent: 'agent'
tools: ['read', 'edit', 'search', 'execute'] tools: ['read', 'edit', 'search', 'execute']
--- ---
@ -376,11 +378,12 @@ tools: ['read', 'edit', 'search', 'execute']
description = this.formatTitle(artifact.name); description = this.formatTitle(artifact.name);
} }
const safeDescription = this.escapeYamlSingleQuote(description);
const agentPath = artifact.agentPath || artifact.relativePath; const agentPath = artifact.agentPath || artifact.relativePath;
const agentFilePath = `{project-root}/_bmad/${agentPath}`; const agentFilePath = `{project-root}/_bmad/${agentPath}`;
return `--- return `---
description: '${description}' description: '${safeDescription}'
agent: 'agent' agent: 'agent'
tools: ['read', 'edit', 'search', 'execute'] tools: ['read', 'edit', 'search', 'execute']
--- ---
@ -428,7 +431,7 @@ tools: ['read', 'edit', 'search', 'execute']
} }
const bmad = '_bmad'; const bmad = '_bmad';
const content = `# BMAD Method — Project Instructions const bmadSection = `# BMAD Method — Project Instructions
## Project Configuration ## Project Configuration
@ -471,13 +474,39 @@ tools: ['read', 'edit', 'search', 'execute']
${agentsTable} ${agentsTable}
## Slash Commands ## Slash Commands
Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent activators. Agents are also available in the agents dropdown. 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.configDir, 'copilot-instructions.md'); const instructionsPath = path.join(projectDir, this.configDir, 'copilot-instructions.md');
await this.writeFile(instructionsPath, content); const markerStart = '<!-- BMAD:START -->';
const markerEnd = '<!-- BMAD:END -->';
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);
console.log(chalk.green(' ✓ Updated BMAD section in copilot-instructions.md'));
} else {
// Existing file without markers — back it up before overwriting
const backupPath = `${instructionsPath}.bak`;
await fs.copy(instructionsPath, backupPath);
console.log(chalk.yellow(` ⚠ Backed up existing copilot-instructions.md → copilot-instructions.md.bak`));
await this.writeFile(instructionsPath, `${markedContent}\n`);
console.log(chalk.green(' ✓ Generated copilot-instructions.md (with BMAD markers)'));
}
} else {
// No existing file — create fresh with markers
await this.writeFile(instructionsPath, `${markedContent}\n`);
console.log(chalk.green(' ✓ Generated copilot-instructions.md')); console.log(chalk.green(' ✓ Generated copilot-instructions.md'));
} }
}
/** /**
* Load module config.yaml for template variables * Load module config.yaml for template variables
@ -502,6 +531,16 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
return {}; 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("'", "''");
}
/** /**
* Format name as title * Format name as title
*/ */
@ -552,12 +591,34 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
} }
} }
// Clean up copilot-instructions.md // Clean up BMAD section from copilot-instructions.md (preserve user content)
const instructionsPath = path.join(projectDir, this.configDir, 'copilot-instructions.md'); const instructionsPath = path.join(projectDir, this.configDir, 'copilot-instructions.md');
if (await fs.pathExists(instructionsPath)) { if (await fs.pathExists(instructionsPath)) {
const existing = await fs.readFile(instructionsPath, 'utf8');
const markerStart = '<!-- BMAD:START -->';
const markerEnd = '<!-- BMAD:END -->';
const startIdx = existing.indexOf(markerStart);
const endIdx = existing.indexOf(markerEnd);
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
// Remove only the BMAD section between markers (inclusive)
const before = existing.slice(0, startIdx);
const after = existing.slice(endIdx + markerEnd.length);
const remaining = (before + after).trim();
if (remaining.length > 0) {
await fs.writeFile(instructionsPath, `${remaining}\n`);
console.log(chalk.dim(' Cleaned up BMAD section from copilot-instructions.md (user content preserved)'));
} else {
await fs.remove(instructionsPath); await fs.remove(instructionsPath);
console.log(chalk.dim(' Cleaned up copilot-instructions.md')); console.log(chalk.dim(' Cleaned up copilot-instructions.md'));
} }
} else {
// No markers — file is either entirely BMAD-generated or entirely user content.
// Leave it alone during cleanup to avoid destroying user content.
console.log(chalk.dim(' Skipped copilot-instructions.md (no BMAD markers found, not modified)'));
}
}
} }
} }