refactor(installer): discover skills by SKILL.md instead of manifest YAML

Switch skill discovery gate from requiring bmad-skill-manifest.yaml with
type: skill to detecting any directory with a valid SKILL.md (frontmatter
name + description, name matches directory name). Delete 34 stub manifests
that carried no data beyond type: skill. Agent manifests (9) are retained
for persona metadata consumed by agent-manifest.csv.
This commit is contained in:
Alex Verkhovsky 2026-03-20 22:51:33 -06:00
parent c28206dca4
commit 02879f7b6b
36 changed files with 53 additions and 154 deletions

View File

@ -1 +0,0 @@
type: skill

View File

@ -1,15 +0,0 @@
type: skill
module: core
capabilities:
- name: bmad-distillator
menu-code: DSTL
description: "Produces lossless LLM-optimized distillate from source documents. Use after producing large human presentable documents that will be consumed later by LLMs"
supports-headless: true
input: source documents
args: output, validate
output: single distillate or folder of distillates next to source input
config-vars-used: null
phase: anytime
before: []
after: []
is-required: false

View File

@ -1 +0,0 @@
type: skill

View File

@ -1 +0,0 @@
type: skill

View File

@ -1 +0,0 @@
type: skill

View File

@ -1 +0,0 @@
type: skill

View File

@ -1 +0,0 @@
type: skill

View File

