diff --git a/test/test-installation-components.js b/test/test-installation-components.js index dda834079..fd3a854ba 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1635,6 +1635,15 @@ async function runTests() { ); await fs.writeFile(path.join(taskSkillDir29, 'workflow.md'), '# Task Skill\n\nSkill in tasks\n'); + // --- Native agent entrypoint inside agents/: core/agents/bmad-tea/ --- + const nativeAgentDir29 = path.join(tempFixture29, 'core', 'agents', 'bmad-tea'); + await fs.ensureDir(nativeAgentDir29); + await fs.writeFile(path.join(nativeAgentDir29, 'bmad-skill-manifest.yaml'), 'type: agent\ncanonicalId: bmad-tea\n'); + await fs.writeFile( + path.join(nativeAgentDir29, 'SKILL.md'), + '---\nname: bmad-tea\ndescription: Native agent entrypoint\n---\n\nPresent a capability menu.\n', + ); + // Minimal agent so core module is detected await fs.ensureDir(path.join(tempFixture29, 'core', 'agents')); const minimalAgent29 = 'p'; @@ -1664,6 +1673,17 @@ async function runTests() { const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill'); assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]'); + // Native agent entrypoint should be installed as a verbatim skill and also + // remain visible to the agent manifest pipeline. + const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea'); + assert(nativeAgentEntry29 !== undefined, 'Native type:agent SKILL.md dir appears in skills[]'); + assert( + nativeAgentEntry29 && nativeAgentEntry29.path.includes('agents/bmad-tea/SKILL.md'), + 'Native type:agent SKILL.md path points to the agent directory entrypoint', + ); + const nativeAgentManifest29 = generator29.agents.find((a) => a.name === 'bmad-tea'); + assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata'); + // Regular workflow should be in workflows, NOT in skills const regularWf29 = generator29.workflows.find((w) => w.name === 'Regular Workflow'); assert(regularWf29 !== undefined, 'Regular type:workflow appears in workflows[]'); @@ -1689,6 +1709,22 @@ async function runTests() { const scannedModules29 = await generator29.scanInstalledModules(tempFixture29); assert(scannedModules29.includes('skill-only-mod'), 'scanInstalledModules recognizes skill-only module'); + + // Test scanInstalledModules recognizes native-agent-only modules too + const agentOnlyModDir29 = path.join(tempFixture29, 'agent-only-mod'); + await fs.ensureDir(path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea')); + await fs.writeFile(path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea', 'bmad-skill-manifest.yaml'), 'type: agent\n'); + await fs.writeFile( + path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea', 'SKILL.md'), + '---\nname: bmad-tea\ndescription: desc\n---\n\nAgent menu.\n', + ); + + const rescannedModules29 = await generator29.scanInstalledModules(tempFixture29); + assert(rescannedModules29.includes('agent-only-mod'), 'scanInstalledModules recognizes native-agent-only module'); + + // skill-manifest.csv should include the native agent entrypoint + const skillManifestCsv29 = await fs.readFile(path.join(tempFixture29, '_config', 'skill-manifest.csv'), 'utf8'); + assert(skillManifestCsv29.includes('bmad-tea'), 'skill-manifest.csv includes native type:agent SKILL.md entrypoint'); } catch (error) { assert(false, 'Unified skill scanner test succeeds', error.message); } finally { diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 68d0c9eab..7932f1516 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -50,6 +50,16 @@ 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'; + } + /** * Clean text for CSV output by normalizing whitespace. * Note: Quote escaping is handled by escapeCsv() at write time. @@ -146,9 +156,10 @@ 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 SKILL.md file with name/description frontmatter. + * 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. * Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths). */ async collectSkills() { @@ -172,11 +183,11 @@ class ManifestGenerator { // Check this directory for skill manifest const manifest = await this.loadSkillManifest(dir); - // Determine if this directory is a skill (type: skill in manifest) + // Determine if this directory is a native SKILL.md entrypoint const skillFile = 'SKILL.md'; const artifactType = this.getArtifactType(manifest, skillFile); - if (artifactType === 'skill' || artifactType === 'agent') { + if (this.isNativeSkillDirType(artifactType)) { const skillMdPath = path.join(dir, 'SKILL.md'); const dirName = path.basename(dir); @@ -190,11 +201,12 @@ class ManifestGenerator { ? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}` : `${this.bmadFolderName}/${moduleName}/${skillFile}`; - // Skills derive canonicalId from directory name — never from manifest - // (agent-type skills legitimately use canonicalId for agent-manifest mapping, so skip warning) + // 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: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (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; @@ -224,21 +236,21 @@ class ManifestGenerator { } } - // Warn if manifest says type:skill but directory was not claimed + // Warn if manifest says this is a native entrypoint but the directory was not claimed if (manifest && !this.skillClaimedDirs.has(dir)) { - let hasSkillType = false; + let hasNativeSkillType = false; if (manifest.__single) { - hasSkillType = manifest.__single.type === 'skill' || manifest.__single.type === 'agent'; + hasNativeSkillType = this.isNativeSkillDirType(manifest.__single.type); } else { for (const key of Object.keys(manifest)) { - if (manifest[key]?.type === 'skill' || manifest[key]?.type === 'agent') { - hasSkillType = true; + if (this.isNativeSkillDirType(manifest[key]?.type)) { + hasNativeSkillType = true; break; } } } - if (hasSkillType && debug) { - console.log(`[DEBUG] collectSkills: dir has type:skill manifest but failed validation: ${dir}`); + if (hasNativeSkillType && debug) { + console.log(`[DEBUG] collectSkills: dir has native SKILL.md manifest but failed validation: ${dir}`); } } @@ -1359,7 +1371,8 @@ class ManifestGenerator { const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks')); const hasTools = await fs.pathExists(path.join(modulePath, 'tools')); - // Check for skill-only modules: recursive scan for bmad-skill-manifest.yaml with type: skill + // Check for native-entrypoint-only modules: recursive scan for + // bmad-skill-manifest.yaml with type: skill or type: agent let hasSkills = false; if (!hasAgents && !hasWorkflows && !hasTasks && !hasTools) { hasSkills = await this._hasSkillManifestRecursive(modulePath); @@ -1378,7 +1391,8 @@ class ManifestGenerator { } /** - * Recursively check if a directory tree contains a bmad-skill-manifest.yaml with type: skill. + * Recursively check if a directory tree contains a bmad-skill-manifest.yaml with + * type: skill or type: agent. * Skips directories starting with . or _. * @param {string} dir - Directory to search * @returns {boolean} True if a skill manifest is found @@ -1395,7 +1409,7 @@ class ManifestGenerator { const manifest = await this.loadSkillManifest(dir); if (manifest) { const type = this.getArtifactType(manifest, 'workflow.md'); - if (type === 'skill') return true; + if (this.isNativeSkillDirType(type)) return true; } // Recurse into subdirectories diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index a93fe0c87..e94cb9edb 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -630,7 +630,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} } /** - * Install verbatim skill directories (type: skill entries from skill-manifest.csv). + * Install verbatim native SKILL.md directories from skill-manifest.csv. * 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