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.',
].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');
return fixtureDir;
@ -1535,7 +1534,6 @@ async function runTests() {
// --- Skill at unusual path: core/custom-area/my-skill/ ---
const skillDir29 = path.join(tempFixture29, 'core', 'custom-area', 'my-skill');
await fs.ensureDir(skillDir29);
await fs.writeFile(path.join(skillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
await fs.writeFile(
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',
@ -1554,7 +1552,6 @@ async function runTests() {
// --- Skill inside workflows/ dir: core/workflows/wf-skill/ (exercises findWorkflows skip logic) ---
const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill');
await fs.ensureDir(wfSkillDir29);
await fs.writeFile(path.join(wfSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
await fs.writeFile(
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',
@ -1564,7 +1561,6 @@ async function runTests() {
// --- 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, 'SKILL.md'),
'---\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
const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod');
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', 'SKILL.md'),
'---\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);
}
/**
* 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.
* 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.
* A native entrypoint directory is one that contains both a
* bmad-skill-manifest.yaml with type: skill or type: agent AND a SKILL.md file
* with name/description frontmatter.
* A directory is discovered as a skill when it contains a SKILL.md file with
* valid name/description frontmatter (name must match directory name).
* Manifest YAML is loaded only when present for install_to_bmad and agent metadata.
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
*/
async collectSkills() {
@ -193,77 +170,55 @@ class ManifestGenerator {
return;
}
// Check this directory for skill manifest
const manifest = await this.loadSkillManifest(dir);
// Determine if this directory is a native SKILL.md entrypoint
// SKILL.md with valid frontmatter is the primary discovery gate
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 skillMdPath = path.join(dir, 'SKILL.md');
const dirName = path.basename(dir);
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
// Validate and parse SKILL.md
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
if (skillMeta) {
// 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)
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
const installPath = relativePath
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
: `${this.bmadFolderName}/${moduleName}/${skillFile}`;
// 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}`;
// Native SKILL.md entrypoints derive canonicalId from directory name.
// Agent entrypoints may keep canonicalId metadata for compatibility, so
// only warn for non-agent SKILL.md directories.
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
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)`,
);
}
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}`);
}
// Native SKILL.md entrypoints derive canonicalId from directory name.
// Agent entrypoints may keep canonicalId metadata for compatibility, so
// only warn for non-agent SKILL.md directories.
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
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)`,
);
}
}
const canonicalId = dirName;
// Warn if manifest says this is a native entrypoint but the directory was not claimed
if (manifest && !this.skillClaimedDirs.has(dir)) {
let hasNativeSkillType = false;
if (manifest.__single) {
hasNativeSkillType = this.isNativeSkillDirType(manifest.__single.type);
} else {
for (const key of Object.keys(manifest)) {
if (this.isNativeSkillDirType(manifest[key]?.type)) {
hasNativeSkillType = true;
break;
}
}
}
if (hasNativeSkillType && debug) {
console.log(`[DEBUG] collectSkills: dir has native SKILL.md manifest but failed validation: ${dir}`);
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}`);
}
}
@ -1384,11 +1339,10 @@ class ManifestGenerator {
const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks'));
const hasTools = await fs.pathExists(path.join(modulePath, 'tools'));
// Check for native-entrypoint-only modules: recursive scan for
// bmad-skill-manifest.yaml with type: skill or type: agent
// Check for native-entrypoint-only modules: recursive scan for SKILL.md
let hasSkills = false;
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
@ -1404,13 +1358,12 @@ class ManifestGenerator {
}
/**
* Recursively check if a directory tree contains a bmad-skill-manifest.yaml that
* declares a native SKILL.md entrypoint (type: skill or type: agent).
* Recursively check if a directory tree contains a SKILL.md file.
* Skips directories starting with . or _.
* @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;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
@ -1418,15 +1371,14 @@ class ManifestGenerator {
return false;
}
// Check for manifest in this directory
const manifest = await this.loadSkillManifest(dir);
if (this.hasNativeSkillManifest(manifest)) return true;
// Check for SKILL.md in this directory
if (entries.some((e) => !e.isDirectory() && e.name === 'SKILL.md')) return true;
// Recurse into subdirectories
for (const entry of entries) {
if (!entry.isDirectory()) 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;