diff --git a/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml b/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml index dd02073a6..9c88e320a 100644 --- a/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +++ b/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-analyst displayName: Mary title: Business Analyst diff --git a/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml b/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml index 24af1bfc8..2aba65602 100644 --- a/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +++ b/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-tech-writer displayName: Paige title: Technical Writer diff --git a/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml b/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml index 85a2fde52..c38b5e1ed 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +++ b/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-pm displayName: John title: Product Manager diff --git a/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml b/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml index bae324913..ca0983b4b 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +++ b/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-ux-designer displayName: Sally title: UX Designer diff --git a/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml b/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml index df54e57ed..ed1006ddd 100644 --- a/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +++ b/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-architect displayName: Winston title: Architect diff --git a/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml index 2feeb538a..c6ca829c2 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-dev displayName: Amelia title: Developer Agent diff --git a/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml index 5d561cd2b..ebf5e98bb 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-qa displayName: Quinn title: QA Engineer diff --git a/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml index 107435a3a..63013f345 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-quick-flow-solo-dev displayName: Barry title: Quick Flow Solo Dev diff --git a/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml index f1f46f84b..71fc35fa6 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-sm displayName: Bob title: Scrum Master diff --git a/test/test-installation-components.js b/test/test-installation-components.js index d75ec9871..8b6f505de 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1648,6 +1648,93 @@ async function runTests() { // 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'); + + // --- Agents at non-agents/ paths (regression test for BMM/CIS layouts) --- + // Create a second fixture with agents at paths like bmm/1-analysis/bmad-agent-analyst/ + const tempFixture29b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-agent-paths-')); + await fs.ensureDir(path.join(tempFixture29b, '_config')); + + // Agent at bmm-style path: bmm/1-analysis/bmad-agent-analyst/ + const bmmAgentDir = path.join(tempFixture29b, 'bmm', '1-analysis', 'bmad-agent-analyst'); + await fs.ensureDir(bmmAgentDir); + await fs.writeFile( + path.join(bmmAgentDir, 'bmad-skill-manifest.yaml'), + [ + 'type: agent', + 'name: bmad-agent-analyst', + 'displayName: Mary', + 'title: Business Analyst', + 'role: Strategic Business Analyst', + 'module: bmm', + ].join('\n') + '\n', + ); + await fs.writeFile( + path.join(bmmAgentDir, 'SKILL.md'), + '---\nname: bmad-agent-analyst\ndescription: Business Analyst agent\n---\n\nAnalyst agent.\n', + ); + + // Agent at cis-style path: cis/skills/bmad-cis-agent-brainstorming-coach/ + const cisAgentDir = path.join(tempFixture29b, 'cis', 'skills', 'bmad-cis-agent-brainstorming-coach'); + await fs.ensureDir(cisAgentDir); + await fs.writeFile( + path.join(cisAgentDir, 'bmad-skill-manifest.yaml'), + [ + 'type: agent', + 'name: bmad-cis-agent-brainstorming-coach', + 'displayName: Carson', + 'title: Brainstorming Specialist', + 'role: Master Facilitator', + 'module: cis', + ].join('\n') + '\n', + ); + await fs.writeFile( + path.join(cisAgentDir, 'SKILL.md'), + '---\nname: bmad-cis-agent-brainstorming-coach\ndescription: Brainstorming coach\n---\n\nCoach.\n', + ); + + // Agent at standard agents/ path (GDS-style): gds/agents/gds-agent-game-dev/ + const gdsAgentDir = path.join(tempFixture29b, 'gds', 'agents', 'gds-agent-game-dev'); + await fs.ensureDir(gdsAgentDir); + await fs.writeFile( + path.join(gdsAgentDir, 'bmad-skill-manifest.yaml'), + [ + 'type: agent', + 'name: gds-agent-game-dev', + 'displayName: Link', + 'title: Game Developer', + 'role: Senior Game Dev', + 'module: gds', + ].join('\n') + '\n', + ); + await fs.writeFile( + path.join(gdsAgentDir, 'SKILL.md'), + '---\nname: gds-agent-game-dev\ndescription: Game developer agent\n---\n\nGame dev.\n', + ); + + const generator29b = new ManifestGenerator(); + await generator29b.generateManifests(tempFixture29b, ['bmm', 'cis', 'gds'], [], { ides: [] }); + + // All three agents should appear in agents[] regardless of directory layout + const bmmAgent = generator29b.agents.find((a) => a.name === 'bmad-agent-analyst'); + assert(bmmAgent !== undefined, 'Agent at bmm/1-analysis/ path appears in agents[]'); + assert(bmmAgent && bmmAgent.module === 'bmm', 'BMM agent module field comes from manifest file'); + assert(bmmAgent && bmmAgent.path.includes('bmm/1-analysis/bmad-agent-analyst'), 'BMM agent path reflects actual directory layout'); + + const cisAgent = generator29b.agents.find((a) => a.name === 'bmad-cis-agent-brainstorming-coach'); + assert(cisAgent !== undefined, 'Agent at cis/skills/ path appears in agents[]'); + assert(cisAgent && cisAgent.module === 'cis', 'CIS agent module field comes from manifest file'); + + const gdsAgent = generator29b.agents.find((a) => a.name === 'gds-agent-game-dev'); + assert(gdsAgent !== undefined, 'Agent at gds/agents/ path appears in agents[]'); + assert(gdsAgent && gdsAgent.module === 'gds', 'GDS agent module field comes from manifest file'); + + // agent-manifest.csv should contain all three + const agentCsv29b = await fs.readFile(path.join(tempFixture29b, '_config', 'agent-manifest.csv'), 'utf8'); + assert(agentCsv29b.includes('bmad-agent-analyst'), 'agent-manifest.csv includes BMM-layout agent'); + assert(agentCsv29b.includes('bmad-cis-agent-brainstorming-coach'), 'agent-manifest.csv includes CIS-layout agent'); + assert(agentCsv29b.includes('gds-agent-game-dev'), 'agent-manifest.csv includes GDS-layout agent'); + + await fs.remove(tempFixture29b).catch(() => {}); } 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 9ada35dc0..14fd8887e 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -268,153 +268,103 @@ class ManifestGenerator { } /** - * Collect all agents from core and selected modules - * Scans the INSTALLED bmad directory, not the source + * Collect all agents from selected modules by walking their directory trees. */ async collectAgents(selectedModules) { this.agents = []; + const debug = process.env.BMAD_DEBUG_MANIFEST === 'true'; - // Use updatedModules which already includes deduplicated 'core' + selectedModules + // Walk each module's full directory tree looking for type:agent manifests for (const moduleName of this.updatedModules) { - const agentsPath = path.join(this.bmadDir, moduleName, 'agents'); + const modulePath = path.join(this.bmadDir, moduleName); + if (!(await fs.pathExists(modulePath))) continue; - if (await fs.pathExists(agentsPath)) { - const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName); - this.agents.push(...moduleAgents); - } + const moduleAgents = await this.getAgentsFromDirRecursive(modulePath, moduleName, '', debug); + this.agents.push(...moduleAgents); } // Get standalone agents from bmad/agents/ directory const standaloneAgentsDir = path.join(this.bmadDir, 'agents'); if (await fs.pathExists(standaloneAgentsDir)) { - const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true }); + const standaloneAgents = await this.getAgentsFromDirRecursive(standaloneAgentsDir, 'standalone', '', debug); + this.agents.push(...standaloneAgents); + } - for (const agentDir of agentDirs) { - if (!agentDir.isDirectory()) continue; - - const agentDirPath = path.join(standaloneAgentsDir, agentDir.name); - const standaloneAgents = await this.getAgentsFromDir(agentDirPath, 'standalone'); - this.agents.push(...standaloneAgents); - } + if (debug) { + console.log(`[DEBUG] collectAgents: total agents found: ${this.agents.length}`); } } /** - * Get agents from a directory recursively - * Only includes .md files with agent content + * Recursively walk a directory tree collecting agents. + * Discovers agents via directory with bmad-skill-manifest.yaml containing type: agent + * + * @param {string} dirPath - Current directory being scanned + * @param {string} moduleName - Module this directory belongs to + * @param {string} relativePath - Path relative to the module root (for install path construction) + * @param {boolean} debug - Emit debug messages */ - async getAgentsFromDir(dirPath, moduleName, relativePath = '') { - // Skip directories claimed by collectSkills - if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return []; + async getAgentsFromDirRecursive(dirPath, moduleName, relativePath = '', debug = false) { const agents = []; - const entries = await fs.readdir(dirPath, { withFileTypes: true }); - // Load skill manifest for this directory (if present) - const skillManifest = await this.loadSkillManifest(dirPath); + let entries; + try { + entries = await fs.readdir(dirPath, { withFileTypes: true }); + } catch { + return agents; + } for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue; + const fullPath = path.join(dirPath, entry.name); - if (entry.isDirectory()) { - // Check for new-format agent: bmad-skill-manifest.yaml with type: agent - // Note: type:agent dirs may also be claimed by collectSkills for IDE installation, - // but we still need to process them here for agent-manifest.csv - const dirManifest = await this.loadSkillManifest(fullPath); - if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') { - const m = dirManifest.__single; - const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; - const installPath = - moduleName === 'core' - ? `${this.bmadFolderName}/core/agents/${dirRelativePath}` - : `${this.bmadFolderName}/${moduleName}/agents/${dirRelativePath}`; - - agents.push({ - name: m.name || entry.name, - displayName: m.displayName || m.name || entry.name, - title: m.title || '', - icon: m.icon || '', - capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '', - role: m.role ? this.cleanForCSV(m.role) : '', - identity: m.identity ? this.cleanForCSV(m.identity) : '', - communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '', - principles: m.principles ? this.cleanForCSV(m.principles) : '', - module: m.module || moduleName, - path: installPath, - canonicalId: m.canonicalId || '', - }); - - this.files.push({ - type: 'agent', - name: m.name || entry.name, - module: moduleName, - path: installPath, - }); - continue; - } - - // Skip directories claimed by collectSkills (non-agent type skills) - if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue; - - // Recurse into subdirectories - const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; - const subDirAgents = await this.getAgentsFromDir(fullPath, moduleName, newRelativePath); - agents.push(...subDirAgents); - } else if (entry.name.endsWith('.md') && entry.name.toLowerCase() !== 'readme.md') { - const content = await fs.readFile(fullPath, 'utf8'); - - // Skip files that don't contain tag (e.g., README files) - if (!content.includes('([^<]+)<\/role>/); - const identityMatch = content.match(/([\s\S]*?)<\/identity>/); - const styleMatch = content.match(/([\s\S]*?)<\/communication_style>/); - const principlesMatch = content.match(/([\s\S]*?)<\/principles>/); - - // Build relative path for installation - const fileRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; - const installPath = - moduleName === 'core' - ? `${this.bmadFolderName}/core/agents/${fileRelativePath}` - : `${this.bmadFolderName}/${moduleName}/agents/${fileRelativePath}`; - - const agentName = entry.name.replace('.md', ''); + // Check for type:agent manifest BEFORE checking skillClaimedDirs — + // agent dirs may be claimed by collectSkills for IDE installation, + // but we still need them in agent-manifest.csv. + const dirManifest = await this.loadSkillManifest(fullPath); + if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') { + const m = dirManifest.__single; + const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + const agentModule = m.module || moduleName; + const installPath = `${this.bmadFolderName}/${agentModule}/${dirRelativePath}`; agents.push({ - name: agentName, - displayName: nameMatch ? nameMatch[1] : agentName, - title: titleMatch ? titleMatch[1] : '', - icon: iconMatch ? iconMatch[1] : '', - capabilities: capabilitiesMatch ? this.cleanForCSV(capabilitiesMatch[1]) : '', - role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '', - identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '', - communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '', - principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', - module: moduleName, + name: m.name || entry.name, + displayName: m.displayName || m.name || entry.name, + title: m.title || '', + icon: m.icon || '', + capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '', + role: m.role ? this.cleanForCSV(m.role) : '', + identity: m.identity ? this.cleanForCSV(m.identity) : '', + communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '', + principles: m.principles ? this.cleanForCSV(m.principles) : '', + module: agentModule, path: installPath, - canonicalId: this.getCanonicalId(skillManifest, entry.name), + canonicalId: m.canonicalId || '', }); - // Add to files list this.files.push({ type: 'agent', - name: agentName, - module: moduleName, + name: m.name || entry.name, + module: agentModule, path: installPath, }); + + if (debug) { + console.log(`[DEBUG] collectAgents: found type:agent "${m.name || entry.name}" at ${fullPath}`); + } + continue; } + + // Skip directories claimed by collectSkills (non-agent type skills) — + // avoids recursing into skill trees that can't contain agents. + if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue; + + // Recurse into subdirectories + const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + const subDirAgents = await this.getAgentsFromDirRecursive(fullPath, moduleName, newRelativePath, debug); + agents.push(...subDirAgents); } return agents; diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index d3edf0cd2..ac0dbd190 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -5,6 +5,33 @@ const { loadSkillManifest, getCanonicalId } = require('./skill-manifest'); /** * Helpers for gathering BMAD agents/tasks from the installed tree. * Shared by installers that need Claude-style exports. + * + * TODO: Dead code cleanup — compiled XML agents are retired. + * + * All agents now use the SKILL.md directory format with bmad-skill-manifest.yaml + * (type: agent). The legacy pipeline below only discovers compiled .md files + * containing XML tags, which no longer exist. The following are dead: + * + * - getAgentsFromBmad() — scans {module}/agents/ for .md files with tags + * - getAgentsFromDir() — recursive helper for the above + * - AgentCommandGenerator — (agent-command-generator.js) generates launcher .md files + * that tell the LLM to load a compiled agent .md file + * - agent-command-template.md — (templates/) the launcher template with hardcoded + * {module}/agents/{{path}} reference + * + * Agent metadata for agent-manifest.csv is now handled entirely by + * ManifestGenerator.getAgentsFromDirRecursive() in manifest-generator.js, + * which walks the full module tree and finds type:agent directories. + * + * IDE installation of agents is handled by the native skill pipeline — + * each agent's SKILL.md directory is installed directly to the IDE's + * skills path, so no launcher intermediary is needed. + * + * Cleanup: remove getAgentsFromBmad, getAgentsFromDir, their exports, + * AgentCommandGenerator, agent-command-template.md, and all call sites + * in IDE installers that invoke collectAgentArtifacts / writeAgentLaunchers / + * writeColonArtifacts / writeDashArtifacts. + * getTasksFromBmad and getTasksFromDir may still be live — verify before removing. */ async function getAgentsFromBmad(bmadDir, selectedModules = []) { const agents = [];