From 34abaf2f4acfbaff0b51c8cf3e61f8bb9ca32ef5 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Mon, 9 Mar 2026 00:37:11 -0600 Subject: [PATCH] refactor(skills): add SKILL.md entrypoint to skill directories Align skill source format with Open Skills standard: each skill directory now contains a SKILL.md with name/description frontmatter where name must match the directory name exactly. The installer copies skill directories verbatim instead of generating SKILL.md. - Add SKILL.md to both tracer bullet skill directories - Strip name/description from workflow.md frontmatter (SKILL.md owns it) - Installer reads metadata from SKILL.md, validates name matches dirname - Install path in manifest CSV now points to SKILL.md - Copy filter excludes OS/editor artifacts (.DS_Store, backups, dotfiles) - Debug-guard validation messages, keep name-mismatch as hard error - Add typeof guard for malformed YAML frontmatter - Add negative test cases for parseSkillMd validation (Suite 30) --- .../bmad-quick-dev-new-preview/SKILL.md | 6 + .../bmad-quick-dev-new-preview/workflow.md | 2 - .../bmad-review-adversarial-general/SKILL.md | 6 + .../workflow.md | 5 - test/test-installation-components.js | 98 ++++++++++-- .../installers/lib/core/manifest-generator.js | 150 ++++++++++-------- .../cli/installers/lib/ide/_config-driven.js | 33 ++-- 7 files changed, 194 insertions(+), 106 deletions(-) create mode 100644 src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md create mode 100644 src/core/tasks/bmad-review-adversarial-general/SKILL.md diff --git a/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md new file mode 100644 index 000000000..3a23670c4 --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md @@ -0,0 +1,6 @@ +--- +name: bmad-quick-dev-new-preview +description: 'Unified quick flow - clarify intent, plan, implement, review, present.' +--- + +Follow the instructions in [workflow.md](workflow.md). diff --git a/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md index 0231240be..79c837459 100644 --- a/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md +++ b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md @@ -1,6 +1,4 @@ --- -name: quick-dev-new-preview -description: 'Unified quick flow - clarify intent, plan, implement, review, present.' main_config: '{project-root}/_bmad/bmm/config.yaml' # Related workflows diff --git a/src/core/tasks/bmad-review-adversarial-general/SKILL.md b/src/core/tasks/bmad-review-adversarial-general/SKILL.md new file mode 100644 index 000000000..88f5b2fa1 --- /dev/null +++ b/src/core/tasks/bmad-review-adversarial-general/SKILL.md @@ -0,0 +1,6 @@ +--- +name: bmad-review-adversarial-general +description: 'Perform a Cynical Review and produce a findings report. Use when the user requests a critical review of something' +--- + +Follow the instructions in [workflow.md](workflow.md). diff --git a/src/core/tasks/bmad-review-adversarial-general/workflow.md b/src/core/tasks/bmad-review-adversarial-general/workflow.md index ae75b7caa..8290ff16d 100644 --- a/src/core/tasks/bmad-review-adversarial-general/workflow.md +++ b/src/core/tasks/bmad-review-adversarial-general/workflow.md @@ -1,8 +1,3 @@ ---- -name: bmad-review-adversarial-general -description: 'Perform a Cynical Review and produce a findings report. Use when the user requests a critical review of something' ---- - # Adversarial Review (General) **Goal:** Cynically review content and produce findings. diff --git a/test/test-installation-components.js b/test/test-installation-components.js index cf075bd67..acb060cf3 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1607,9 +1607,10 @@ async function runTests() { await fs.ensureDir(skillDir29); await fs.writeFile(path.join(skillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n'); await fs.writeFile( - path.join(skillDir29, 'workflow.md'), - '---\nname: My Custom Skill\ndescription: A skill at an unusual path\n---\n\nSkill body content\n', + path.join(skillDir29, 'SKILL.md'), + '---\nname: my-skill\ndescription: A skill at an unusual path\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n', ); + await fs.writeFile(path.join(skillDir29, 'workflow.md'), '# My Custom Skill\n\nSkill body content\n'); // --- Regular workflow dir: core/workflows/regular-wf/ (type: workflow) --- const wfDir29 = path.join(tempFixture29, 'core', 'workflows', 'regular-wf'); @@ -1625,18 +1626,20 @@ async function runTests() { await fs.ensureDir(wfSkillDir29); await fs.writeFile(path.join(wfSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n'); await fs.writeFile( - path.join(wfSkillDir29, 'workflow.md'), - '---\nname: Workflow Skill\ndescription: A skill inside workflows dir\n---\n\nSkill in workflows\n', + path.join(wfSkillDir29, 'SKILL.md'), + '---\nname: wf-skill\ndescription: A skill inside workflows dir\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n', ); + await fs.writeFile(path.join(wfSkillDir29, 'workflow.md'), '# Workflow Skill\n\nSkill in workflows\n'); // --- Skill inside tasks/ dir: core/tasks/task-skill/ --- const taskSkillDir29 = path.join(tempFixture29, 'core', 'tasks', 'task-skill'); await fs.ensureDir(taskSkillDir29); await fs.writeFile(path.join(taskSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n'); await fs.writeFile( - path.join(taskSkillDir29, 'workflow.md'), - '---\nname: Task Skill\ndescription: A skill inside tasks dir\n---\n\nSkill in tasks\n', + path.join(taskSkillDir29, 'SKILL.md'), + '---\nname: task-skill\ndescription: A skill inside tasks dir\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n', ); + await fs.writeFile(path.join(taskSkillDir29, 'workflow.md'), '# Task Skill\n\nSkill in tasks\n'); // Minimal agent so core module is detected await fs.ensureDir(path.join(tempFixture29, 'core', 'agents')); @@ -1649,14 +1652,14 @@ async function runTests() { // Skill at unusual path should be in skills const skillEntry29 = generator29.skills.find((s) => s.canonicalId === 'my-skill'); assert(skillEntry29 !== undefined, 'Skill at unusual path appears in skills[]'); - assert(skillEntry29 && skillEntry29.name === 'My Custom Skill', 'Skill has correct name from frontmatter'); + assert(skillEntry29 && skillEntry29.name === 'my-skill', 'Skill has correct name from frontmatter'); assert( - skillEntry29 && skillEntry29.path.includes('custom-area/my-skill/workflow.md'), + skillEntry29 && skillEntry29.path.includes('custom-area/my-skill/SKILL.md'), 'Skill path includes relative path from module root', ); // Skill should NOT be in workflows - const inWorkflows29 = generator29.workflows.find((w) => w.name === 'My Custom Skill'); + const inWorkflows29 = generator29.workflows.find((w) => w.name === 'my-skill'); assert(inWorkflows29 === undefined, 'Skill at unusual path does NOT appear in workflows[]'); // Skill in tasks/ dir should be in skills @@ -1664,7 +1667,7 @@ async function runTests() { assert(taskSkillEntry29 !== undefined, 'Skill in tasks/ dir appears in skills[]'); // Skill in tasks/ should NOT appear in tasks[] - const inTasks29 = generator29.tasks.find((t) => t.name === 'Task Skill'); + const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill'); assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]'); // Regular workflow should be in workflows, NOT in skills @@ -1677,7 +1680,7 @@ async function runTests() { // Skill inside workflows/ should be in skills[], NOT in workflows[] (exercises findWorkflows skip at lines 311/322) const wfSkill29 = generator29.skills.find((s) => s.canonicalId === 'wf-skill'); assert(wfSkill29 !== undefined, 'Skill in workflows/ dir appears in skills[]'); - const wfSkillInWorkflows29 = generator29.workflows.find((w) => w.name === 'Workflow Skill'); + const wfSkillInWorkflows29 = generator29.workflows.find((w) => w.name === 'wf-skill'); assert(wfSkillInWorkflows29 === undefined, 'Skill in workflows/ dir does NOT appear in workflows[]'); // Test scanInstalledModules recognizes skill-only modules @@ -1685,9 +1688,10 @@ async function runTests() { await fs.ensureDir(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill')); await fs.writeFile(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'bmad-skill-manifest.yaml'), 'type: skill\n'); await fs.writeFile( - path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'workflow.md'), - '---\nname: Nested Skill\ndescription: desc\n---\nbody\n', + path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'SKILL.md'), + '---\nname: my-skill\ndescription: desc\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n', ); + await fs.writeFile(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'workflow.md'), '# Nested Skill\n\nbody\n'); const scannedModules29 = await generator29.scanInstalledModules(tempFixture29); assert(scannedModules29.includes('skill-only-mod'), 'scanInstalledModules recognizes skill-only module'); @@ -1699,6 +1703,74 @@ async function runTests() { console.log(''); + // ============================================================ + // Suite 30: parseSkillMd validation (negative cases) + // ============================================================ + console.log(`${colors.yellow}Test Suite 30: parseSkillMd Validation${colors.reset}\n`); + + let tempFixture30; + try { + tempFixture30 = path.join(os.tmpdir(), `bmad-test-30-${Date.now()}`); + await fs.ensureDir(tempFixture30); + + const generator30 = new ManifestGenerator(); + generator30.bmadFolderName = '_bmad'; + + // Case 1: Missing SKILL.md entirely + const noSkillDir = path.join(tempFixture30, 'no-skill-md'); + await fs.ensureDir(noSkillDir); + const result1 = await generator30.parseSkillMd(path.join(noSkillDir, 'SKILL.md'), noSkillDir, 'no-skill-md'); + assert(result1 === null, 'parseSkillMd returns null when SKILL.md is missing'); + + // Case 2: SKILL.md with no frontmatter + const noFmDir = path.join(tempFixture30, 'no-frontmatter'); + await fs.ensureDir(noFmDir); + await fs.writeFile(path.join(noFmDir, 'SKILL.md'), '# Just a heading\n\nNo frontmatter here.\n'); + const result2 = await generator30.parseSkillMd(path.join(noFmDir, 'SKILL.md'), noFmDir, 'no-frontmatter'); + assert(result2 === null, 'parseSkillMd returns null when SKILL.md has no frontmatter'); + + // Case 3: SKILL.md missing description + const noDescDir = path.join(tempFixture30, 'no-desc'); + await fs.ensureDir(noDescDir); + await fs.writeFile(path.join(noDescDir, 'SKILL.md'), '---\nname: no-desc\n---\n\nBody.\n'); + const result3 = await generator30.parseSkillMd(path.join(noDescDir, 'SKILL.md'), noDescDir, 'no-desc'); + assert(result3 === null, 'parseSkillMd returns null when description is missing'); + + // Case 4: SKILL.md missing name + const noNameDir = path.join(tempFixture30, 'no-name'); + await fs.ensureDir(noNameDir); + await fs.writeFile(path.join(noNameDir, 'SKILL.md'), '---\ndescription: has desc but no name\n---\n\nBody.\n'); + const result4 = await generator30.parseSkillMd(path.join(noNameDir, 'SKILL.md'), noNameDir, 'no-name'); + assert(result4 === null, 'parseSkillMd returns null when name is missing'); + + // Case 5: Name mismatch + const mismatchDir = path.join(tempFixture30, 'actual-dir-name'); + await fs.ensureDir(mismatchDir); + await fs.writeFile(path.join(mismatchDir, 'SKILL.md'), '---\nname: wrong-name\ndescription: A skill\n---\n\nBody.\n'); + const result5 = await generator30.parseSkillMd(path.join(mismatchDir, 'SKILL.md'), mismatchDir, 'actual-dir-name'); + assert(result5 === null, 'parseSkillMd returns null when name does not match directory name'); + + // Case 6: Valid SKILL.md (positive control) + const validDir = path.join(tempFixture30, 'valid-skill'); + await fs.ensureDir(validDir); + await fs.writeFile(path.join(validDir, 'SKILL.md'), '---\nname: valid-skill\ndescription: A valid skill\n---\n\nBody.\n'); + const result6 = await generator30.parseSkillMd(path.join(validDir, 'SKILL.md'), validDir, 'valid-skill'); + assert(result6 !== null && result6.name === 'valid-skill', 'parseSkillMd returns metadata for valid SKILL.md'); + + // Case 7: Malformed YAML (non-object) + const malformedDir = path.join(tempFixture30, 'malformed'); + await fs.ensureDir(malformedDir); + await fs.writeFile(path.join(malformedDir, 'SKILL.md'), '---\njust a string\n---\n\nBody.\n'); + const result7 = await generator30.parseSkillMd(path.join(malformedDir, 'SKILL.md'), malformedDir, 'malformed'); + assert(result7 === null, 'parseSkillMd returns null for non-object YAML frontmatter'); + } catch (error) { + assert(false, 'parseSkillMd validation test succeeds', error.message); + } finally { + if (tempFixture30) await fs.remove(tempFixture30).catch(() => {}); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index b02ccde9e..040d5ecc9 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -148,7 +148,7 @@ class ManifestGenerator { /** * Recursively walk a module directory tree, collecting skill directories. * A skill directory is one that contains both a bmad-skill-manifest.yaml with - * type: skill AND a workflow.md file. + * type: skill AND a SKILL.md file with name/description frontmatter. * Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths). */ async collectSkills() { @@ -169,75 +169,62 @@ class ManifestGenerator { return; } - // Check this directory for skill manifest + workflow file + // Check this directory for skill manifest const manifest = await this.loadSkillManifest(dir); - const workflowFile = 'workflow.md'; - const workflowPath = path.join(dir, workflowFile); - if (await fs.pathExists(workflowPath)) { - const artifactType = this.getArtifactType(manifest, workflowFile); - if (artifactType === 'skill') { - // Read and parse the workflow file - try { - const rawContent = await fs.readFile(workflowPath, 'utf8'); - const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); + // Determine if this directory is a skill (type: skill in manifest) + const skillFile = 'SKILL.md'; + const artifactType = this.getArtifactType(manifest, skillFile); - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (frontmatterMatch) { - const workflow = yaml.parse(frontmatterMatch[1]); + if (artifactType === 'skill') { + const skillMdPath = path.join(dir, 'SKILL.md'); + const dirName = path.basename(dir); - if (!workflow || !workflow.name || !workflow.description) { - if (debug) console.log(`[DEBUG] collectSkills: skipped (missing name/description): ${workflowPath}`); - } else { - // Build path relative from module root - const relativePath = path.relative(modulePath, dir).split(path.sep).join('/'); - const installPath = relativePath - ? `${this.bmadFolderName}/${moduleName}/${relativePath}/${workflowFile}` - : `${this.bmadFolderName}/${moduleName}/${workflowFile}`; + // Validate and parse SKILL.md + const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug); - // Skills derive canonicalId from directory name — never from manifest - if (manifest && manifest.__single && manifest.__single.canonicalId) { - console.warn( - `Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`, - ); - } - const canonicalId = path.basename(dir); + if (skillMeta) { + // Build path relative from module root (points to SKILL.md — the permanent entrypoint) + const relativePath = path.relative(modulePath, dir).split(path.sep).join('/'); + const installPath = relativePath + ? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}` + : `${this.bmadFolderName}/${moduleName}/${skillFile}`; - this.skills.push({ - name: workflow.name, - description: this.cleanForCSV(workflow.description), - module: moduleName, - path: installPath, - canonicalId, - install_to_bmad: this.getInstallToBmad(manifest, workflowFile), - }); + // Skills derive canonicalId from directory name — never from manifest + if (manifest && manifest.__single && manifest.__single.canonicalId) { + console.warn( + `Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`, + ); + } + const canonicalId = dirName; - // Add to files list - this.files.push({ - type: 'skill', - name: workflow.name, - module: moduleName, - path: installPath, - }); + this.skills.push({ + name: skillMeta.name, + description: this.cleanForCSV(skillMeta.description), + module: moduleName, + path: installPath, + canonicalId, + install_to_bmad: this.getInstallToBmad(manifest, skillFile), + }); - this.skillClaimedDirs.add(dir); + // Add to files list + this.files.push({ + type: 'skill', + name: skillMeta.name, + module: moduleName, + path: installPath, + }); - if (debug) { - console.log(`[DEBUG] collectSkills: claimed skill "${workflow.name}" as ${canonicalId} at ${dir}`); - } - } - } else { - if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`); - } - } catch (error) { - if (debug) console.log(`[DEBUG] collectSkills: failed to parse ${workflowPath}: ${error.message}`); + this.skillClaimedDirs.add(dir); + + if (debug) { + console.log(`[DEBUG] collectSkills: claimed skill "${skillMeta.name}" as ${canonicalId} at ${dir}`); } } } - // Warn if manifest says type:skill but no workflow file found + // Warn if manifest says type:skill but directory was not claimed if (manifest && !this.skillClaimedDirs.has(dir)) { - // Check if any entry in the manifest is type:skill let hasSkillType = false; if (manifest.__single) { hasSkillType = manifest.__single.type === 'skill'; @@ -250,12 +237,7 @@ class ManifestGenerator { } } if (hasSkillType && debug) { - const hasWorkflow = entries.some((e) => e.name === workflowFile); - if (hasWorkflow) { - console.log(`[DEBUG] collectSkills: dir has type:skill manifest but workflow file failed to parse: ${dir}`); - } else { - console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md: ${dir}`); - } + console.log(`[DEBUG] collectSkills: dir has type:skill manifest but failed validation: ${dir}`); } } @@ -275,6 +257,50 @@ class ManifestGenerator { } } + /** + * Parse and validate SKILL.md for a skill directory. + * Returns parsed frontmatter object with name/description, or null if invalid. + * @param {string} skillMdPath - Absolute path to SKILL.md + * @param {string} dir - Skill directory path (for error messages) + * @param {string} dirName - Expected name (must match frontmatter name) + * @param {boolean} debug - Whether to emit debug-level messages + * @returns {Promise} Parsed frontmatter or null + */ + async parseSkillMd(skillMdPath, dir, dirName, debug = false) { + if (!(await fs.pathExists(skillMdPath))) { + if (debug) console.log(`[DEBUG] parseSkillMd: "${dir}" is missing SKILL.md — skipping`); + return null; + } + + try { + const rawContent = await fs.readFile(skillMdPath, 'utf8'); + const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); + + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (frontmatterMatch) { + const skillMeta = yaml.parse(frontmatterMatch[1]); + + if (!skillMeta || typeof skillMeta !== 'object' || !skillMeta.name || !skillMeta.description) { + if (debug) console.log(`[DEBUG] parseSkillMd: SKILL.md in "${dir}" is missing name or description — skipping`); + return null; + } + + if (skillMeta.name !== dirName) { + console.error(`Error: SKILL.md name "${skillMeta.name}" does not match directory name "${dirName}" — skipping`); + return null; + } + + return skillMeta; + } + + if (debug) console.log(`[DEBUG] parseSkillMd: SKILL.md in "${dir}" has no frontmatter — skipping`); + return null; + } catch (error) { + if (debug) console.log(`[DEBUG] parseSkillMd: failed to parse SKILL.md in "${dir}": ${error.message} — skipping`); + return null; + } + } + /** * Collect all workflows from core and selected modules * Scans the INSTALLED bmad directory, not the source diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 610913dd7..0a711c05f 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -627,7 +627,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} /** * Install verbatim skill directories (type: skill entries from skill-manifest.csv). - * Copies the entire source directory into the IDE skill directory, auto-generating SKILL.md. + * Copies the entire source directory as-is into the IDE skill directory. + * The source SKILL.md is used directly — no frontmatter transformation or file generation. * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory * @param {string} targetPath - Target skills directory @@ -653,7 +654,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} if (!canonicalId) continue; // Derive source directory from path column - // path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md" + // path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md" // Strip bmadFolderName prefix and join with bmadDir, then get dirname const relativePath = record.path.replace(new RegExp(`^${bmadFolderName}/`), ''); const sourceFile = path.join(bmadDir, relativePath); @@ -666,30 +667,14 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} await fs.remove(skillDir); await fs.ensureDir(skillDir); - // Parse workflow.md frontmatter for description - let description = `${canonicalId} skill`; - try { - const workflowContent = await fs.readFile(sourceFile, 'utf8'); - const fmMatch = workflowContent.match(/^---\r?\n([\s\S]*?)\r?\n---/); - if (fmMatch) { - const frontmatter = yaml.parse(fmMatch[1]); - if (frontmatter?.description) { - description = frontmatter.description; - } - } - } catch (error) { - await prompts.log.warn(`Failed to parse frontmatter from ${sourceFile}: ${error.message}`); - } - - // Generate SKILL.md with YAML-safe frontmatter - const frontmatterYaml = yaml.stringify({ name: canonicalId, description: String(description) }, { lineWidth: 0 }).trimEnd(); - const skillMd = `---\n${frontmatterYaml}\n---\n\nIT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL workflow.md, READ its entire contents and follow its directions exactly!\n`; - await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd); - - // Copy all files except bmad-skill-manifest.yaml + // Copy all skill files, filtering OS/editor artifacts + const skipPatterns = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']); + const skipSuffixes = ['~', '.swp', '.swo', '.bak']; const entries = await fs.readdir(sourceDir, { withFileTypes: true }); for (const entry of entries) { - if (entry.name === 'bmad-skill-manifest.yaml') continue; + if (skipPatterns.has(entry.name)) continue; + if (entry.name.startsWith('.') && entry.name !== '.gitkeep') continue; + if (skipSuffixes.some((s) => entry.name.endsWith(s))) continue; const srcPath = path.join(sourceDir, entry.name); const destPath = path.join(skillDir, entry.name); await fs.copy(srcPath, destPath);