@ -78,7 +78,6 @@ async function createTestBmadFixture() {
'You are a test agent.', 'You are a test agent.',
].join('\n'), ].join('\n'),
); );
await fs.writeFile(path.join(skillDir, 'bmad-skill-manifest.yaml'), 'SKILL.md:\n type: skill\n');
await fs.writeFile(path.join(skillDir, 'workflow.md'), '# Test Workflow\nStep 1: Do the thing.\n'); await fs.writeFile(path.join(skillDir, 'workflow.md'), '# Test Workflow\nStep 1: Do the thing.\n');
return fixtureDir; return fixtureDir;
@ -1535,7 +1534,6 @@ async function runTests() {
// --- Skill at unusual path: core/custom-area/my-skill/ --- // --- Skill at unusual path: core/custom-area/my-skill/ ---
const skillDir29 = path.join(tempFixture29, 'core', 'custom-area', 'my-skill'); const skillDir29 = path.join(tempFixture29, 'core', 'custom-area', 'my-skill');
await fs.ensureDir(skillDir29); await fs.ensureDir(skillDir29);
await fs.writeFile(path.join(skillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
await fs.writeFile( await fs.writeFile(
path.join(skillDir29, 'SKILL.md'), 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', '---\nname: my-skill\ndescription: A skill at an unusual path\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',
@ -1554,7 +1552,6 @@ async function runTests() {
// --- Skill inside workflows/ dir: core/workflows/wf-skill/ (exercises findWorkflows skip logic) --- // --- Skill inside workflows/ dir: core/workflows/wf-skill/ (exercises findWorkflows skip logic) ---
const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill'); const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill');
await fs.ensureDir(wfSkillDir29); await fs.ensureDir(wfSkillDir29);
await fs.writeFile(path.join(wfSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
await fs.writeFile( await fs.writeFile(
path.join(wfSkillDir29, 'SKILL.md'), 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', '---\nname: wf-skill\ndescription: A skill inside workflows dir\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',
@ -1564,7 +1561,6 @@ async function runTests() {
// --- Skill inside tasks/ dir: core/tasks/task-skill/ --- // --- Skill inside tasks/ dir: core/tasks/task-skill/ ---
const taskSkillDir29 = path.join(tempFixture29, 'core', 'tasks', 'task-skill'); const taskSkillDir29 = path.join(tempFixture29, 'core', 'tasks', 'task-skill');
await fs.ensureDir(taskSkillDir29); await fs.ensureDir(taskSkillDir29);
await fs.writeFile(path.join(taskSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
await fs.writeFile( await fs.writeFile(
path.join(taskSkillDir29, 'SKILL.md'), 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', '---\nname: task-skill\ndescription: A skill inside tasks dir\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',
@ -1636,7 +1632,6 @@ async function runTests() {
// Test scanInstalledModules recognizes skill-only modules // Test scanInstalledModules recognizes skill-only modules
const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod'); const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod');
await fs.ensureDir(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill')); 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( await fs.writeFile(
path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'SKILL.md'), 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', '---\nname: my-skill\ndescription: desc\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',

View File

@ -50,29 +50,6 @@ class ManifestGenerator {
return getInstallToBmadShared(manifest, filename); return getInstallToBmadShared(manifest, filename);
} }
/**
* Native SKILL.md entrypoints can be packaged as either skills or agents.
* Both need verbatim installation for skill-format IDEs.
* @param {string|null} artifactType - Manifest type resolved for SKILL.md
* @returns {boolean} True when the directory should be installed verbatim
*/
isNativeSkillDirType(artifactType) {
return artifactType === 'skill' || artifactType === 'agent';
}
/**
* Check whether a loaded bmad-skill-manifest.yaml declares a native
* SKILL.md entrypoint, either as a single-entry manifest or a multi-entry map.
* @param {Object|null} manifest - Loaded manifest
* @returns {boolean} True when the manifest contains a native skill/agent entrypoint
*/
hasNativeSkillManifest(manifest) {
if (!manifest) return false;
if (manifest.__single) return this.isNativeSkillDirType(manifest.__single.type);
return Object.values(manifest).some((entry) => this.isNativeSkillDirType(entry?.type));
}
/** /**
* Clean text for CSV output by normalizing whitespace. * Clean text for CSV output by normalizing whitespace.
* Note: Quote escaping is handled by escapeCsv() at write time. * Note: Quote escaping is handled by escapeCsv() at write time.
@ -170,9 +147,9 @@ class ManifestGenerator {
/** /**
* Recursively walk a module directory tree, collecting native SKILL.md entrypoints. * Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
* A native entrypoint directory is one that contains both a * A directory is discovered as a skill when it contains a SKILL.md file with
* bmad-skill-manifest.yaml with type: skill or type: agent AND a SKILL.md file * valid name/description frontmatter (name must match directory name).
* with name/description frontmatter. * Manifest YAML is loaded only when present for install_to_bmad and agent metadata.
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths). * Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
*/ */
async collectSkills() { async collectSkills() {
@ -193,77 +170,55 @@ class ManifestGenerator {
return; return;
} }
// Check this directory for skill manifest // SKILL.md with valid frontmatter is the primary discovery gate
const manifest = await this.loadSkillManifest(dir);
// Determine if this directory is a native SKILL.md entrypoint
const skillFile = 'SKILL.md'; const skillFile = 'SKILL.md';
const artifactType = this.getArtifactType(manifest, skillFile); const skillMdPath = path.join(dir, skillFile);
const dirName = path.basename(dir);
if (this.isNativeSkillDirType(artifactType)) { const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
const skillMdPath = path.join(dir, 'SKILL.md');
const dirName = path.basename(dir);
// Validate and parse SKILL.md if (skillMeta) {
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug); // Load manifest when present (for install_to_bmad and agent metadata)
const manifest = await this.loadSkillManifest(dir);
const artifactType = this.getArtifactType(manifest, skillFile);
if (skillMeta) { // Build path relative from module root (points to SKILL.md — the permanent entrypoint)
// Build path relative from module root (points to SKILL.md — the permanent entrypoint) const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/'); const installPath = relativePath
const installPath = relativePath ? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}` : `${this.bmadFolderName}/${moduleName}/${skillFile}`;
: `${this.bmadFolderName}/${moduleName}/${skillFile}`;
// Native SKILL.md entrypoints derive canonicalId from directory name. // Native SKILL.md entrypoints derive canonicalId from directory name.
// Agent entrypoints may keep canonicalId metadata for compatibility, so // Agent entrypoints may keep canonicalId metadata for compatibility, so
// only warn for non-agent SKILL.md directories. // only warn for non-agent SKILL.md directories.
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') { if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
console.warn( console.warn(
`Warning: Native entrypoint manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for SKILL.md directories (directory name is the canonical ID)`, `Warning: Native entrypoint manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for SKILL.md directories (directory name is the canonical ID)`,
); );
}
const canonicalId = dirName;
this.skills.push({
name: skillMeta.name,
description: this.cleanForCSV(skillMeta.description),
module: moduleName,
path: installPath,
canonicalId,
install_to_bmad: this.getInstallToBmad(manifest, skillFile),
});
// Add to files list
this.files.push({
type: 'skill',
name: skillMeta.name,
module: moduleName,
path: installPath,
});
this.skillClaimedDirs.add(dir);
if (debug) {
console.log(`[DEBUG] collectSkills: claimed skill "${skillMeta.name}" as ${canonicalId} at ${dir}`);
}
} }
} const canonicalId = dirName;
// Warn if manifest says this is a native entrypoint but the directory was not claimed this.skills.push({
if (manifest && !this.skillClaimedDirs.has(dir)) { name: skillMeta.name,
let hasNativeSkillType = false; description: this.cleanForCSV(skillMeta.description),
if (manifest.__single) { module: moduleName,
hasNativeSkillType = this.isNativeSkillDirType(manifest.__single.type); path: installPath,
} else { canonicalId,
for (const key of Object.keys(manifest)) { install_to_bmad: this.getInstallToBmad(manifest, skillFile),
if (this.isNativeSkillDirType(manifest[key]?.type)) { });
hasNativeSkillType = true;
break; // Add to files list
} this.files.push({
} type: 'skill',
} name: skillMeta.name,
if (hasNativeSkillType && debug) { module: moduleName,
console.log(`[DEBUG] collectSkills: dir has native SKILL.md manifest but failed validation: ${dir}`); path: installPath,
});
this.skillClaimedDirs.add(dir);
if (debug) {
console.log(`[DEBUG] collectSkills: claimed skill "${skillMeta.name}" as ${canonicalId} at ${dir}`);
} }
} }
@ -1384,11 +1339,10 @@ class ManifestGenerator {
const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks')); const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks'));
const hasTools = await fs.pathExists(path.join(modulePath, 'tools')); const hasTools = await fs.pathExists(path.join(modulePath, 'tools'));
// Check for native-entrypoint-only modules: recursive scan for // Check for native-entrypoint-only modules: recursive scan for SKILL.md
// bmad-skill-manifest.yaml with type: skill or type: agent
let hasSkills = false; let hasSkills = false;
if (!hasAgents && !hasWorkflows && !hasTasks && !hasTools) { if (!hasAgents && !hasWorkflows && !hasTasks && !hasTools) {
hasSkills = await this._hasSkillManifestRecursive(modulePath); hasSkills = await this._hasSkillMdRecursive(modulePath);
} }
// If it has any of these directories or skill manifests, it's likely a module // If it has any of these directories or skill manifests, it's likely a module
@ -1404,13 +1358,12 @@ class ManifestGenerator {
} }
/** /**
* Recursively check if a directory tree contains a bmad-skill-manifest.yaml that * Recursively check if a directory tree contains a SKILL.md file.
* declares a native SKILL.md entrypoint (type: skill or type: agent).
* Skips directories starting with . or _. * Skips directories starting with . or _.
* @param {string} dir - Directory to search * @param {string} dir - Directory to search
* @returns {boolean} True if a skill manifest is found * @returns {boolean} True if a SKILL.md is found
*/ */
async _hasSkillManifestRecursive(dir) { async _hasSkillMdRecursive(dir) {
let entries; let entries;
try { try {
entries = await fs.readdir(dir, { withFileTypes: true }); entries = await fs.readdir(dir, { withFileTypes: true });
@ -1418,15 +1371,14 @@ class ManifestGenerator {
return false; return false;
} }
// Check for manifest in this directory // Check for SKILL.md in this directory
const manifest = await this.loadSkillManifest(dir); if (entries.some((e) => !e.isDirectory() && e.name === 'SKILL.md')) return true;
if (this.hasNativeSkillManifest(manifest)) return true;
// Recurse into subdirectories // Recurse into subdirectories
for (const entry of entries) { for (const entry of entries) {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue; if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
if (await this._hasSkillManifestRecursive(path.join(dir, entry.name))) return true; if (await this._hasSkillMdRecursive(path.join(dir, entry.name))) return true;
} }
return false; return false;